PK!7dgaphor/UML/__init__.pyfrom gaphor.UML import modelfactory as model from gaphor.UML.collection import collection from gaphor.UML.elementfactory import ElementFactory from gaphor.UML.uml2 import * from gaphor.UML.umlfmt import format from gaphor.UML.umllex import parse PK!Vy.gaphor/UML/collection.py""" 1:n and n:m relations in the data model are saved using a collection. """ import inspect from gaphor.UML.event import AssociationChangeEvent from gaphor.misc.listmixins import querymixin, recursemixin, getslicefix class collectionlist(recursemixin, querymixin, getslicefix, list): """ >>> c = collectionlist() >>> c.append('a') >>> c.append('b') >>> c.append('c') >>> c ['a', 'b', 'c'] It should work with the datamodel too: >>> from gaphor.UML import * >>> c = Class() >>> c.ownedOperation = Operation() >>> c.ownedOperation # doctest: +ELLIPSIS [] >>> c.ownedOperation[0] # doctest: +ELLIPSIS >>> c.ownedOperation = Operation() >>> c.ownedOperation[0].formalParameter = Parameter() >>> c.ownedOperation[0].formalParameter = Parameter() >>> c.ownedOperation[0].formalParameter[0].name = 'foo' >>> c.ownedOperation[0].formalParameter[0].name 'foo' >>> c.ownedOperation[0].formalParameter[1].name = 'bar' >>> list(c.ownedOperation[0].formalParameter[:].name) ['foo', 'bar'] >>> c.ownedOperation[:].formalParameter.name # doctest: +ELLIPSIS >>> list(c.ownedOperation[:].formalParameter.name) ['foo', 'bar'] >>> c.ownedOperation[0].formalParameter['it.name=="foo"', 0].name 'foo' >>> c.ownedOperation[:].formalParameter['it.name=="foo"', 0].name 'foo' """ class collection(object): """ Collection (set-like) for model elements' 1:n and n:m relationships. """ def __init__(self, property, object, type): self.property = property self.object = object self.type = type self.items = collectionlist() def __len__(self): return len(self.items) def __setitem__(self, key, value): raise RuntimeError("items should not be overwritten.") def __delitem__(self, key): self.remove(key) def __getitem__(self, key): return self.items.__getitem__(key) def __contains__(self, obj): return self.items.__contains__(obj) def __iter__(self): return iter(self.items) def __str__(self): return str(self.items) __repr__ = __str__ def __bool__(self): return self.items != [] # Maintains Python2 Compatibility __nonzero__ = __bool__ def append(self, value): if isinstance(value, self.type): self.property._set(self.object, value) else: raise TypeError("Object is not of type %s" % self.type.__name__) def remove(self, value): if value in self.items: self.property.__delete__(self.object, value) else: raise ValueError("%s not in collection" % value) def index(self, key): """ Given an object, return the position of that object in the collection. """ return self.items.index(key) # OCL members (from SMW by Ivan Porres, http://www.abo.fi/~iporres/smw) def size(self): return len(self.items) def includes(self, o): return o in self.items def excludes(self, o): return not self.includes(o) def count(self, o): c = 0 for x in self.items: if x == o: c = c + 1 return c def includesAll(self, c): for o in c: if o not in self.items: return 0 return 1 def excludesAll(self, c): for o in c: if o in self.items: return 0 return 1 def select(self, f): result = list() for v in self.items: if f(v): result.append(v) return result def reject(self, f): result = list() for v in self.items: if not f(v): result.append(v) return result def collect(self, f): result = list() for v in self.items: result.append(f(v)) return result def isEmpty(self): return len(self.items) == 0 def nonEmpty(self): return not self.isEmpty() def sum(self): r = 0 for o in self.items: r = r + o return o def forAll(self, f): if not self.items or not inspect.getargspec(f)[0]: return True nargs = len(inspect.getargspec(f)[0]) if inspect.getargspec(f)[3]: nargs = nargs - len(inspect.getargspec(f)[3]) assert nargs > 0 nitems = len(self.items) index = [0] * nargs while True: args = [] for x in index: args.append(self.items[x]) if not f(*args): return False c = len(index) - 1 index[c] = index[c] + 1 while index[c] == nitems: index[c] = 0 c = c - 1 if c < 0: return True else: index[c] = index[c] + 1 if index[c] == nitems - 1: c = c - 1 return False def exist(self, f): if not self.items or not inspect.getargspec(f)[0]: return False nargs = len(inspect.getargspec(f)[0]) if inspect.getargspec(f)[3]: nargs = nargs - len(inspect.getargspec(f)[3]) assert nargs > 0 nitems = len(self.items) index = [0] * nargs while True: args = [] for x in index: args.append(self.items[x]) if f(*args): return True c = len(index) - 1 index[c] = index[c] + 1 while index[c] == nitems: index[c] = 0 c = c - 1 if c < 0: return False else: index[c] = index[c] + 1 if index[c] == nitems - 1: c = c - 1 return False def swap(self, item1, item2): """ Swap two elements. Return true if swap was successful. """ try: i1 = self.items.index(item1) i2 = self.items.index(item2) self.items[i1], self.items[i2] = self.items[i2], self.items[i1] # send a notification that this list has changed factory = self.object.factory if factory: factory._handle(AssociationChangeEvent(self.object, self.property)) return True except IndexError as ex: return False except ValueError as ex: return False # vi:sw=4:et:ai PK!ň gaphor/UML/diagram.py# vim: sw=4 """This module contains a model element Diagram which is the abstract representation of a UML diagram. Diagrams can be visualized and edited. The DiagramCanvas class extends the gaphas.Canvas class. """ import uuid import gaphas from gaphor.UML.uml2 import Namespace, PackageableElement class DiagramCanvas(gaphas.Canvas): """DiagramCanvas extends the gaphas.Canvas class. Updates to the canvas can be blocked by setting the block_updates property to true. A save function can be applied to all root canvas items. Canvas items can be selected with an optional expression filter.""" def __init__(self, diagram): """Initialize the diagram canvas with the supplied diagram. By default, updates are not blocked.""" super(DiagramCanvas, self).__init__() self._diagram = diagram self._block_updates = False diagram = property(lambda s: s._diagram) def _set_block_updates(self, block): """Sets the block_updates property. If false, the diagram canvas is updated immediately.""" self._block_updates = block if not block: self.update_now() block_updates = property(lambda s: s._block_updates, _set_block_updates) def update_now(self): """Update the diagram canvas, unless block_updates is true.""" if self._block_updates: return super(DiagramCanvas, self).update_now() def save(self, save_func): """Apply the supplied save function to all root diagram items.""" for item in self.get_root_items(): save_func(None, item) def postload(self): """Called after the diagram canvas has loaded. Currently does nothing. """ pass def select(self, expression=lambda e: True): """Return a list of all canvas items that match expression.""" return list(filter(expression, self.get_all_items())) class Diagram(Namespace, PackageableElement): """Diagrams may contain model elements and can be owned by a Package. A diagram is a Namespace and a PackageableElement.""" def __init__(self, id=None, factory=None): """Initialize the diagram with an optional id and element factory. The diagram also has a canvas.""" super(Diagram, self).__init__(id, factory) self.canvas = DiagramCanvas(self) def save(self, save_func): """Apply the supplied save function to this diagram and the canvas.""" super(Diagram, self).save(save_func) save_func("canvas", self.canvas) def postload(self): """Handle post-load functionality for the diagram canvas.""" super(Diagram, self).postload() self.canvas.postload() def create(self, type, parent=None, subject=None): """Create a new canvas item on the canvas. It is created with a unique ID and it is attached to the diagram's root item. The type parameter is the element class to create. The new element also has an optional parent and subject.""" assert issubclass(type, gaphas.Item) obj = type(str(uuid.uuid1())) if subject: obj.subject = subject self.canvas.add(obj, parent) return obj def unlink(self): """Unlink all canvas items then unlink this diagram.""" for item in self.canvas.get_all_items(): try: item.unlink() except: pass super(Diagram, self).unlink() PK!0 gaphor/UML/element.py#!/usr/bin/env python """ Base class for UML model elements. """ __all__ = ["Element"] import threading import uuid from gaphor.UML.properties import umlproperty class Element(object): """ Base class for UML data classes. """ def __init__(self, id=None, factory=None): """ Create an element. As optional parameters an id and factory can be given. Id is a serial number for the element. The default id is None and will result in an automatic creation of an id. An existing id (such as an int or string) can be provided as well. An id False will result in no id being given (for "transient" or helper classes). Factory can be provided to refer to the class that maintains the lifecycle of the element. """ self._id = id or (id is not False and str(uuid.uuid1()) or False) # The factory this element belongs to. self._factory = factory self._unlink_lock = threading.Lock() id = property(lambda self: self._id, doc="Id") factory = property( lambda self: self._factory, doc="The factory that created this element" ) def umlproperties(self): """ Iterate over all UML properties """ umlprop = umlproperty class_ = type(self) for propname in dir(class_): if not propname.startswith("_"): prop = getattr(class_, propname) if isinstance(prop, umlprop): yield prop def save(self, save_func): """ Save the state by calling save_func(name, value). """ for prop in self.umlproperties(): prop.save(self, save_func) def load(self, name, value): """ Loads value in name. Make sure that for every load postload() should be called. """ try: prop = getattr(type(self), name) except AttributeError as e: raise AttributeError( "'%s' has no property '%s'" % (type(self).__name__, name) ) else: prop.load(self, value) def postload(self): """ Fix up the odds and ends. """ for prop in self.umlproperties(): prop.postload(self) def unlink(self): """Unlink the element. All the elements references are destroyed. The unlink lock is acquired while unlinking this elements properties to avoid recursion problems.""" if self._unlink_lock.locked(): return with self._unlink_lock: for prop in self.umlproperties(): prop.unlink(self) if self._factory: self._factory._unlink_element(self) # OCL methods: (from SMW by Ivan Porres (http://www.abo.fi/~iporres/smw)) def isKindOf(self, class_): """ Returns true if the object is an instance of `class_`. """ return isinstance(self, class_) def isTypeOf(self, other): """ Returns true if the object is of the same type as other. """ return isinstance(self, type(other)) def __getstate__(self): d = dict(self.__dict__) try: del d["_factory"] except KeyError: pass return d def __setstate__(self, state): self._factory = None self.__dict__.update(state) try: import psyco except ImportError: pass else: psyco.bind(Element) # vim:sw=4:et PK!D+gaphor/UML/elementfactory.py"""Factory for and registration of model elements.""" import uuid from zope import component from zope.interface import implementer from gaphor.UML.diagram import Diagram from gaphor.UML.element import Element from gaphor.UML.event import ( ElementCreateEvent, ElementDeleteEvent, FlushFactoryEvent, ModelFactoryEvent, ) from gaphor.UML.interfaces import IElementChangeEvent from gaphor.core import inject from gaphor.interfaces import IService, IEventFilter from gaphor.misc import odict class ElementFactory(object): """ The ElementFactory is used to create elements and do lookups to elements. Notifications are sent as arguments (name, element, `*user_data`). The following names are used: create - a new model element is created (element is newly created element) remove - a model element is removed (element is to be removed element) model - a new model has been loaded (element is None) flush - model is flushed: all element are removed from the factory (element is None) """ def __init__(self): self._elements = odict.odict() self._observers = list() def create(self, type): """ Create a new model element of type ``type``. """ obj = self.create_as(type, str(uuid.uuid1())) return obj def create_as(self, type, id): """ Create a new model element of type 'type' with 'id' as its ID. This method should only be used when loading models, since it does not emit an ElementCreateEvent event. """ assert issubclass(type, Element) obj = type(id, self) self._elements[id] = obj return obj def bind(self, element): """ Bind an already created element to the element factory. The element may not be bound to another factory already. """ if hasattr(element, "_factory") and element._factory: raise AttributeError("element is already bound") if self._elements.get(element.id): raise AttributeError("an element already exists with the same id") element._factory = self self._elements[element.id] = element def size(self): """ Return the amount of elements currently in the factory. """ return len(self._elements) def lookup(self, id): """ Find element with a specific id. """ return self._elements.get(id) __getitem__ = lookup def __contains__(self, element): return self.lookup(element.id) is element def select(self, expression=None): """ Iterate elements that comply with expression. """ if expression is None: for e in self._elements.values(): yield e else: for e in self._elements.values(): if expression(e): yield e def lselect(self, expression=None): """ Like select(), but returns a list. """ return list(self.select(expression)) def keys(self): """ Return a list with all id's in the factory. """ return list(self._elements.keys()) def iterkeys(self): """ Return a iterator with all id's in the factory. """ return iter(self._elements.keys()) def values(self): """ Return a list with all elements in the factory. """ return list(self._elements.values()) def itervalues(self): """ Return a iterator with all elements in the factory. """ return iter(self._elements.values()) def is_empty(self): """ Returns True if the factory holds no elements. """ return bool(self._elements) def flush(self): """Flush all elements (remove them from the factory). Diagram elements are flushed first. This is so that canvas updates are blocked. The remaining elements are then flushed. """ flush_element = self._flush_element for element in self.lselect(lambda e: isinstance(e, Diagram)): element.canvas.block_updates = True flush_element(element) for element in self.lselect(): flush_element(element) def _flush_element(self, element): element.unlink() def _unlink_element(self, element): """ NOTE: Invoked from Element.unlink() to perform an element unlink. """ try: del self._elements[element.id] except KeyError: pass def swap_element(self, element, new_class): assert element in list(self._elements.values()) if element.__class__ is not new_class: element.__class__ = new_class def _handle(self, event): """ Handle events coming from elements. """ # Invoke default handler, so properties get updated. component.handle(event) @implementer(IService) class ElementFactoryService(ElementFactory): """Service version of the ElementFactory.""" component_registry = inject("component_registry") def init(self, app): pass def shutdown(self): self.flush() def create(self, type): """ Create a new model element of type ``type``. """ obj = super(ElementFactoryService, self).create(type) self.component_registry.handle(ElementCreateEvent(self, obj)) return obj def flush(self): """Flush all elements (remove them from the factory). First test if the element factory has a Gaphor application instance. If yes, the application will handle a FlushFactoryEvent and will register a ElementChangedEventBlocker adapter. Diagram elements are flushed first. This is so that canvas updates are blocked. The remaining elements are then flushed. Finally, the ElementChangedEventBlocker adapter is unregistered if the factory has an application instance.""" self.component_registry.handle(FlushFactoryEvent(self)) self.component_registry.register_subscription_adapter( ElementChangedEventBlocker ) try: super(ElementFactoryService, self).flush() finally: self.component_registry.unregister_subscription_adapter( ElementChangedEventBlocker ) def notify_model(self): """ Send notification that a new model has been loaded by means of the ModelFactoryEvent event from gaphor.UML.event. """ self.component_registry.handle(ModelFactoryEvent(self)) def _unlink_element(self, element): """ NOTE: Invoked from Element.unlink() to perform an element unlink. """ self.component_registry.handle(ElementDeleteEvent(self, element)) super(ElementFactoryService, self)._unlink_element(element) def _handle(self, event): """ Handle events coming from elements (used internally). """ self.component_registry.handle(event) @implementer(IEventFilter) @component.adapter(IElementChangeEvent) class ElementChangedEventBlocker(object): """Blocks all events of type IElementChangeEvent. This filter is placed when the the element factory flushes it's content. """ def __init__(self, event): self._event = event def filter(self): """Returns something that evaluates to `True` so events are blocked.""" return "Blocked by ElementFactory.flush()" PK!SSgaphor/UML/event.py"""The core UML metamodel events.""" from zope.interface import implementer from gaphor.UML.interfaces import ( IAssociationAddEvent, IAssociationDeleteEvent, IElementCreateEvent, ) from gaphor.UML.interfaces import ( IAttributeChangeEvent, IAssociationChangeEvent, IAssociationSetEvent, ) from gaphor.UML.interfaces import ( IElementFactoryEvent, IModelFactoryEvent, IElementDeleteEvent, IFlushFactoryEvent, ) @implementer(IAttributeChangeEvent) class AttributeChangeEvent(object): """A UML attribute has changed value.""" def __init__(self, element, attribute, old_value, new_value): """Constructor. The element parameter is the element with the changing attribute. The attribute parameter is the parameter element that changed. The old_value is the old value of the attribute and the new_value is the new value of the attribute.""" self.element = element self.property = attribute self.old_value = old_value self.new_value = new_value @implementer(IAssociationChangeEvent) class AssociationChangeEvent(object): """An association UML element has changed.""" def __init__(self, element, association): """Constructor. The element parameter is the element the association is changing from. The association parameter is the changed association element.""" self.element = element self.property = association @implementer(IAssociationSetEvent) class AssociationSetEvent(AssociationChangeEvent): """An association element has been set.""" def __init__(self, element, association, old_value, new_value): """Constructor. The element parameter is the element setting the association element. The association parameter is the association element being set. The old_value parameter is the old association and the new_value parameter is the new association.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value self.new_value = new_value @implementer(IAssociationAddEvent) class AssociationAddEvent(AssociationChangeEvent): """An association element has been added.""" def __init__(self, element, association, new_value): """Constructor. The element parameter is the element the association has been added to. The association parameter is the association element being added.""" AssociationChangeEvent.__init__(self, element, association) self.new_value = new_value @implementer(IAssociationDeleteEvent) class AssociationDeleteEvent(AssociationChangeEvent): """An association element has been deleted.""" def __init__(self, element, association, old_value): """Constructor. The element parameter is the element the association has been deleted from. The association parameter is the deleted association element.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value class DerivedChangeEvent(AssociationChangeEvent): """A derived property has changed.""" pass @implementer(IAssociationSetEvent) class DerivedSetEvent(DerivedChangeEvent): """A generic derived set event.""" def __init__(self, element, association, old_value, new_value): """Constructor. The element parameter is the element to which the derived set belongs. The association parameter is the association of the derived set.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value self.new_value = new_value @implementer(IAssociationAddEvent) class DerivedAddEvent(DerivedChangeEvent): """A derived property has been added.""" def __init__(self, element, association, new_value): """Constructor. The element parameter is the element to which the derived property belongs. The association parameter is the association of the derived property.""" AssociationChangeEvent.__init__(self, element, association) self.new_value = new_value @implementer(IAssociationDeleteEvent) class DerivedDeleteEvent(DerivedChangeEvent): """A derived property has been deleted.""" def __init__(self, element, association, old_value): """Constructor. The element parameter is the element to which the derived property belongs. The association parameter is the association of the derived property.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value @implementer(IAssociationSetEvent) class RedefineSetEvent(AssociationChangeEvent): """A redefined property has been set.""" def __init__(self, element, association, old_value, new_value): """Constructor. The element parameter is the element to which the property belongs. The association parameter is association of the property.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value self.new_value = new_value @implementer(IAssociationAddEvent) class RedefineAddEvent(AssociationChangeEvent): """A redefined property has been added.""" def __init__(self, element, association, new_value): """Constructor. The element parameter is the element to which the property belongs. The association parameter is the association of the property.""" AssociationChangeEvent.__init__(self, element, association) self.new_value = new_value @implementer(IAssociationDeleteEvent) class RedefineDeleteEvent(AssociationChangeEvent): """A redefined property has been deleted.""" def __init__(self, element, association, old_value): """Constructor. The element parameter is the element to which the property belongs. The association parameter is the association of the property.""" AssociationChangeEvent.__init__(self, element, association) self.old_value = old_value @implementer(IElementCreateEvent) class DiagramItemCreateEvent(object): """A diagram item has been created.""" def __init__(self, element): """Constructor. The element parameter is the element being created.""" self.element = element @implementer(IElementCreateEvent, IElementFactoryEvent) class ElementCreateEvent(object): """An element has been created.""" def __init__(self, service, element): """Constructor. The service parameter is the service responsible for creating the element. The element parameter is the element being created.""" self.service = service self.element = element @implementer(IElementDeleteEvent, IElementFactoryEvent) class ElementDeleteEvent(object): """An element has been deleted.""" def __init__(self, service, element): """Constructor. The service parameter is the service responsible for deleting the element. The element parameter is the element being deleted.""" self.service = service self.element = element @implementer(IModelFactoryEvent) class ModelFactoryEvent(object): """A generic element factory event.""" def __init__(self, service): """Constructor. The service parameter is the service the emitted the event.""" self.service = service @implementer(IFlushFactoryEvent) class FlushFactoryEvent(object): """The element factory has been flushed.""" def __init__(self, service): """Constructor. The service parameter is the service responsible for flushing the factory.""" self.service = service PK!`  gaphor/UML/interfaces.py""" UML events emited on a change in the data model. """ from zope import interface from gaphor.interfaces import IServiceEvent class IElementEvent(interface.Interface): """Generic event fired when element state changes. """ element = interface.Attribute("The changed element") class IElementCreateEvent(IElementEvent): """A new element has been created. """ class IElementDeleteEvent(IElementEvent): """An element is deleted from the model. """ class IElementChangeEvent(IElementEvent): """ Generic event fired when element state changes. """ property = interface.Attribute("The property that changed") old_value = interface.Attribute("The property value before the change") new_value = interface.Attribute("The property value after the change") class IAttributeChangeEvent(IElementChangeEvent): """ An attribute has changed. """ class IAssociationChangeEvent(IElementChangeEvent): """ An association hs changed. This event may be fired for both ends of the association. """ class IAssociationSetEvent(IAssociationChangeEvent): """ An association with [0..1] multiplicity has been changed. """ class IAssociationAddEvent(IAssociationChangeEvent): """ An association with [0..*] multiplicity has been changed: a new entry is added. ``new_value`` contains the property being added. """ class IAssociationDeleteEvent(IAssociationChangeEvent): """ An association with [0..*] multiplicity has been changed: an entry has been removed. ``old_value`` contains the property that has been removed. """ class IElementFactoryEvent(IServiceEvent): """ Events related to individual model elements. """ class IModelFactoryEvent(IElementFactoryEvent): """ A new model is loaded into the ElementFactory. """ class IFlushFactoryEvent(IElementFactoryEvent): """ All elements are removed from the ElementFactory. This event is emitted before the factory is emptied. """ # vim: sw=4:et PK! ##gaphor/UML/modelfactory.py""" UML model support functions. Functions collected in this module allow to - create more complex UML model structures - perform specific searches and manipulations """ import itertools from gaphor.UML.uml2 import * # '<<%s>>' STEREOTYPE_FMT = "<<%s>>" def stereotypes_str(element, stereotypes=()): """ Identify stereotypes of an UML metamodel instance and return coma separated stereotypes as string. :Parameters: element Element having stereotypes, can be None. stereotypes List of additional stereotypes, can be empty. """ # generate string with stereotype names separated by coma if element: applied = (stereotype_name(st) for st in get_applied_stereotypes(element)) else: applied = () s = ", ".join(itertools.chain(stereotypes, applied)) if s: return STEREOTYPE_FMT % s else: return "" def stereotype_name(stereotype): """ Return stereotype name suggested by UML specification. First will be character lowercase unless the second character is uppercase. :Parameters: stereotype Stereotype UML metamodel instance. """ name = stereotype.name if not name: return "" elif len(name) > 1 and name[1].isupper(): return name else: return name[0].lower() + name[1:] def apply_stereotype(factory, element, stereotype): """ Apply a stereotype to an element. :Parameters: factory UML metamodel factory. element UML metamodel class instance. stereotype UML metamodel stereotype instance. """ obj = factory.create(InstanceSpecification) obj.classifier = stereotype element.appliedStereotype = obj return obj def find_instances(factory, element): """ Find instance specification which extend classifier `element`. """ return factory.select( lambda e: e.isKindOf(InstanceSpecification) and e.classifier and e.classifier[0] == element ) def remove_stereotype(element, stereotype): """ Remove a stereotype from an element. :Parameters: element UML metamodel element instance. stereotype UML metamodel stereotype instance. """ for obj in element.appliedStereotype: if obj.classifier and obj.classifier[0] is stereotype: del element.appliedStereotype[obj] obj.unlink() break def get_stereotypes(factory, element): """ Get sorted collection of possible stereotypes for specified element. """ # UML specs does not allow to extend stereotypes with stereotypes if isinstance(element, Stereotype): return () cls = type(element) # find out names of classes, which are superclasses of element class names = set(c.__name__ for c in cls.__mro__ if issubclass(c, Element)) # find stereotypes that extend element class classes = factory.select(lambda e: e.isKindOf(Class) and e.name in names) stereotypes = set(ext.ownedEnd.type for cls in classes for ext in cls.extension) return sorted(stereotypes, key=lambda st: st.name) def get_applied_stereotypes(element): """ Get collection of applied stereotypes to an element. """ return element.appliedStereotype[:].classifier def create_extension(factory, element, stereotype): """ Extend an element with a stereotype. """ ext = factory.create(Extension) p = factory.create(Property) ext_end = factory.create(ExtensionEnd) ext.memberEnd = p ext.memberEnd = ext_end ext.ownedEnd = ext_end ext_end.type = stereotype ext_end.aggregation = "composite" p.type = element p.name = "baseClass" stereotype.ownedAttribute = p return ext extend_with_stereotype = create_extension def add_slot(factory, instance, definingFeature): """ Add slot to instance specification for an attribute. """ slot = factory.create(Slot) slot.definingFeature = definingFeature instance.slot = slot return slot def create_dependency(factory, supplier, client): dep = factory.create(Dependency) dep.supplier = supplier dep.client = client return dep def create_realization(factory, realizingClassifier, abstraction): dep = factory.create(Realization) dep.realizingClassifier = realizingClassifier dep.abstraction = abstraction return dep def create_generalization(factory, general, specific): gen = factory.create(Generalization) gen.general = general gen.specific = specific return gen def create_implementation(factory, contract, implementatingClassifier): impl = factory.create(Implementation) impl.contract = contract impl.implementatingClassifier = implementatingClassifier return impl def create_association(factory, type_a, type_b): """ Create an association between two items. """ assoc = factory.create(Association) end_a = factory.create(Property) end_b = factory.create(Property) assoc.memberEnd = end_a assoc.memberEnd = end_b end_a.type = type_a end_b.type = type_b # set default navigability (unknown) set_navigability(assoc, end_a, None) set_navigability(assoc, end_b, None) return assoc def set_navigability(assoc, end, nav): """ Set navigability of an association end (property). There are free possible values for ``nav`` parameter True association end is navigable False association end is not navigable None association end navigability is unkown There are two ways of specifing than an end is navigable - an end is in Association.navigableOwnedEnd collection - an end is class (interface) attribute (stored in Class.ownedAttribute collection) Let's consider the graph:: A -----> B y x There two association ends A.x and B.y, A.x is navigable. Therefore navigable association ends are constructed in following way - if A is a class or an interface, then A.x is an attribute owned by A - if A is other classifier, then association is more general relationship; it may mean that participating instance of B can be "accessed efficiently" - i.e. when A is a Component, then association may be some compositing relationship - when A and B are instances of Node class, then it is a communication path Therefore navigable association end may be stored as one of - {Class,Interface}.ownedAttribute due to their capabilities of editing owned members - Association.navigableOwnedEnd When an end has unknown (unspecified) navigability, then it is owned by association (but not by classifier). When an end is non-navigable, then it is just member of an association. """ # remove "navigable" and "unspecified" navigation indicators first if type(end.type) in (Class, Interface): owner = end.opposite.type if end in owner.ownedAttribute: owner.ownedAttribute.remove(end) if end in assoc.ownedEnd: assoc.ownedEnd.remove(end) if end in assoc.navigableOwnedEnd: assoc.navigableOwnedEnd.remove(end) assert end not in assoc.ownedEnd assert end not in assoc.navigableOwnedEnd if nav is True: if type(end.type) in (Class, Interface): owner = end.opposite.type owner.ownedAttribute = end else: assoc.navigableOwnedEnd = end elif nav is None: assoc.ownedEnd = end # elif nav is False, non-navigable def dependency_type(client, supplier): """ Determine dependency type between client (tail) and supplier (arrowhead). There can be different dependencies detected automatically - usage when supplier is an interface - realization when client is component and supplier is a classifier If none of above is detected then standard dependency is determined. """ dt = Dependency # test interface first as it is a classifier if isinstance(supplier, Interface): dt = Usage elif isinstance(client, Component) and isinstance(supplier, Classifier): dt = Realization return dt def create_message(factory, msg, inverted=False): """ Create new message based on speciied message. If inverted is set to True, then inverted message is created. """ message = factory.create(Message) send = None receive = None if msg.sendEvent: send = factory.create(MessageOccurrenceSpecification) sl = msg.sendEvent.covered send.covered = sl if msg.receiveEvent: receive = factory.create(MessageOccurrenceSpecification) rl = msg.receiveEvent.covered receive.covered = rl if inverted: # inverted message goes in different direction, than original # message message.sendEvent = receive message.receiveEvent = send else: message.sendEvent = send message.receiveEvent = receive return message # vim:sw=4:et:ai PK!ĸllgaphor/UML/properties.py""" Properties used to create the UML 2.0 data model. The logic for creating and destroying connections between UML objects is implemented in Python property classes. These classes are simply instantiated like this: class Class(Element): pass class Comment(Element): pass Class.ownedComment = association('ownedComment', Comment, 0, '*', 'annotatedElement') Comment.annotatedElement = association('annotatedElement', Element, 0, '*', 'ownedComment') Same for attributes and enumerations. Each property type (association, attribute and enumeration) has three specific methods: _get(): return the value _set(value): set the value or add it to a list _del(value=None): delete the value. 'value' is used to tell which value is to be removed (in case of associations with multiplicity > 1). load(value): load 'value' as the current value for this property save(save_func): send the value of the property to save_func(name, value) """ __all__ = ["attribute", "enumeration", "association", "derivedunion", "redefine"] import logging from zope import component from gaphor.UML.collection import collection, collectionlist from gaphor.UML.event import AssociationAddEvent, AssociationDeleteEvent from gaphor.UML.event import AttributeChangeEvent, AssociationSetEvent from gaphor.UML.event import DerivedAddEvent, DerivedDeleteEvent from gaphor.UML.event import DerivedChangeEvent, DerivedSetEvent from gaphor.UML.event import RedefineSetEvent, RedefineAddEvent, RedefineDeleteEvent from gaphor.UML.interfaces import IAssociationDeleteEvent from gaphor.UML.interfaces import IAssociationSetEvent, IAssociationAddEvent from gaphor.UML.interfaces import IElementChangeEvent, IAssociationChangeEvent log = logging.getLogger(__name__) class umlproperty(object): """ Superclass for attribute, enumeration and association. The subclasses should define a ``name`` attribute that contains the name of the property. Derived properties (derivedunion and redefine) can be connected, they will be notified when the value changes. In some cases properties call out and delegate actions to the ElementFactory, for example in the case of event handling. """ def __get__(self, obj, class_=None): if obj: return self._get(obj) return self def __set__(self, obj, value): self._set(obj, value) def __delete__(self, obj, value=None): self._del(obj, value) def save(self, obj, save_func): if hasattr(obj, self._name): save_func(self.name, self._get(obj)) def load(self, obj, value): self._set(obj, value) def postload(self, obj): pass def unlink(self, obj): """ This is called from the Element to denote the element is unlinking. """ pass def handle(self, event): factory = event.element.factory if factory: factory._handle(event) else: component.handle(event) class attribute(umlproperty): """ Attribute. Element.attr = attribute('attr', types.StringType, '') """ # TODO: check if lower and upper are actually needed for attributes def __init__(self, name, type, default=None, lower=0, upper=1): self.name = name self._name = "_" + name self.type = type self.default = default self.lower = lower self.upper = upper def load(self, obj, value): """Load the attribute value.""" try: setattr(obj, self._name, self.type(value)) except ValueError: error_msg = "Failed to load attribute %s of type %s with value %s" % ( self._name, self.type, value, ) raise TypeError(error_msg) def __str__(self): if self.lower == self.upper: return "" % ( self.name, self.type, self.lower, self.default, ) else: return "" % ( self.name, self.type, self.lower, self.upper, self.default, ) def _get(self, obj): try: return getattr(obj, self._name) except AttributeError: return self.default def _set(self, obj, value): if value is not None: if not isinstance(value, self.type) and not isinstance(value, str): raise AttributeError( "Value should be of type %s" % hasattr(self.type, "__name__") and self.type.__name__ or self.type ) if value == self._get(obj): return # undoattributeaction(self, obj, self._get(obj)) old = self._get(obj) if value == self.default and hasattr(obj, self._name): delattr(obj, self._name) else: setattr(obj, self._name, value) self.handle(AttributeChangeEvent(obj, self, old, value)) def _del(self, obj, value=None): old = self._get(obj) try: # undoattributeaction(self, obj, self._get(obj)) delattr(obj, self._name) except AttributeError: pass else: self.handle(AttributeChangeEvent(obj, self, old, self.default)) class enumeration(umlproperty): """ Enumeration Element.enum = enumeration('enum', ('one', 'two', 'three'), 'one') An enumeration is a special kind of attribute that can only hold a predefined set of values. Multiplicity is always `[0..1]` """ # All enumerations have a type 'str' type = property(lambda s: str) def __init__(self, name, values, default): self.name = name self._name = "_" + name self.values = values self.default = default self.lower = 0 self.upper = 1 def __str__(self): return "" % (self.name, self.values, self.default) def _get(self, obj): try: return getattr(obj, self._name) except AttributeError: return self.default def load(self, obj, value): if not value in self.values: raise AttributeError("Value should be one of %s" % str(self.values)) setattr(obj, self._name, value) def _set(self, obj, value): if not value in self.values: raise AttributeError("Value should be one of %s" % str(self.values)) old = self._get(obj) if value == old: return if value == self.default: delattr(obj, self._name) else: setattr(obj, self._name, value) self.handle(AttributeChangeEvent(obj, self, old, value)) def _del(self, obj, value=None): old = self._get(obj) try: delattr(obj, self._name) except AttributeError: pass else: self.handle(AttributeChangeEvent(obj, self, old, self.default)) class association(umlproperty): """ Association, both uni- and bi-directional. Element.assoc = association('assoc', Element, opposite='other') A listerer is connected to the value added to the association. This will cause the association to be ended if the element on the other end of the association is unlinked. If the association is a composite relationship, the association will unlink all elements attached to if it is unlinked. """ def __init__(self, name, type, lower=0, upper="*", composite=False, opposite=None): self.name = name self._name = "_" + name self.type = type self.lower = lower self.upper = upper self.composite = composite self.opposite = opposite and opposite self.stub = None def load(self, obj, value): if not isinstance(value, self.type): raise AttributeError( "Value for %s should be of type %s (%s)" % (self.name, self.type.__name__, type(value).__name__) ) self._set(obj, value, do_notify=False) def postload(self, obj): """ In the postload step, ensure that bi-directional associations are bi-directional. """ values = self._get(obj) if not values: return if self.upper == 1: values = [values] for value in values: if not isinstance(value, self.type): raise AttributeError( "Error in postload validation for %s: Value %s should be of type %s" % (self.name, value, self.type.__name__) ) def __str__(self): if self.lower == self.upper: s = "" or "", self.opposite) return s + ">" def _get(self, obj): # TODO: Handle lower and add items if lower > 0 try: return getattr(obj, self._name) except AttributeError: if self.upper == 1: return None else: # Create the empty collection here since it might be used to # add c = collection(self, obj, self.type) setattr(obj, self._name, c) return c def _set(self, obj, value, from_opposite=False, do_notify=True): """ Set a new value for our attribute. If this is a collection, append to the existing collection. This method is called from the opposite association property. """ if not (isinstance(value, self.type) or (value is None and self.upper == 1)): raise AttributeError("Value should be of type %s" % self.type.__name__) # Remove old value only for uni-directional associations if self.upper == 1: old = self._get(obj) # do nothing if we are assigned our current value: # Still do your thing, since undo handlers expect that. if value is old: return if old: self._del(obj, old, from_opposite=from_opposite, do_notify=False) if do_notify: event = AssociationSetEvent(obj, self, old, value) if value is None: if do_notify: self.handle(event) return setattr(obj, self._name, value) else: # Set the actual value c = self._get(obj) if not c: c = collection(self, obj, self.type) setattr(obj, self._name, c) elif value in c: return c.items.append(value) if do_notify: event = AssociationAddEvent(obj, self, value) if not from_opposite and self.opposite: opposite = getattr(type(value), self.opposite) if not opposite.opposite: opposite.stub = self opposite._set(value, obj, from_opposite=True, do_notify=do_notify) elif not self.opposite: if not self.stub: self.stub = associationstub(self) setattr(self.type, "UML_associationstub_%x" % id(self), self.stub) self.stub._set(value, obj) if do_notify: self.handle(event) def _del(self, obj, value, from_opposite=False, do_notify=True): """ Delete is used for element deletion and for removal of elements from a list. """ if not value: if self.upper != "*" and self.upper > 1: raise Exception("Can not delete collections") old = value = self._get(obj) if value is None: return if not from_opposite and self.opposite: getattr(type(value), self.opposite)._del(value, obj, from_opposite=True) elif not self.opposite: if self.stub: self.stub._del(value, obj, from_opposite=True) event = None if self.upper == 1: try: delattr(obj, self._name) except: pass else: if do_notify: event = AssociationSetEvent(obj, self, value, None) else: c = self._get(obj) if c: items = c.items try: items.remove(value) except: pass else: if do_notify: event = AssociationDeleteEvent(obj, self, value) # Remove items collection if empty if not items: delattr(obj, self._name) if do_notify and event: self.handle(event) def unlink(self, obj): values = self._get(obj) composite = self.composite if values: if self.upper == 1: values = [values] for value in list(values): # TODO: make normal unlinking work through this method. self.__delete__(obj, value) if composite: value.unlink() class AssociationStubError(Exception): pass class associationstub(umlproperty): """ An association stub is an internal thingy that ensures all associations are always bi-directional. This helps the application when one end of the association is unlinked. On unlink() of an element all `umlproperty`'s are iterated and called by their unlink() method. """ def __init__(self, association): self.association = association self._name = "_stub_%x" % id(self) def __get__(self, obj, class_=None): if obj: raise AssociationStubError("getting values not allowed") return self def __set__(self, obj, value): raise AssociationStubError("setting values not allowed") def __delete__(self, obj, value=None): raise AssociationStubError("deleting values not allowed") def save(self, obj, save_func): pass def load(self, obj, value): pass def unlink(self, obj): try: values = getattr(obj, self._name) except AttributeError: pass else: for value in set(values): self.association.__delete__(value, obj) def _set(self, obj, value): try: getattr(obj, self._name).add(value) except AttributeError: setattr(obj, self._name, set([value])) def _del(self, obj, value, from_opposite=False): try: c = getattr(obj, self._name) except AttributeError: pass else: c.discard(value) class unioncache(object): """ Small cache helper object for derivedunions. """ def __init__(self, data, version): self.data = data self.version = version class derived(umlproperty): """ Base class for derived properties, both derived unions and custom properties. Note that, although this derived property sends DerivedAddEvent, -Delete- and Set events, this gives just an assumption that something may have changed. If something actually changed depends on the filter applied to the derived property. """ def __init__(self, name, type, lower, upper, *subsets): self.name = name self._name = "_" + name self.version = 1 self.type = type self.lower = lower self.upper = upper self.subsets = set(subsets) self.single = len(subsets) == 1 component.provideHandler(self._association_changed) def load(self, obj, value): raise ValueError( "Derivedunion: Properties should not be loaded in a derived union %s: %s" % (self.name, value) ) def postload(self, obj): self.version += 1 def save(self, obj, save_func): pass def __str__(self): return "" % (self.name, str(list(map(str, self.subsets)))[1:-1]) def filter(self, obj): """ Filter should return something iterable. """ raise NotImplementedError("Implement this in the property.") def _update(self, obj): """ Update the list of items. Returns a unioncache instance. """ u = self.filter(obj) if self.upper != "*" and self.upper <= 1: assert len(u) <= 1, ( "Derived union %s of item %s should have length 1 %s" % (self.name, obj.id, tuple(u)) ) # maybe code below is better instead the assertion above? # if len(u) > 1: # log.warning('Derived union %s of item %s should have length 1 %s' % (self.name, obj.id, tuple(u))) if u: u = next(iter(u)) else: u = None uc = unioncache(u, self.version) setattr(obj, self._name, uc) return uc def _get(self, obj): try: uc = getattr(obj, self._name) if uc.version != self.version: uc = self._update(obj) except AttributeError: uc = self._update(obj) return uc.data def _set(self, obj, value): raise AttributeError("Can not set values on a union") def _del(self, obj, value=None): raise AttributeError("Can not delete values on a union") @component.adapter(IElementChangeEvent) def _association_changed(self, event): """ Re-emit state change for the derived properties as Derived*Event's. TODO: We should fetch the old and new state of the namespace item in stead of the old and new values of the item that changed. If multiplicity is [0..1]: send DerivedSetEvent if len(union) < 2 if multiplicity is [*]: send DerivedAddEvent and DerivedDeleteEvent if value not in derived union and """ if event.property in self.subsets: # Make sure unions are created again self.version += 1 if not IAssociationChangeEvent.providedBy(event): return # mimic the events for Set/Add/Delete if self.upper == 1: # This is a [0..1] event # TODO: This is an error: [0..*] associations may be used for updating [0..1] associations assert IAssociationSetEvent.providedBy(event) old_value, new_value = event.old_value, event.new_value self.handle(DerivedSetEvent(event.element, self, old_value, new_value)) else: if IAssociationSetEvent.providedBy(event): old_value, new_value = event.old_value, event.new_value # Do a filter? Change to self.handle(DerivedDeleteEvent(event.element, self, old_value)) self.handle(DerivedAddEvent(event.element, self, new_value)) elif IAssociationAddEvent.providedBy(event): new_value = event.new_value self.handle(DerivedAddEvent(event.element, self, new_value)) elif IAssociationDeleteEvent.providedBy(event): old_value = event.old_value self.handle(DerivedDeleteEvent(event.element, self, old_value)) elif IAssociationChangeEvent.providedBy(event): self.handle(DerivedChangeEvent(event.element, self)) else: log.error( "Don" "t know how to handle event " + str(event) + " for derived union" ) class derivedunion(derived): """ Derived union Element.union = derivedunion('union', subset1, subset2..subsetn) The subsets are the properties that participate in the union (Element.name). """ def _union(self, obj, exclude=None): """ Returns a union of all values as a set. """ if self.single: return next(iter(self.subsets)).__get__(obj) else: u = set() for s in self.subsets: if s is exclude: continue tmp = s.__get__(obj) if tmp: try: u.update(tmp) except TypeError: # [0..1] property u.add(tmp) return collectionlist(u) # Filter is our default filter filter = _union @component.adapter(IElementChangeEvent) def _association_changed(self, event): """ Re-emit state change for the derived union (as Derived*Event's). TODO: We should fetch the old and new state of the namespace item in stead of the old and new values of the item that changed. If multiplicity is [0..1]: send DerivedSetEvent if len(union) < 2 if multiplicity is [*]: send DerivedAddEvent and DerivedDeleteEvent if value not in derived union and """ if event.property in self.subsets: # Make sure unions are created again self.version += 1 if not IAssociationChangeEvent.providedBy(event): return values = self._union(event.element, exclude=event.property) if self.upper == 1: assert IAssociationSetEvent.providedBy(event) old_value, new_value = event.old_value, event.new_value # This is a [0..1] event if self.single: # Only one subset element, so pass the values on self.handle( DerivedSetEvent(event.element, self, old_value, new_value) ) else: new_values = set(values) if new_value: new_values.add(new_value) if len(new_values) > 1: # In an in-between state. Do not emit notifications return if values: new_value = next(iter(values)) self.handle( DerivedSetEvent(event.element, self, old_value, new_value) ) else: if IAssociationSetEvent.providedBy(event): old_value, new_value = event.old_value, event.new_value if old_value and old_value not in values: self.handle(DerivedDeleteEvent(event.element, self, old_value)) if new_value and new_value not in values: self.handle(DerivedAddEvent(event.element, self, new_value)) elif IAssociationAddEvent.providedBy(event): new_value = event.new_value if new_value not in values: self.handle(DerivedAddEvent(event.element, self, new_value)) elif IAssociationDeleteEvent.providedBy(event): old_value = event.old_value if old_value not in values: self.handle(DerivedDeleteEvent(event.element, self, old_value)) elif IAssociationChangeEvent.providedBy(event): self.handle(DerivedChangeEvent(event.element, self)) else: log.error( "Don" "t know how to handle event " + str(event) + " for derived union" ) class redefine(umlproperty): """ Redefined association Element.redefine = redefine(Element, 'redefine', Class, Element.assoc) If the redefine eclipses the original property (it has the same name) it ensures that the original values are saved and restored. """ def __init__(self, decl_class, name, type, original): self.decl_class = decl_class self.name = name self._name = "_" + name self.type = type self.original = original component.provideHandler(self._association_changed) upper = property(lambda s: s.original.upper) lower = property(lambda s: s.original.lower) opposite = property(lambda s: s.original.opposite) def load(self, obj, value): if self.original.name == self.name: self.original.load(obj, value) def postload(self, obj): if self.original.name == self.name: self.original.postload(obj) def save(self, obj, save_func): if self.original.name == self.name: self.original.save(obj, save_func) def unlink(self, obj): if self.original.name == self.name: self.original.unlink(obj) def __str__(self): return "" % ( self.name, self.type.__name__, str(self.original), ) def __get__(self, obj, class_=None): # No longer needed if not obj: return self return self.original.__get__(obj, class_) def __set__(self, obj, value): # No longer needed if not isinstance(value, self.type): raise AttributeError("Value should be of type %s" % self.type.__name__) self.original.__set__(obj, value) def __delete__(self, obj, value=None): # No longer needed self.original.__delete__(obj, value) def _get(self, obj): return self.original._get(obj) def _set(self, obj, value, from_opposite=False): return self.original._set(obj, value, from_opposite) def _del(self, obj, value, from_opposite=False): return self.original._del(obj, value, from_opposite) @component.adapter(IAssociationChangeEvent) def _association_changed(self, event): if event.property is self.original and isinstance( event.element, self.decl_class ): # mimic the events for Set/Add/Delete if IAssociationSetEvent.providedBy(event): self.handle( RedefineSetEvent( event.element, self, event.old_value, event.new_value ) ) elif IAssociationAddEvent.providedBy(event): self.handle(RedefineAddEvent(event.element, self, event.new_value)) elif IAssociationDeleteEvent.providedBy(event): self.handle(RedefineDeleteEvent(event.element, self, event.old_value)) else: log.error( "Don" "t know how to handle event " + str(event) + " for redefined association" ) try: import psyco except ImportError: pass else: psyco.bind(umlproperty) psyco.bind(attribute) psyco.bind(enumeration) psyco.bind(association) psyco.bind(derivedunion) psyco.bind(redefine) PK!gaphor/UML/tests/__init__.pyPK!XXX#gaphor/UML/tests/test_collection.py""" Test if the collection's list supports all trickery. """ import unittest from gaphor.UML.collection import collectionlist class CollectionlistTestCase(unittest.TestCase): def test_listing(self): c = collectionlist() c.append("a") c.append("b") c.append("c") assert str(c) == "['a', 'b', 'c']" PK!. $ 'gaphor/UML/tests/test_elementfactory.pyimport unittest from gaphor.UML import * from gaphor.UML.interfaces import * import gc import weakref, sys class ElementFactoryTestCase(unittest.TestCase): def setUp(self): self.factory = ElementFactory() def tearDown(self): del self.factory def testCreate(self): ef = self.factory p = ef.create(Parameter) assert len(list(ef.values())) == 1 def testFlush(self): ef = self.factory p = ef.create(Parameter) # wp = weakref.ref(p) assert len(list(ef.values())) == 1 ef.flush() del p gc.collect() # assert wp() is None assert len(list(ef.values())) == 0, list(ef.values()) def testWithoutApplication(self): ef = ElementFactory() p = ef.create(Parameter) assert ef.size() == 1, ef.size() ef.flush() assert ef.size() == 0, ef.size() p = ef.create(Parameter) assert ef.size() == 1, ef.size() p.unlink() assert ef.size() == 0, ef.size() def testUnlink(self): ef = self.factory p = ef.create(Parameter) assert len(list(ef.values())) == 1 p.unlink() assert len(list(ef.values())) == 0, list(ef.values()) p = ef.create(Parameter) p.defaultValue = "l" assert len(list(ef.values())) == 1 p.unlink() del p assert len(list(ef.values())) == 0, list(ef.values()) from zope import component from gaphor.application import Application # Event handlers are registered as persisting top level handlers, since no # unsubscribe functionality is provided. handled = False events = [] last_event = None @component.adapter(IServiceEvent) def handler(event): global handled, events, last_event handled = True events.append(event) last_event = event component.provideHandler(handler) class ElementFactoryServiceTestCase(unittest.TestCase): def setUp(self): Application.init(["element_factory"]) self.factory = Application.get_service("element_factory") def tearDown(self): del self.factory self.clearEvents() Application.shutdown() def clearEvents(self): global handled, events, last_event handled = False events = [] last_event = None def testCreateEvent(self): ef = self.factory global handled p = ef.create(Parameter) self.assertTrue(IElementCreateEvent.providedBy(last_event)) self.assertTrue(handled) def testRemoveEvent(self): ef = self.factory global handled p = ef.create(Parameter) self.assertTrue(IElementCreateEvent.providedBy(last_event)) self.assertTrue(handled) self.clearEvents() p.unlink() self.assertTrue(IElementDeleteEvent.providedBy(last_event)) def testModelEvent(self): ef = self.factory global handled ef.notify_model() self.assertTrue(IModelFactoryEvent.providedBy(last_event)) def testFlushEvent(self): ef = self.factory global handled ef.flush() self.assertTrue(IFlushFactoryEvent.providedBy(last_event)) PK!G!r-r-%gaphor/UML/tests/test_modelfactory.pyfrom gaphor import UML from gaphor.application import Application from gaphor.UML.modelfactory import STEREOTYPE_FMT as fmt import unittest class TestCaseBase(unittest.TestCase): def setUp(self): self.factory = UML.ElementFactory() class StereotypesTestCase(TestCaseBase): def test_stereotype_name(self): """Test stereotype name """ stereotype = self.factory.create(UML.Stereotype) stereotype.name = "Test" self.assertEqual("test", UML.model.stereotype_name(stereotype)) stereotype.name = "TEST" self.assertEqual("TEST", UML.model.stereotype_name(stereotype)) stereotype.name = "T" self.assertEqual("t", UML.model.stereotype_name(stereotype)) stereotype.name = "" self.assertEqual("", UML.model.stereotype_name(stereotype)) stereotype.name = None self.assertEqual("", UML.model.stereotype_name(stereotype)) def test_stereotypes_conversion(self): """Test stereotypes conversion """ s1 = self.factory.create(UML.Stereotype) s2 = self.factory.create(UML.Stereotype) s3 = self.factory.create(UML.Stereotype) s1.name = "s1" s2.name = "s2" s3.name = "s3" cls = self.factory.create(UML.Class) UML.model.apply_stereotype(self.factory, cls, s1) UML.model.apply_stereotype(self.factory, cls, s2) UML.model.apply_stereotype(self.factory, cls, s3) self.assertEqual(fmt % "s1, s2, s3", UML.model.stereotypes_str(cls)) def test_no_stereotypes(self): """Test stereotypes conversion without applied stereotypes """ cls = self.factory.create(UML.Class) self.assertEqual("", UML.model.stereotypes_str(cls)) def test_additional_stereotypes(self): """Test additional stereotypes conversion """ s1 = self.factory.create(UML.Stereotype) s2 = self.factory.create(UML.Stereotype) s3 = self.factory.create(UML.Stereotype) s1.name = "s1" s2.name = "s2" s3.name = "s3" cls = self.factory.create(UML.Class) UML.model.apply_stereotype(self.factory, cls, s1) UML.model.apply_stereotype(self.factory, cls, s2) UML.model.apply_stereotype(self.factory, cls, s3) result = UML.model.stereotypes_str(cls, ("test",)) self.assertEqual(fmt % "test, s1, s2, s3", result) def test_just_additional_stereotypes(self): """Test additional stereotypes conversion without applied stereotypes """ cls = self.factory.create(UML.Class) result = UML.model.stereotypes_str(cls, ("test",)) self.assertEqual(fmt % "test", result) def test_getting_stereotypes(self): """Test getting possible stereotypes """ cls = self.factory.create(UML.Class) cls.name = "Class" st1 = self.factory.create(UML.Stereotype) st1.name = "st1" st2 = self.factory.create(UML.Stereotype) st2.name = "st2" # first extend with st2, to check sorting UML.model.extend_with_stereotype(self.factory, cls, st2) UML.model.extend_with_stereotype(self.factory, cls, st1) c1 = self.factory.create(UML.Class) result = tuple(st.name for st in UML.model.get_stereotypes(self.factory, c1)) self.assertEqual(("st1", "st2"), result) def test_getting_stereotypes_unique(self): """Test if possible stereotypes are unique """ cls1 = self.factory.create(UML.Class) cls1.name = "Class" cls2 = self.factory.create(UML.Class) cls2.name = "Component" st1 = self.factory.create(UML.Stereotype) st1.name = "st1" st2 = self.factory.create(UML.Stereotype) st2.name = "st2" # first extend with st2, to check sorting UML.model.extend_with_stereotype(self.factory, cls1, st2) UML.model.extend_with_stereotype(self.factory, cls1, st1) UML.model.extend_with_stereotype(self.factory, cls2, st1) UML.model.extend_with_stereotype(self.factory, cls2, st2) c1 = self.factory.create(UML.Component) result = tuple(st.name for st in UML.model.get_stereotypes(self.factory, c1)) self.assertEqual(("st1", "st2"), result) def test_finding_stereotype_instances(self): """Test finding stereotype instances """ s1 = self.factory.create(UML.Stereotype) s2 = self.factory.create(UML.Stereotype) s1.name = "s1" s2.name = "s2" c1 = self.factory.create(UML.Class) c2 = self.factory.create(UML.Class) UML.model.apply_stereotype(self.factory, c1, s1) UML.model.apply_stereotype(self.factory, c1, s2) UML.model.apply_stereotype(self.factory, c2, s1) result = [ e.classifier[0].name for e in UML.model.find_instances(self.factory, s1) ] self.assertEqual(2, len(result)) self.assertTrue("s1" in result, result) self.assertFalse("s2" in result, result) class AssociationTestCase(TestCaseBase): """ Association tests. """ def test_creation(self): """Test association creation """ c1 = self.factory.create(UML.Class) c2 = self.factory.create(UML.Class) assoc = UML.model.create_association(self.factory, c1, c2) types = [p.type for p in assoc.memberEnd] self.assertTrue(c1 in types, assoc.memberEnd) self.assertTrue(c2 in types, assoc.memberEnd) c1 = self.factory.create(UML.Interface) c2 = self.factory.create(UML.Interface) assoc = UML.model.create_association(self.factory, c1, c2) types = [p.type for p in assoc.memberEnd] self.assertTrue(c1 in types, assoc.memberEnd) self.assertTrue(c2 in types, assoc.memberEnd) class AssociationEndNavigabilityTestCase(TestCaseBase): """ Association navigability changes tests. """ def test_attribute_navigability(self): """Test navigable attribute of a class or an interface """ c1 = self.factory.create(UML.Class) c2 = self.factory.create(UML.Class) assoc = UML.model.create_association(self.factory, c1, c2) end = assoc.memberEnd[0] assert end.type is c1 assert end.type is c1 UML.model.set_navigability(assoc, end, True) # class/interface navigability, Association.navigableOwnedEnd not # involved self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end in c2.ownedAttribute) self.assertTrue(end.navigability is True) # unknown navigability UML.model.set_navigability(assoc, end, None) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end in assoc.ownedEnd) self.assertTrue(end not in c2.ownedAttribute) self.assertTrue(end.owner is assoc) self.assertTrue(end.navigability is None) # non-navigability UML.model.set_navigability(assoc, end, False) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end not in c2.ownedAttribute) self.assertTrue(end.owner is None) self.assertTrue(end.navigability is False) # check other navigability change possibilities UML.model.set_navigability(assoc, end, None) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end in assoc.ownedEnd) self.assertTrue(end not in c2.ownedAttribute) self.assertTrue(end.owner is assoc) self.assertTrue(end.navigability is None) UML.model.set_navigability(assoc, end, True) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end in c2.ownedAttribute) self.assertTrue(end.owner is c2) self.assertTrue(end.navigability is True) def test_relationship_navigability(self): """Test navigable relationship of a classifier """ n1 = self.factory.create(UML.Node) n2 = self.factory.create(UML.Node) assoc = UML.model.create_association(self.factory, n1, n2) end = assoc.memberEnd[0] assert end.type is n1 UML.model.set_navigability(assoc, end, True) # class/interface navigability, Association.navigableOwnedEnd not # involved self.assertTrue(end in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end.navigability is True) # unknown navigability UML.model.set_navigability(assoc, end, None) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end in assoc.ownedEnd) self.assertTrue(end.navigability is None) # non-navigability UML.model.set_navigability(assoc, end, False) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end.navigability is False) # check other navigability change possibilities UML.model.set_navigability(assoc, end, None) self.assertTrue(end not in assoc.navigableOwnedEnd) self.assertTrue(end in assoc.ownedEnd) self.assertTrue(end.navigability is None) UML.model.set_navigability(assoc, end, True) self.assertTrue(end in assoc.navigableOwnedEnd) self.assertTrue(end not in assoc.ownedEnd) self.assertTrue(end.navigability is True) class DependencyTypeTestCase(TestCaseBase): """ Tests for automatic dependency discovery """ def test_usage(self): """Test automatic dependency: usage """ cls = self.factory.create(UML.Class) iface = self.factory.create(UML.Interface) dt = UML.model.dependency_type(cls, iface) self.assertEqual(UML.Usage, dt) def test_usage_by_component(self): """Test automatic dependency: usage (by component) """ c = self.factory.create(UML.Component) iface = self.factory.create(UML.Interface) dt = UML.model.dependency_type(c, iface) # it should be usage not realization (interface is classifier as # well) self.assertEqual(UML.Usage, dt) def test_realization(self): """Test automatic dependency: realization """ c = self.factory.create(UML.Component) cls = self.factory.create(UML.Class) dt = UML.model.dependency_type(c, cls) self.assertEqual(UML.Realization, dt) class MessageTestCase(TestCaseBase): """ Tests for interaction messages. """ def test_create(self): """Test message creation """ m = self.factory.create(UML.Message) send = self.factory.create(UML.MessageOccurrenceSpecification) receive = self.factory.create(UML.MessageOccurrenceSpecification) sl = self.factory.create(UML.Lifeline) rl = self.factory.create(UML.Lifeline) send.covered = sl receive.covered = rl m.sendEvent = send m.receiveEvent = receive m1 = UML.model.create_message(self.factory, m, False) m2 = UML.model.create_message(self.factory, m, True) self.assertTrue(m1.sendEvent.covered is sl) self.assertTrue(m1.receiveEvent.covered is rl) self.assertTrue(m2.sendEvent.covered is rl) self.assertTrue(m2.receiveEvent.covered is sl) # vim:sw=4:et PK! SOSO#gaphor/UML/tests/test_properties.py#!/usr/bin/env python import unittest from zope import component from gaphor.application import Application from gaphor.UML.properties import * from gaphor.UML.element import Element from gaphor.UML.interfaces import IAssociationChangeEvent class PropertiesTestCase(unittest.TestCase): def test_association_1_x(self): # # 1:- # class A(Element): pass class B(Element): pass A.one = association("one", B, 0, 1, opposite="two") B.two = association("two", A, 0, 1) a = A() b = B() a.one = b assert a.one is b assert b.two is a a.one = None assert a.one is None assert b.two is None a.one = b assert a.one is b assert b.two is a del a.one assert a.one is None assert b.two is None def test_association_n_x(self): # # n:- # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, "*", opposite="two") B.two = association("two", A, 0, 1) a = A() b = B() a.one = b assert b in a.one assert b.two is a def test_association_1_1(self): # # 1:1 # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, 1, opposite="two") B.two = association("two", A, 0, 1, opposite="one") a = A() b = B() a.one = b a.one = b assert a.one is b assert b.two is a # assert len(a._observers.get('__unlink__')) == 0 # assert len(b._observers.get('__unlink__')) == 0 a.one = B() assert a.one is not b assert b.two is None # assert len(a._observers.get('__unlink__')) == 0 # assert len(b._observers.get('__unlink__')) == 0 c = C() try: a.one = c except Exception as e: pass # ok else: assert a.one is not c del a.one assert a.one is None assert b.two is None # assert len(a._observers.get('__unlink__')) == 0 # assert len(b._observers.get('__unlink__')) == 0 def test_association_1_n(self): # # 1:n # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, lower=0, upper=1, opposite="two") B.two = association("two", A, lower=0, upper="*", opposite="one") a1 = A() a2 = A() b1 = B() b2 = B() b1.two = a1 assert len(b1.two) == 1, "len(b1.two) == %d" % len(b1.two) assert a1 in b1.two assert a1.one is b1, "%s/%s" % (a1.one, b1) b1.two = a1 b1.two = a1 assert len(b1.two) == 1, "len(b1.two) == %d" % len(b1.two) assert a1 in b1.two assert a1.one is b1, "%s/%s" % (a1.one, b1) # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 b1.two = a2 assert a1 in b1.two assert a2 in b1.two assert a1.one is b1 assert a2.one is b1 try: del b1.two except Exception: pass # ok else: assert b1.two != [] assert a1 in b1.two assert a2 in b1.two assert a1.one is b1 assert a2.one is b1 b1.two.remove(a1) assert len(b1.two) == 1 assert a1 not in b1.two assert a2 in b1.two assert a1.one is None assert a2.one is b1 # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 a2.one = b2 assert len(b1.two) == 0 assert len(b2.two) == 1 assert a2 in b2.two assert a1.one is None assert a2.one is b2 try: del b1.two[a1] except ValueError: pass # ok else: assert 0, "should not be removed" def test_association_n_n(self): # # n:n # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, "*", opposite="two") B.two = association("two", A, 0, "*", opposite="one") a1 = A() a2 = A() b1 = B() b2 = B() a1.one = b1 assert b1 in a1.one assert a1 in b1.two assert not a2.one assert not b2.two a1.one = b2 assert b1 in a1.one assert b2 in a1.one assert a1 in b1.two assert a1 in b2.two assert not a2.one # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 a2.one = b1 assert len(a1.one) == 2 assert len(a2.one) == 1 assert len(b1.two) == 2 assert len(b2.two) == 1 assert b1 in a1.one assert b2 in a1.one assert a1 in b1.two assert a1 in b2.two assert b1 in a2.one assert a2 in b1.two del a1.one[b1] assert len(a1.one) == 1 assert len(a2.one) == 1 assert len(b1.two) == 1 assert len(b2.two) == 1 assert b1 not in a1.one assert b2 in a1.one assert a1 not in b1.two assert a1 in b2.two assert b1 in a2.one assert a2 in b1.two # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 def test_association_swap(self): class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, "*") a = A() b1 = B() b2 = B() a.one = b1 a.one = b2 assert a.one.size() == 2 assert a.one[0] is b1 assert a.one[1] is b2 events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) # Application.register_handler(handler) # try: a.one.swap(b1, b2) # assert len(events) == 1 # assert events[0].property is A.one # assert events[0].element is a # finally: # Application.unregister_handler(handler) assert a.one.size() == 2 assert a.one[0] is b2 assert a.one[1] is b1 def test_association_unlink_1(self): # # unlink # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, "*") a1 = A() a2 = A() b1 = B() b2 = B() a1.one = b1 a1.one = b2 assert b1 in a1.one assert b2 in a1.one a2.one = b1 # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 # remove b1 from all elements connected to b1 # also the signal should be removed b1.unlink() # assert len(a1._observers.get('__unlink__')) == 1, a1._observers.get('__unlink__') # assert len(b1._observers.get('__unlink__')) == 0, b1._observers.get('__unlink__') assert b1 not in a1.one assert b2 in a1.one def test_association_unlink_2(self): # # unlink # class A(Element): pass class B(Element): pass class C(Element): pass A.one = association("one", B, 0, "*", opposite="two") B.two = association("two", A, 0, "*") a1 = A() a2 = A() b1 = B() b2 = B() a1.one = b1 a1.one = b2 assert b1 in a1.one assert b2 in a1.one assert a1 in b1.two assert a1 in b2.two a2.one = b1 # assert len(a1._observers.get('__unlink__')) == 0 # assert len(b1._observers.get('__unlink__')) == 0 # remove b1 from all elements connected to b1 # also the signal should be removed b1.unlink() # assert len(a1._observers.get('__unlink__')) == 1, a1._observers.get('__unlink__') # assert len(b1._observers.get('__unlink__')) == 0, b1._observers.get('__unlink__') assert b1 not in a1.one assert b2 in a1.one assert a1 not in b1.two assert a1 in b2.two def test_attributes(self): import types class A(Element): pass A.a = attribute("a", bytes, "default") a = A() assert a.a == "default", a.a a.a = "bar" assert a.a == "bar", a.a del a.a assert a.a == "default" try: a.a = 1 except AttributeError: pass # ok else: assert 0, "should not set integer" def test_enumerations(self): import types class A(Element): pass A.a = enumeration("a", ("one", "two", "three"), "one") a = A() assert a.a == "one" a.a = "two" assert a.a == "two" a.a = "three" assert a.a == "three" try: a.a = "four" except AttributeError: assert a.a == "three" else: assert 0, "a.a could not be four" del a.a assert a.a == "one" def skiptest_notify(self): import types class A(Element): notified = None def notify(self, name, pspec): self.notified = name A.assoc = association("assoc", A) A.attr = attribute("attr", bytes, "default") A.enum = enumeration("enum", ("one", "two"), "one") a = A() assert a.notified == None a.assoc = A() assert a.notified == "assoc", a.notified a.attr = "newval" assert a.notified == "attr", a.notified a.enum = "two" assert a.notified == "enum", a.notified a.notified = None a.enum = "two" # should not notify since value hasn't changed. assert a.notified == None def test_derivedunion(self): class A(Element): pass A.a = association("a", A) A.b = association("b", A, 0, 1) A.u = derivedunion("u", object, 0, "*", A.a, A.b) a = A() assert len(a.a) == 0, "a.a = %s" % a.a assert len(a.u) == 0, "a.u = %s" % a.u a.a = b = A() a.a = c = A() assert len(a.a) == 2, "a.a = %s" % a.a assert b in a.a assert c in a.a assert len(a.u) == 2, "a.u = %s" % a.u assert b in a.u assert c in a.u a.b = d = A() assert len(a.a) == 2, "a.a = %s" % a.a assert b in a.a assert c in a.a assert d == a.b assert len(a.u) == 3, "a.u = %s" % a.u assert b in a.u assert c in a.u assert d in a.u def skiptest_deriveduntion_notify(self): class A(Element): pass class E(Element): notified = False def notify(self, name, pspec): if name == "u": self.notified = True E.a = association("a", A) E.u = derivedunion("u", A, 0, "*", E.a) e = E() assert e.notified == False e.a = A() assert e.notified == True def test_derivedunion_listmixins(self): class A(Element): pass A.a = association("a", A) A.b = association("b", A) A.u = derivedunion("u", A, 0, "*", A.a, A.b) A.name = attribute("name", str, "default") a = A() a.a = A() a.a = A() a.b = A() a.a[0].name = "foo" a.a[1].name = "bar" a.b[0].name = "baz" assert list(a.a[:].name) == ["foo", "bar"] assert sorted(list(a.u[:].name)) == ["bar", "baz", "foo"] def test_composite(self): class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.comp = association("comp", A, composite=True, opposite="other") A.other = association("other", A, composite=False, opposite="comp") a = A() a.name = "a" b = A() b.name = "b" a.comp = b assert b in a.comp assert a in b.other a.unlink() assert a.is_unlinked assert b.is_unlinked def skiptest_derivedunion(self): class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.a = association("a", A, upper=1) A.b = association("b", A) A.derived_a = derivedunion("derived_a", A, 0, 1, A.a) A.derived_b = derivedunion("derived_b", A, 0, "*", A.b) events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) Application.register_handler(handler) try: a = A() a.a = A() assert len(events) == 2, events assert events[0].property is A.derived_a assert events[1].property is A.a finally: Application.unregister_handler(handler) def skiptest_derivedunion_events(self): from zope import component from gaphor.UML.event import ( DerivedSetEvent, DerivedAddEvent, DerivedDeleteEvent, ) class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.a1 = association("a1", A, upper=1) A.a2 = association("a2", A, upper=1) A.b1 = association("b1", A, upper="*") A.b2 = association("b2", A, upper="*") A.b3 = association("b3", A, upper=1) A.derived_a = derivedunion("derived_a", object, 0, 1, A.a1, A.a2) A.derived_b = derivedunion("derived_b", object, 0, "*", A.b1, A.b2, A.b3) events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) Application.register_handler(handler) try: a = A() a.a1 = A() assert len(events) == 2 assert events[0].property is A.derived_a assert events[1].property is A.a1 assert a.derived_a is a.a1 a.a1 = A() assert len(events) == 4, len(events) assert a.derived_a is a.a1 a.a2 = A() # Should not emit DerivedSetEvent assert len(events) == 5, len(events) assert events[4].property is A.a2 del events[:] old_a1 = a.a1 del a.a1 assert len(events) == 2, len(events) assert events[0].property is A.derived_a assert events[0].new_value is a.a2, "%s %s %s" % ( a.a1, a.a2, events[3].new_value, ) assert events[0].old_value is old_a1, "%s %s %s" % ( a.a1, a.a2, events[3].old_value, ) assert events[1].property is A.a1 del events[:] old_a2 = a.a2 del a.a2 assert len(events) == 2, len(events) assert events[0].property is A.derived_a assert events[0].new_value is None, "%s %s %s" % ( a.a1, a.a2, events[5].new_value, ) assert events[0].old_value is old_a2, "%s %s %s" % ( a.a1, a.a2, events[5].old_value, ) assert events[1].property is A.a2 del events[:] assert len(events) == 0, len(events) a.b1 = A() assert len(events) == 2 assert events[0].property is A.derived_b assert events[1].property is A.b1 a.b2 = A() assert len(events) == 4 assert events[2].property is A.derived_b assert events[3].property is A.b2 a.b2 = A() assert len(events) == 6 assert events[4].property is A.derived_b assert events[5].property is A.b2 a.b3 = A() assert len(events) == 8, len(events) assert events[6].property is A.derived_b assert events[7].property is A.b3 # Add b3's value to b2, should not emit derived union event a.b2 = a.b3 assert len(events) == 9, len(events) assert events[8].property is A.b2 # Remove b3's value to b2 del a.b2[a.b3] assert len(events) == 10, len(events) assert events[9].property is A.b2 a.b3 = A() assert len(events) == 13, len(events) assert events[10].property is A.derived_b assert isinstance(events[10], DerivedDeleteEvent), type(events[10]) assert events[11].property is A.derived_b assert isinstance(events[11], DerivedAddEvent), type(events[11]) assert events[12].property is A.b3 del a.b3 assert len(events) == 15, len(events) assert events[13].property is A.derived_b assert isinstance(events[13], DerivedDeleteEvent), type(events[10]) assert events[14].property is A.b3 finally: Application.unregister_handler(handler) def skiptest_redefine(self): from zope import component from gaphor.application import Application class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.a = association("a", A, upper=1) A.a = redefine(A, "a", A, A.a) events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) Application.register_handler(handler) try: a = A() a.a = A() assert len(events) == 2 assert events[0].property is A.a, events[0].property assert events[1].property is A.a.original, events[1].property finally: Application.unregister_handler(handler) def skiptest_redefine_subclass(self): from zope import component class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.a = association("a", A, upper=1) class B(A): pass B.b = redefine(B, "b", A, A.a) events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) Application.register_handler(handler) try: a = A() a.a = A() # Only a.a changes, no B class involved assert len(events) == 1 assert events[0].property is A.a, events[0].property # assert events[1].property is A.a.original, events[1].property del events[:] a = B() a.a = A() # Now events are sent for both association and redefine assert len(events) == 2 assert events[0].property is B.b, events[0].property assert events[1].property is B.b.original, events[1].property finally: Application.unregister_handler(handler) if __name__ == "__main__": unittest.main() PK! i,00gaphor/UML/tests/test_uml2.pyimport unittest import gaphor.UML as UML class ClassesTestCase(unittest.TestCase): def setUp(self): self.factory = UML.ElementFactory() def tearDown(self): del self.factory def test_association(self): """Testing Association elements in the meta-model""" try: element = self.factory.create(UML.Association) except AttributeError: self.fail("Association elements are not part of the meta-model") self.assertFalse( element.isDerived, "The isDerived property should default to False - %s" % element.isDerived, ) property1 = self.factory.create(UML.Property) property2 = self.factory.create(UML.Property) element.memberEnd = property1 element.memberEnd = property2 element.ownedEnd = property1 element.navigableOwnedEnd = property1 self.assertTrue( property1 in element.member, "Namespace.member does not contain memberEnd - %s" % element.member, ) self.assertTrue( property2 in element.member, "Namespace.member does not contain memberEnd - %s" % element.member, ) self.assertTrue( property1 in element.feature, "Classifier.feature does not contain ownedEnd - %s" % element.feature, ) self.assertTrue( property1 in element.ownedMember, "Namespace.ownedMember does not contain ownedEnd - %s" % element.ownedEnd, ) self.assertTrue( property1 in element.ownedEnd, "Association.ownedEnd does not contain navigableOwnedEnd - %s" % element.ownedEnd, ) # def test_association_class(self): # try: # element = self.factory.create(UML.AssociationClass) # except AttributeError: # self.fail('AssociationClass elements are not part of the meta-model') def test_class(self): """Testing Class elements in the meta-model""" try: element = self.factory.create(UML.Class) except AttributeError: self.fail("Class elements are not part of the meta-model") property1 = self.factory.create(UML.Property) operation1 = self.factory.create(UML.Operation) element.ownedAttribute = property1 element.ownedOperation = operation1 self.assertTrue( property1 in element.attribute, "Classifier.attribute does not contain ownedAttribute - %s" % element.attribute, ) self.assertTrue( property1 in element.ownedMember, "Namespace.ownedMember does not contain ownedAttribute - %s" % element.ownedMember, ) self.assertTrue( operation1 in element.feature, "Classifier.feature does not contain ownedOperation - %s" % element.feature, ) self.assertTrue( operation1 in element.ownedMember, "Namespace.ownedMember does not contain ownedOperation" % element.ownedMember, ) def test_comment(self): """Testing Comment elements in the meta-model""" try: element = self.factory.create(UML.Comment) except AttributeError: self.fail("Comment elements are not part of the meta-model") element.body = "Comment body" self.assertTrue( element.body == "Comment body", "Incorrect comment body - %s" % element.body ) annotatedElement = self.factory.create(UML.Class) element.annotatedElement = annotatedElement self.assertTrue( annotatedElement in element.annotatedElement, "Incorrect annotated element - %s" % element.annotatedElement, ) def test_constraint(self): """Testing Constraint elements in the meta-model""" try: element = self.factory.create(UML.Constraint) except AttributeError: self.fail("Constraint elements are not part of the meta-model") constrainedElement = self.factory.create(UML.Class) element.constrainedElement = constrainedElement element.specification = "Constraint specification" self.assertTrue( constrainedElement in element.constrainedElement, "Constraint.constrainedElement does not contain the correct element - %s" % element.constrainedElement, ) self.assertTrue( element.specification == "Constraint specification", "Constraint.specification is incorrect - %s" % element.specification, ) def test_dependency(self): """Testing Dependency elements in the meta-model""" try: element = self.factory.create(UML.Dependency) except AttributeError: self.fail("Dependency elements are not part of the meta-model") client = self.factory.create(UML.Package) supplier = self.factory.create(UML.Package) element.client = client element.supplier = supplier self.assertTrue( client in element.client, "Dependency.client does not contain client - %s" % element.client, ) self.assertTrue( supplier in element.supplier, "Dependency.supplier does not contain supplier - %s" % element.supplier, ) def test_element_import(self): try: element = self.factory.create(UML.ElementImport) except AttributeError: self.fail("ElementImport elements are not part of the meta-model") def test_enumeration(self): try: element = self.factory.create(UML.Enumeration) except AttributeError: self.fail("Enumeration elements are not part of the meta-model") def test_generalization(self): try: element = self.factory.create(UML.Generalization) except AttributeError: self.fail("Generalization elements are not part of the meta-model") def test_interface(self): try: element = self.factory.create(UML.Interface) except AttributeError: self.fail("Interface elements are not part of the meta-model") def test_namespace(self): try: element = self.factory.create(UML.Namespace) except AttributeError: self.fail("Namespace elements are not part of the meta-model") def test_operation(self): try: element = self.factory.create(UML.Operation) except AttributeError: self.fail("Operation elements are not part of the meta-model") def test_package(self): try: element = self.factory.create(UML.Package) except AttributeError: self.fail("Package elements are not part of the meta-model") def test_parameter(self): try: element = self.factory.create(UML.Parameter) except AttributeError: self.fail("Parameter elements are not part of the meta-model") def test_property(self): try: element = self.factory.create(UML.Property) except AttributeError: self.fail("Property elements are not part of the meta-model") def test_realization(self): try: element = self.factory.create(UML.Realization) except AttributeError: self.fail("Realization elements are not part of the meta-model") class Uml2TestCase(unittest.TestCase): def test_ids(self): factory = UML.ElementFactory() c = factory.create(UML.Class) assert c.id p = factory.create_as(UML.Class, id=False) assert p.id is False, p.id def test1(self): factory = UML.ElementFactory() c = factory.create(UML.Class) p = factory.create(UML.Package) c.package = p self.assertEqual(c.package, p) self.assertEqual(c.namespace, p) self.assertTrue(c in p.ownedElement) def testOwnedMember_Unlink(self): factory = UML.ElementFactory() c = factory.create(UML.Class) p = factory.create(UML.Package) c.package = p c.unlink() self.assertEqual([p], factory.lselect()) # def test_lower_upper(self): # """ # Test MultiplicityElement.{lower|upper} # """ # assert UML.MultiplicityElement.lowerValue in UML.MultiplicityElement.lower.subsets # # e = UML.MultiplicityElement() # e.lowerValue = '2' # assert e.lower == '2', e.lower # # assert UML.MultiplicityElement.upperValue in UML.MultiplicityElement.upper.subsets # # e.upperValue = 'up' # assert UML.MultiplicityElement.upper.version == 4, UML.MultiplicityElement.upper.version # assert e.upper == 'up' # e.upperValue = 'down' # assert UML.MultiplicityElement.upper.version == 5, UML.MultiplicityElement.upper.version # assert e.upper == 'down', e.upper # # # TODO: test signal handling def test_property_is_composite(self): p = UML.Property() assert p.isComposite == False, p.isComposite p.aggregation = "shared" assert p.isComposite == False, p.isComposite p.aggregation = "composite" assert p.isComposite == True, p.isComposite def test_association_endType(self): factory = UML.ElementFactory() c1 = UML.Class() c2 = UML.Class() a = UML.Association() a.memberEnd = UML.Property() a.memberEnd = UML.Property() a.memberEnd[0].type = c1 a.memberEnd[1].type = c2 c1.ownedAttribute = a.memberEnd[0] c2.ownedAttribute = a.memberEnd[1] assert c1 in a.endType assert c2 in a.endType c3 = UML.Class() a.memberEnd[1].type = c3 assert c1 in a.endType assert c3 in a.endType def test_property_navigability(self): factory = UML.ElementFactory() p = factory.create(UML.Property) assert p.navigability is None c1 = factory.create(UML.Class) c2 = factory.create(UML.Class) a = UML.model.create_association(factory, c1, c2) assert a.memberEnd[0].navigability is None assert a.memberEnd[1].navigability is None UML.model.set_navigability(a, a.memberEnd[0], True) assert a.memberEnd[0].navigability is True assert a.memberEnd[1].navigability is None UML.model.set_navigability(a, a.memberEnd[0], False) assert a.memberEnd[0].navigability is False assert a.memberEnd[1].navigability is None def test_namedelement_qualifiedname(self): factory = UML.ElementFactory() p = factory.create(UML.Package) p.name = "Package" c = factory.create(UML.Class) c.name = "Class" self.assertEqual(("Class",), c.qualifiedName) p.ownedClassifier = c self.assertEqual(("Package", "Class"), c.qualifiedName) def test_extension_metaclass(self): factory = UML.ElementFactory() c = factory.create(UML.Class) c.name = "Class" s = factory.create(UML.Stereotype) s.name = "Stereotype" e = UML.model.create_extension(factory, c, s) self.assertEqual(c, e.metaclass) def test_metaclass_extension(self): factory = UML.ElementFactory() c = factory.create(UML.Class) c.name = "Class" s = factory.create(UML.Stereotype) s.name = "Stereotype" self.assertEqual([], c.extension) self.assertEqual([], s.extension) e = UML.model.create_extension(factory, c, s) print(e.memberEnd) self.assertEqual([e], c.extension) self.assertEqual([], s.extension) assert e.ownedEnd.type is s def test_operation_parameter_deletion(self): factory = UML.ElementFactory() self.assertEqual(0, len(factory.lselect())) c = factory.create(UML.Class) c.name = "Class" o = factory.create(UML.Operation) c.ownedOperation = o UML.parse(o, "a(x: int, y: int)") c.unlink() self.assertEqual(0, len(factory.lselect()), factory.lselect()) PK!sBgaphor/UML/tests/test_umlfmt.py""" Formatting of UML model elements into text tests. """ import unittest from gaphor.application import Application from gaphor.UML.elementfactory import ElementFactory from gaphor.UML.umlfmt import format import gaphor.UML.uml2 as UML factory = ElementFactory() class AttributeTestCase(unittest.TestCase): def setUp(self): pass def tearDown(self): factory.flush() def test_simple_format(self): """Test simple attribute formatting """ a = factory.create(UML.Property) a.name = "myattr" self.assertEqual("+ myattr", format(a)) a.typeValue = "int" self.assertEqual("+ myattr: int", format(a)) PK! i4gaphor/UML/tests/test_umllex.py""" Parsing of UML model elements from string tests. """ import unittest from gaphor.application import Application from gaphor.UML.elementfactory import ElementFactory from gaphor.UML.umllex import parse from gaphor.UML.umllex import attribute_pat, operation_pat, parameter_pat from gaphor import UML def dump_prop(prop): m = attribute_pat.match(prop) # print m.groupdict() def dump_oper(oper): m = operation_pat.match(oper) if m: g = m.group else: # set name to oper return # print g('vis'), g('name'), g('type'), g('mult_l'), g('mult_u'), g('tags') if g("params"): params = g("params") while params: m = parameter_pat.match(params) g = m.group # print ' ', g('dir') or 'in', g('name'), g('type'), g('mult_l'), g('mult_u'), g('default'), g('tags') params = g("rest") dump_prop("#/name") dump_prop('+ / name : str[1..*] = "aap" { static }') dump_prop('+ / name : str[*] = "aap" { static }') dump_oper('myfunc(aap:str = "aap", out two): type') dump_oper(" myfunc2 ( ): type") dump_oper('myfunc(aap:str[1] = "aap" { tag1, tag2 }, out two {tag3}): type') factory = ElementFactory() class AttributeTestCase(unittest.TestCase): """ Parsing an attribute tests. """ def setUp(self): pass def tearDown(self): factory.flush() def test_parse_property_simple(self): """Test simple property parsing """ a = factory.create(UML.Property) UML.parse(a, "myattr") self.assertFalse(a.isDerived) self.assertEqual("myattr", a.name) self.assertTrue(a.typeValue is None, a.typeValue) self.assertTrue(a.lowerValue is None, a.lowerValue) self.assertTrue(a.upperValue is None, a.upperValue) self.assertTrue(a.defaultValue is None, a.defaultValue) def test_parse_property_complex(self): """Test complex property parsing """ a = factory.create(UML.Property) UML.parse(a, '+ / name : str[0..*] = "aap" { static }') self.assertEqual("public", a.visibility) self.assertTrue(a.isDerived) self.assertEqual("name", a.name) self.assertEqual("str", a.typeValue) self.assertEqual("0", a.lowerValue) self.assertEqual("*", a.upperValue) self.assertEqual('"aap"', a.defaultValue) def test_parse_property_invalid(self): """Test parsing property with invalid syntax """ a = factory.create(UML.Property) UML.parse(a, '+ name = str[*] = "aap" { static }') self.assertEqual('+ name = str[*] = "aap" { static }', a.name) self.assertFalse(a.isDerived) self.assertTrue(not a.typeValue) self.assertTrue(not a.lowerValue) self.assertTrue(not a.upperValue) self.assertTrue(not a.defaultValue) class AssociationEndTestCase(unittest.TestCase): """ Parsing association end tests. """ def setUp(self): pass def tearDown(self): factory.flush() def test_parse_association_end(self): """Test parsing of association end """ a = factory.create(UML.Association) p = factory.create(UML.Property) p.association = a UML.parse(p, "end") self.assertEqual("end", p.name) self.assertTrue(not p.typeValue) self.assertTrue(not p.lowerValue) self.assertTrue(not p.upperValue) self.assertTrue(not p.defaultValue) def test_parse_multiplicity(self): """Test parsing of multiplicity """ a = factory.create(UML.Association) p = factory.create(UML.Property) p.association = a UML.parse(p, "0..2 { tag }") self.assertTrue(p.name is None) self.assertTrue(not p.typeValue) self.assertEqual("0", p.lowerValue) self.assertEqual("2", p.upperValue) self.assertTrue(not p.defaultValue) def test_parse_multiplicity2(self): """Test parsing of multiplicity with multiline constraints """ a = factory.create(UML.Association) p = factory.create(UML.Property) p.association = a UML.parse(p, "0..2 { tag1, \ntag2}") self.assertTrue(p.name is None) self.assertTrue(not p.typeValue) self.assertEqual("0", p.lowerValue) self.assertEqual("2", p.upperValue) self.assertTrue(not p.defaultValue) def test_parse_derived_end(self): """Test parsing derived association end """ a = factory.create(UML.Association) p = factory.create(UML.Property) p.association = a UML.parse(p, "-/end[*] { mytag}") self.assertEqual("private", p.visibility) self.assertTrue(p.isDerived) self.assertEqual("end", p.name) self.assertTrue(not p.typeValue) self.assertTrue(not p.lowerValue) self.assertEqual("*", p.upperValue) self.assertTrue(not p.defaultValue) class OperationTestCase(unittest.TestCase): """ Operation parsing tests. """ def setUp(self): factory.flush() def tearDown(self): factory.flush() def test_parse_operation(self): """Test parsing simple operation """ o = factory.create(UML.Operation) UML.parse(o, "myfunc()") self.assertEqual("myfunc", o.name) self.assertTrue(not o.returnResult[0].typeValue) self.assertFalse(o.formalParameter) def test_parse_operation_return(self): """Test parsing operation with return value """ o = factory.create(UML.Operation) UML.parse(o, "+ myfunc(): int") self.assertEqual("myfunc", o.name) self.assertEqual("int", o.returnResult[0].typeValue) self.assertEqual("public", o.visibility) self.assertTrue(not o.formalParameter) def test_parse_operation_2_params(self): """Test parsing of operation with two parameters """ o = factory.create(UML.Operation) UML.parse(o, "# myfunc2 (a: str, b: int = 3 { static}): float") self.assertEqual("myfunc2", o.name) self.assertEqual("float", o.returnResult[0].typeValue) self.assertEqual("protected", o.visibility) self.assertEqual(2, len(o.formalParameter)) self.assertEqual("a", o.formalParameter[0].name) self.assertEqual("str", o.formalParameter[0].typeValue) self.assertTrue(o.formalParameter[0].defaultValue is None) self.assertEqual("b", o.formalParameter[1].name) self.assertEqual("int", o.formalParameter[1].typeValue) self.assertEqual("3", o.formalParameter[1].defaultValue) def test_parse_operation_1_param(self): """Test parsing of operation with one parameter """ o = factory.create(UML.Operation) UML.parse(o, "- myfunc2 (a: node): double") self.assertEqual("myfunc2", o.name) self.assertEqual("double", o.returnResult[0].typeValue) self.assertEqual("private", o.visibility) self.assertEqual(1, len(o.formalParameter)) self.assertEqual("a", o.formalParameter[0].name) self.assertEqual("node", o.formalParameter[0].typeValue) self.assertTrue(o.formalParameter[0].defaultValue is None) def test_parse_operation_invalid_syntax(self): """Test operation parsing with invalid syntax """ o = factory.create(UML.Operation) UML.parse(o, "- myfunc2: myType2") self.assertEqual("- myfunc2: myType2", o.name) PK!*gaphor/UML/uml2.gaphor 0 0 0 AssociationClasses composite ownedOperation * public extension 1 public EnumerationLiteral owningInstance 1 public Expression 0 {All ends of CommunicationPath are typed by Nodes} 0 PackageMerge 0 value Integer Extension Multiplicities 0 0 0 (1.0, 0.0, 0.0, 1.0, 189.0, 37.0) 100.0 59.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 129.0, 172.0) 472.0 131.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 611.0, 180.0) 174.0 93.0 0 0 (1.0, 0.0, 0.0, 1.0, 611.0, 212.0) 0 1 [(0.0, 0.0), (-244.0, -40.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 611.0, 258.9999999999999) 0 1 [(0.0, 0.0), (-244.0, 44.000000000000114)] 0 0 (1.0, 0.0, 0.0, 1.0, 239.0, 96.0) 0 1 [(0.0, 0.0), (0.0, 76.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 169.0, 437.0) 140.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 239.73051224928787, 380.6650717703349) 0 1 [(0.0, 0.0), (-1.7305122492878695, 56.33492822966508)] 0 (1.0, 0.0, 0.0, 1.0, 309.0, 464.0) 0 1 [(0.0, 0.0), (140.0, 5.684341886080802e-14)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 449.0, 435.0) 100.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 161.5247524752475, 330.6650717703349) 166.475247525 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 420.5247524752475, 330.6650717703349) 195.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 496.0, 392.6650717703349) 0 1 [(0.0, 0.0), (0.9999999999998863, 42.33492822966508)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 150.0, 611.4439461883408) 174.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 238.00000000000028, 496.0) 0 1 [(0.0, 0.0), (-0.35555555555708906, 115.44394618834076)] 0 (1.0, 0.0, 0.0, 1.0, 300.0, 520.0) 201.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 237.84783121346643, 545.4070896364867) 0 1 [(0.0, 0.0), (62.152168786533565, -9.407089636486717)] 0 abstraction 1 public Actor composite 0 upperValue 1 public redefinedProperty * public Reception 0 0 0 (1.0, 0.0, 0.0, 1.0, 544.0, 197.5) 100.0 62.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 522.0, 96.0) 177.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 615.7835820895523, 146.0) 0 1 [(0.0, 0.0), (-22.783582089552283, 51.5)] (1.0, 0.0, 0.0, 1.0, 13.0, 14.0) 538.0 44.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 314.5, 96.0) 158.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 106.0, 96.0) 118.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 106.0, 368.0) 114.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 543.0, 290.0) 111.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 113.0, 203.0) 100.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 322.0, 209.0) 100.0 117.0 0 0 (1.0, 0.0, 0.0, 1.0, 374.0, 146.0) 0 0 [(0.0, 0.0), (0.0, 63.0)] 0 (1.0, 0.0, 0.0, 1.0, 165.0, 146.0) 0 0 [(0.0, 0.0), (0.0, 57.0)] 0 (1.0, 0.0, 0.0, 1.0, 164.0, 254.0) 0 0 [(0.0, 0.0), (0.0, 114.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 213.0, 229.0) 0 0 [(0.0, 0.0), (109.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 422.0, 224.0) 0 0 [(0.0, 0.0), (122.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 422.0, 313.0) 0 0 [(0.0, 0.0), (121.0, 0.0)] 0 0 symbol String 1 Kernel 0 composite specification 1 public composite action * composite literal * public MergeNode body String composite manifestation * public ownedComment * public Feature Components 0 0 0 (1.0, 0.0, 0.0, 1.0, 9.0, 139.0) 111.0 103.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 248.0, 136.0) 471.0 118.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 338.0, 30.0) 100.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 385.99999999999994, 92.0) 0 1 [(0.0, 0.0), (75.63765807962574, 44.0)] 0 (1.0, 0.0, 0.0, 1.0, 120.0, 167.0) 0 1 [(0.0, 0.0), (128.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 120.0, 225.0) 0 1 [(0.0, 0.0), (128.0, -2.842170943040401e-14)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 327.0, 355.0) 135.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 393.9130434782608, 355.0) 0 1 [(0.0, 0.0), (64.33354066359271, -101.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 307.13148788927333, 523.0) 156.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 385.04297328597187, 523.0) 0 1 [(0.0, 0.0), (10.0439832357672, -106.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 785.5824175824175, 148.0) 195.0 76.0 0 0 (1.0, 0.0, 0.0, 1.0, 785.5824175824175, 179.92000000000004) 0 1 [(0.0, 0.0), (-66.58241758241752, 1.079999999999984)] 0 composite presentation * public value String Artifacts 1 0 0 (1.0, 0.0, 0.0, 1.0, 131.0, 209.0) 471.0 71.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 120.0, 47.0) 156.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 196.8181818181818, 119.0) 0 1 [(0.0, 0.0), (147.93321743554958, 90.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 522.0, 220.0) 136.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 602.0, 236.00000000000003) 0 1 [(0.0, 0.0), (-80.0, 1.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 530.3801369863014, 47.0) 135.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 596.898165968527, 109.0) 0 1 [(0.0, 0.0), (-6.898165968526996, 111.0)] Actions 0 0 0 (1.0, 0.0, 0.0, 1.0, 253.0, 51.0) 125.0 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 187.0, 172.0) 125.0 60.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 320.0, 173.0) 125.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 281.75, 101.0) 0 1 [(0.0, 0.0), (-36.879629629629676, 71.0)] 0 (1.0, 0.0, 0.0, 1.0, 338.0, 101.0) 0 1 [(0.0, 0.0), (45.75, 72.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 610.0, 170.0) 125.0 60.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 573.0, 323.0) 174.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 660.0, 230.0) 0 1 [(0.0, 0.0), (0.6444444444455257, 93.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 445.0, 201.0) 0 1 [(0.0, 0.0), (165.0, -1.9999999999999716)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 246.0, 317.0) 472.0 79.0 0 0 (1.0, 0.0, 0.0, 1.0, 237.76, 232.0) 0 1 [(0.0, 0.0), (175.2335594675826, 85.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 370.0, 233.0) 0 1 [(0.0, 0.0), (132.16487762988424, 84.0)] 0 1 Relationship useCase 1 public 1 0 lower Integer 1 composite nestedInterface * public 1 importedMember * public ObjectFlow 0 1 1 target * public No inheritance, since it is already done by BehavioredClassifier. PowerTypes are not implemented yet. 0 ownerReturnParam 1 composite ownedDiagram * public LiteralSpecification ActivityFinalNode PowerTypes (1.0, 0.0, 0.0, 1.0, 46.0, 51.0) 243.0 86.0 0 importedPackage 1 public in direction ParameterDirectionKind nodeContents * 1 extensionLocation * public RedefinableElement owningProfile 1 composite ownedRule * public 0 * 0 CommunicationPath Classes 0 0 0 (1.0, 0.0, 0.0, 1.0, 332.0, 26.0) 156.0 58.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 333.0, 159.0) 100.0 264.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 796.0, 114.0) 472.0 294.0 0 0 (1.0, 0.0, 0.0, 1.0, 385.0, 93.0) 0 1 [(0.0, 0.0), (-1.7053025658242404e-13, 66.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 1328.0, 354.0) 472.0 111.0 0 0 (1.0, 0.0, 0.0, 1.0, 433.0, 185.0) 0 1 [(0.0, 0.0), (363.0, 0.7792642140468047)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 845.6333333333332, 19.19999999999999) 168.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 930.3367149758458, 79.19999999999999) 0 1 [(0.0, 0.0), (81.85116590102393, 34.8)] 0 (1.0, 0.0, 0.0, 1.0, 433.0, 247.0) 1 1 [(0.0, 0.0), (102.0, 0.0), (102.0, 50.0), (0.0, 50.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 796.0, 236.9096989966555) 1 1 [(0.0, 0.0), (-164.0, 0.0), (-164.0, 53.09698996655521), (0.0, 53.09698996655521)] 0 0 (1.0, 0.0, 0.0, 1.0, 796.0, 330.3210702341137) 1 1 [(0.0, 0.0), (-164.0, 0.0), (-164.0, 53.09698996655521), (0.0, 53.09698996655521)] 0 0 (1.0, 0.0, 0.0, 1.0, 488.0, 51.51999999999998) 1 1 [(0.0, 0.0), (211.0, 0.0), (211.0, 77.22916387959867), (308.0, 77.22916387959867)] 0 0 (1.0, 0.0, 0.0, 1.0, 333.0, 217.88551185910802) 1 1 [(0.0, 0.0), (-196.0, 0.0), (-196.0, -173.32551185910802), (-1.0, -173.32551185910802)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 331.9004524886878, 473.20000000000005) 178.0 133.0 0 0 (1.0, 0.0, 0.0, 1.0, 333.0, 368.88551185910796) 1 1 [(0.0, 0.0), (-214.0, 0.0), (-214.0, 160.88571808741614), (-1.0995475113122097, 160.88571808741614)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 1392.0, 115.0) 472.0 128.0 0 0 (1.0, 0.0, 0.0, 1.0, 1090.0, 114.0) 0 1 [(0.0, 0.0), (302.0, 17.925619834710744)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1361.4027149321266, 13.0) 129.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1498.1656184486374, 14.0) 156.0 58.0 0 0 (1.0, 0.0, 0.0, 1.0, 1428.532778280543, 63.0) 0 1 [(0.0, 0.0), (49.386514797388145, 52.0)] 0 (1.0, 0.0, 0.0, 1.0, 1576.7984978422126, 72.0) 0 1 [(0.0, 0.0), (192.29173288870197, 43.0)] 0 (1.0, 0.0, 0.0, 1.0, 1864.0, 179.5663716814159) 0 1 [(0.0, 0.0), (-131.0589390962673, -17.673514538558692)] 0 0 (1.0, 0.0, 0.0, 1.0, 1090.0, 114.0) 0 1 [(0.0, 0.0), (302.0, 79.28099173553719)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1393.156186612576, 249.0) 174.0 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 1393.156186612576, 273.83999999999986) 0 1 [(0.0, 0.0), (-303.156186612576, 134.16000000000014)] 0 0 (1.0, 0.0, 0.0, 1.0, 1090.0, 408.0) 1 1 [(0.0, 0.0), (36.0, 0.0), (36.0, 0.0), (0.0, 0.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1732.9410609037327, 145.0) 100.0 50.0 0 (1.0, 0.0, 0.0, 1.0, 403.0, 97.0) 162.0 78.0 0 0 (1.0, 0.0, 0.0, 1.0, 1392.0, 237.49462365591398) 0 0 [(0.0, 0.0), (-302.0, -123.49462365591398)] 0 composite implementation * public contract 1 public 1 provided * public 1 1 endType * public composite 1 input * 1 parameter * public 0 visibility VisibilityKind 1 public 0 0 namespace_ 1 public 0 importedProfile 1 public Permission true isUnique Boolean 0 1 isOrdered Boolean none composite slot * public mergedPackage 1 public FinalNode 1 0 default String 1 isAbstract Boolean 0 substitutingClassifier 1 public extensionPoint * public composite ownedMember * public composite 0 lowerValue 1 public Realization parameter 1 composite 0 specification 1 public isSubstitutable Boolean general 1 public language String Deployments composite generalization * public 0 Pin Generalization 0 value object false isQuery Boolean 0 preContext 1 public 0 decisionInput 1 0 ObjectNode CommonBehaviors 0 0 0 (1.0, 0.0, 0.0, 1.0, 85.0, 80.0) 156.0 72.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 40.0, 195.0) 194.0 122.0 0 0 (1.0, 0.0, 0.0, 1.0, 162.24271844660194, 152.0) 0 1 [(0.0, 0.0), (-26.89577967109173, 43.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 507.0, 75.0) 100.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 478.0, 193.0) 468.0 338.0 0 0 (1.0, 0.0, 0.0, 1.0, 554.9999999999999, 137.0) 0 1 [(0.0, 0.0), (130.79507389162575, 56.0)] 0 (1.0, 0.0, 0.0, 1.0, 234.0, 230.6529680365297) 0 1 [(0.0, 0.0), (244.0, -1.4711498547115411)] 0 0 (1.0, 0.0, 0.0, 1.0, 478.0, 450.5454545454545) 1 1 [(0.0, 0.0), (-152.0, 0.0), (-152.0, 50.0), (0.0, 50.0)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 510.0, 599.0) 472.0 91.0 0 0 (1.0, 0.0, 0.0, 1.0, 566.1214285714284, 531.0) 0 1 [(0.0, 0.0), (129.8729770229769, 68.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 56.0, 364.87351778656125) 464.0 79.0 0 0 (1.0, 0.0, 0.0, 1.0, 520.0, 396.88951931658806) 0 1 [(0.0, 0.0), (-42.0, -4.889519316588121)] 0 composite value_ 1 body String public composite ownedOperation * public 0 Device none aggregation AggregationKind composite 0 metamodelReference * public 1 fileName String public 0 postContext 1 public composite precondition * public StructuralFeature 0 0 interface_ 1 NamedElement addition 1 public subsets ownedElement 1 0 owner 1 public 1 0 context 1 public raisedException * public UML UseCase LiteralNull composite returnResult * public ActivityNode InputPin Manifestation composite ownedStereotype * public composite 1 ownedMember * public ControlNode none 0 composite 0 defaultValue 1 public composite ownedMember * public Component composite nestedClassifier * public Interface composite elementImport * public 0 composite ownedAttribute * public 1 Element UseCases constrainedElement * public 0 subsets target OpaqueExpression Dependency composite 0 defaultValue 1 public PowerTypes 0 composite formalParameter * public includingCase 1 public Features 0 0 0 (1.0, 0.0, 0.0, 1.0, 357.0, 58.0) 186.0 58.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 39.0, 202.0) 156.0 58.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 338.0, 198.0) 472.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 195.0, 232.15999999999994) 0 1 [(0.0, 0.0), (143.0, 0.2958823529411916)] 0 0 (1.0, 0.0, 0.0, 1.0, 452.54098360655746, 116.0) 0 1 [(0.0, 0.0), (125.6486004863275, 82.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 180.0, 412.0) 472.0 71.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 224.0, 297.0) 140.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 278.7071428571428, 347.0) 0 1 [(0.0, 0.0), (115.06864189622041, 65.0)] 0 (1.0, 0.0, 0.0, 1.0, 484.64206186723567, 269.0) 0 1 [(0.0, 0.0), (-5.098291151696401, 143.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 483.0, 413.0) 464.0 250.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 575.4566473988439, 284.0) 121.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 630.8305419692704, 334.0) 0 1 [(0.0, 0.0), (-37.187267115469126, 79.0)] 0 (1.0, 0.0, 0.0, 1.0, 464.0000000000003, 269.0) 0 1 [(0.0, 0.0), (83.99999999999972, 144.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 664.0, 71.0) 472.0 131.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 1035.0, 413.0) 472.0 184.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1019.045871559633, 281.0) 140.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 1084.0, 331.0) 0 1 [(0.0, 0.0), (110.11202466598115, 82.0)] 0 (1.0, 0.0, 0.0, 1.0, 1274.1961073184823, 331.0) 0 1 [(0.0, 0.0), (60.796355853836985, 82.0)] 0 (1.0, 0.0, 0.0, 1.0, 655.0, 413.0) 0 1 [(0.0, 0.0), (380.0, 33.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 655.0, 413.0) 0 1 [(0.0, 0.0), (380.0, 91.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 655.0, 663.0) 0 1 [(0.0, 0.0), (380.0, -98.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 655.0, 663.0) 0 1 [(0.0, 0.0), (305.625, -22.22401213292926)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1502.1196581196582, 492.0) 174.0 82.0 0 0 (1.0, 0.0, 0.0, 1.0, 1507.0, 514.0) 0 1 [(0.0, 0.0), (-4.880341880341803, -0.3529227883763042)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 66.0, 297.0) 167.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 150.7101449275362, 347.0) 0 1 [(0.0, 0.0), (70.17391304347825, 65.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 1179.7485893246762, 281.0) 174.876410675 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 960.625, 624.4707903780069) 100.0 50.0 0 Association 0 inout classifier * public Include True isIndirectlyInstantiated Boolean public 0 Interfaces annotatedElement * public Profiles 0 0 0 (1.0, 0.0, 0.0, 1.0, 41.0, 121.0) 100.0 64.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 350.0, 13.0) 151.0 51.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 381.0, 484.0) 146.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 545.0, 120.0) 100.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 43.0, 290.0) 100.0 236.0 0 0 (1.0, 0.0, 0.0, 1.0, 87.00000000000003, 185.0) 0 1 [(0.0, 0.0), (0.9999999999999432, 105.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 346.0, 121.0) 168.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 141.0, 151.72) 0 1 [(0.0, 0.0), (205.0, 0.28000000000000114)] 0 0 (1.0, 0.0, 0.0, 1.0, 408.3636363636365, 177.0) 0 1 [(0.0, 0.0), (-0.36363636363648766, 132.0), (-265.3636363636365, 133.0)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 540.0, 346.0) 118.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 591.0000000000001, 182.0) 0 1 [(0.0, 0.0), (-0.26000000000033197, 164.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 824.8669136550875, 22.0) 158.125760338 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 821.0, 121.0) 471.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 645.0, 151.0) 0 1 [(0.0, 0.0), (176.0, -0.2258064516128968)] 0 0 (1.0, 0.0, 0.0, 1.0, 898.8580159336416, 72.0) 0 1 [(0.0, 0.0), (143.0776246178416, 49.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 1070.0, 120.0) 136.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1072.0, 20.0) 131.866913655 58.0 0 0 (1.0, 0.0, 0.0, 1.0, 1137.9273592177235, 78.0) 0 1 [(0.0, 0.0), (-0.562873236414589, 42.0)] 0 (1.0, 0.0, 0.0, 1.0, 1292.0, 146.2258064516129) 0 1 [(0.0, 0.0), (-222.0, -0.2258064516128968)] 0 0 (1.0, 0.0, 0.0, 1.0, 1124.0, 176.0) 0 1 [(0.0, 0.0), (2.0, 195.0), (-466.0, 197.6785714285715)] 0 0 (1.0, 0.0, 0.0, 1.0, 143.0, 369.0) 0 1 [(0.0, 0.0), (397.0, 3.5714285714284983)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 382.8901098901099, 415.0) 145.10989011 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 143.0, 435.0) 0 1 [(0.0, 0.0), (239.8901098901099, -0.5599999999999454)] 0 0 (1.0, 0.0, 0.0, 1.0, 143.0, 501.0) 0 1 [(0.0, 0.0), (238.0, -1.3199999999999932)] 0 0 (1.0, 0.0, 0.0, 1.0, 423.4520413870247, 64.0) 0 1 [(0.0, 0.0), (-1.088405023388134, 57.0)] source 1 Profiles LiteralInteger 0 importingNamespace 1 public 0 ProfileApplication composite 1 ownedElement * public composite ownedUseCase * public ExtensionPoint 0 class_ 1 public edgeContents * composite group * false isAbstract Boolean Expressions 1 0 0 (1.0, 0.0, 0.0, 1.0, 361.14285714285717, 145.57142857142858) 174.0 67.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 97.14285714285717, 318.57142857142844) 472.0 79.4285714286 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 360.14285714285717, 417.57142857142844) 468.0 83.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 534.1428571428571, 317.57142857142844) 141.0 80.0 0 0 (1.0, 0.0, 0.0, 1.0, 361.14285714285717, 207.00000000000006) 0 1 [(0.0, 0.0), (-16.894117647058863, 111.57142857142838)] 0 (1.0, 0.0, 0.0, 1.0, 451.704260651629, 212.57142857142858) 0 1 [(0.0, 0.0), (147.7567783094102, 204.99999999999986)] 0 (1.0, 0.0, 0.0, 1.0, 535.1428571428571, 212.57142857142858) 0 1 [(0.0, 0.0), (64.00000000000011, 104.99999999999986)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 825.1428571428571, 322.57142857142844) 198.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 675.1428571428571, 343.57142857142844) 0 1 [(0.0, 0.0), (150.0, 0.9999999999997726)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 140.0, 607.0) 471.0 71.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 234.8571428571429, 689.8571428571428) 473.0 71.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 377.2857142857143, 607.0) 473.0 71.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 447.7142857142857, 691.7142857142858) 472.0 71.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 568.1428571428571, 606.0) 108.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 378.1136363636363, 500.57142857142844) 0 1 [(0.0, 0.0), (-170.39935064935062, 106.42857142857156)] 0 (1.0, 0.0, 0.0, 1.0, 414.6396103896103, 500.57142857142844) 0 1 [(0.0, 0.0), (-112.63961038961037, 189.28571428571433)] 0 (1.0, 0.0, 0.0, 1.0, 445.3214285714285, 500.57142857142844) 0 1 [(0.0, 0.0), (-13.321428571428612, 106.42857142857156)] 0 (1.0, 0.0, 0.0, 1.0, 489.1525974025975, 500.57142857142844) 0 1 [(0.0, 0.0), (59.99025974025949, 191.14285714285734)] 0 (1.0, 0.0, 0.0, 1.0, 540.1428571428571, 500.57142857142844) 0 1 [(0.0, 0.0), (79.47663551401888, 105.42857142857156)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 394.52173913043475, 23.0) 140.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 451.0, 73.0) 0 1 [(0.0, 0.0), (0.9999999999999432, 72.57142857142858)] 0 (1.0, 0.0, 0.0, 1.0, 97.14285714285717, 369.1761670185316) 1 1 [(0.0, 0.0), (-72.14285714285717, 0.0), (-72.14285714285717, -193.1761670185316), (264.0, -193.1761670185316)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 285.0, 320.0) 471.0 85.0 0 0 (1.0, 0.0, 0.0, 1.0, 416.9624060150377, 212.57142857142858) 0 1 [(0.0, 0.0), (95.54489854613087, 107.42857142857142)] (1.0, 0.0, 0.0, 1.0, 620.0, 447.0) 143.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 540.1428571428571, 417.57142857142844) 0 1 [(0.0, 0.0), (79.85714285714289, 50.7080733741812)] BasicBehaviors composite mapping 1 public Usage 0 name String 1 public subsettedProperty * public true isOrdered Boolean 0 0 association 1 public 0 BasicActivities 0 1 member * public TypedElement composite ownedAttribute * public 0 type 1 public composite realization * public composite substitution * public Of this diagram only this inheritance relationship is modeled. This diagram is not complete. 1 PackageableElement 0 * 0 ElementImport 1 0 context_ 1 subject * public supplierDependency * public visibility VisibilityKind composite ownedOperation * public 0 owningAssociation 1 public effect String 0 0 0 false isReadOnly Boolean Constraints 0 0 0 (1.0, 0.0, 0.0, 1.0, 497.0, 45.0) 195.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 521.0, 173.0) 149.0 111.0 0 0 (1.0, 0.0, 0.0, 1.0, 591.9787234042551, 107.0) 0 1 [(0.0, 0.0), (-1.9787234042551063, 66.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 89.0, 174.0) 121.0 92.0 0 0 (1.0, 0.0, 0.0, 1.0, 210.0, 196.0) 0 1 [(0.0, 0.0), (311.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 210.0, 250.0) 0 1 [(0.0, 0.0), (311.0, 0.9999999999999716)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 879.5555555555555, 177.0) 100.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 879.5555555555555, 234.0) 174.0 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 670.0, 196.99999999999997) 0 1 [(0.0, 0.0), (209.55555555555554, 1.0000000000000284)] 0 0 (1.0, 0.0, 0.0, 1.0, 670.0, 250.99999999999997) 0 1 [(0.0, 0.0), (209.55555555555554, 1.360000000000042)] 0 public visibility VisibilityKind public InstanceSpecification composite postcondition * public composite ownedEnd * public 0 bodyContext 1 public shared Components 0 0 0 language String 1 0 0 class_ 1 public ExtensionEnd 0 enumeration 1 public redefinedClassifier * public Enumeration 0 1 isUnique Boolean 1 redefinedElement * public 0 package 1 public 0 specific 1 public composite operand * public redefinedOperation * public Slot extendedCase 1 public composite ownedClassifier * public ExecutableNode method * 0 clientDependency * public visibility VisibilityKind 0 0 private realizingClassifier 1 public 0 activity 1 composite packageImport * public 0 0 1 1 extension * public InitialNode 0 0 classifier 1 composite 2 memberEnd * public Control nodes 0 0 0 (1.0, 0.0, 0.0, 1.0, 307.0, 60.0) 129.0 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 22.0, 200.0) 115.0 54.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 144.0, 200.0) 107.0 54.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 504.0, 200.0) 121.0 54.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 656.0, 239.0) 138.0 54.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 43.0, 355.0) 165.0 54.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 672.0, 372.0) 139.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 168.0, 254.0) 0 0 [(0.0, 0.0), (-43.87022900763361, 100.0), (-41.87022900763361, 101.0)] 0 (1.0, 0.0, 0.0, 1.0, 728.0, 293.0) 0 1 [(0.0, 0.0), (6.612612612612907, 79.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (-299.99999999999994, 41.0), (-299.99999999999994, 90.0)] 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (-175.49999999999994, 41.0), (-175.49999999999994, 90.0)] 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (181.00000000000017, 41.0), (181.00000000000017, 90.0)] 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (334.00000000000006, 41.0), (334.00000000000006, 129.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 232.0, 355.0) 138.0 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 223.0, 254.0) 0 0 [(0.0, 0.0), (58.0, 101.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 366.0, 270.0) 472.0 66.0 0 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (160.54198473282446, 41.0), (160.54198473282446, 160.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 264.0, 200.0) 101.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 372.99999999999994, 110.0) 1 0 [(0.0, 0.0), (0.0, 41.0), (-52.99999999999994, 41.0), (-52.99999999999994, 90.0)] Stereotype Diagram 1 0 superGroup 1 composite 0 ownedEnd 1 public 0 class_ 1 public DataType Dependencies 0 activity 1 value Boolean 0 DataTypes 0 0 0 (1.0, 0.0, 0.0, 1.0, 96.0, 47.0) 156.0 58.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 99.0, 144.0) 141.0 139.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 23.0, 396.0) 135.0 55.0 0 0 (1.0, 0.0, 0.0, 1.0, 123.0, 283.0) 0 1 [(0.0, 0.0), (0.0, 113.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 197.0, 396.0) 129.0 55.0 0 0 (1.0, 0.0, 0.0, 1.0, 215.00000000000003, 283.0) 0 1 [(0.0, 0.0), (0.0, 113.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 613.0, 137.0) 183.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 240.0, 162.00000000000003) 0 1 [(0.0, 0.0), (373.0, 0.559999999999917)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 617.0, 229.0) 171.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 240.0, 257.0) 0 1 [(0.0, 0.0), (377.0, 0.15999999999996817)] 0 0 (1.0, 0.0, 0.0, 1.0, 168.179104477612, 105.0) 0 1 [(0.0, 0.0), (-0.1791044776119861, 39.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 583.0, 321.0) 198.0 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 594.0, 423.0) 178.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 679.9795918367347, 371.0) 0 1 [(0.0, 0.0), (0.020408163265301482, 52.0)] 0 (1.0, 0.0, 0.0, 1.0, 326.0, 417.99999999999983) 1 1 [(0.0, 0.0), (167.0, 0.0), (167.0, 28.000000000000284), (268.0, 28.000000000000284)] 0 ActivityEdge true isLeaf Boolean value UnlimitedNatural AggregationKind 0 0 composite ownedAttribute * public 1 0 default String 1 0 composite 0 composite include * public raisedException * public composite packageExtension * public 0 operation 1 public 1 metaclass 1 public Artifact in 0 subject 1 public composite formalParameter * public composite 0 bodyCondition 1 public 0 1 inheritedMember * public LiteralUnlimitedNatural Operations 0 0 0 (1.0, 0.0, 0.0, 1.0, 111.0, 62.0) 172.0 58.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 90.0, 195.0) 472.0 419.0 0 0 (1.0, 0.0, 0.0, 1.0, 192.25153374233116, 120.0) 0 1 [(0.0, 0.0), (102.8517128426464, 75.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 729.0, 212.0) 223.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 729.0, 245.0) 1 1 [(0.0, 0.0), (-121.0, 0.0), (-121.0, -20.0), (-167.0, -20.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 729.4259259259259, 276.0) 112.0 177.0 0 0 (1.0, 0.0, 0.0, 1.0, 562.0, 315.0) 0 1 [(0.0, 0.0), (167.42592592592587, -2.999999999999943)] 0 0 (1.0, 0.0, 0.0, 1.0, 562.0, 370.0) 0 1 [(0.0, 0.0), (167.42592592592587, -3.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 328.0, 614.0) 0 1 [(0.0, 0.0), (401.42592592592587, -189.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 328.0, 614.0) 0 1 [(0.0, 0.0), (403.9681697612732, -125.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 328.0, 614.0) 1 1 [(0.0, 0.0), (160.0, 0.0), (160.0, 0.0), (0.0, 0.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 731.9681697612732, 474.0) 110.031830239 75.2181818182 0 0 (1.0, 0.0, 0.0, 1.0, 328.0, 614.0) 0 1 [(0.0, 0.0), (403.9681697612732, -85.00000000000011)] 0 false isDerivedUnion Boolean 0 alias String 1 instance 1 public composite extend * public Flows 0 0 0 (1.0, 0.0, 0.0, 1.0, 66.0, 121.0) 132.0 94.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 50.0, 265.0) 174.0 67.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 371.0, 121.0) 127.0 189.0 0 0 (1.0, 0.0, 0.0, 1.0, 224.0, 294.48) 0 1 [(0.0, 0.0), (147.0, -1.4800000000000182)] 0 0 (1.0, 0.0, 0.0, 1.0, 198.0, 142.0) 0 1 [(0.0, 0.0), (173.0, -1.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 198.0, 195.0) 0 1 [(0.0, 0.0), (173.0, 0.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 346.0, 24.0) 186.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 431.0, 96.0) 0 1 [(0.0, 0.0), (-1.0, 25.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 772.0, 139.0) 139.0 88.0 0 0 (1.0, 0.0, 0.0, 1.0, 498.0, 171.0) 0 1 [(0.0, 0.0), (274.0, -0.32000000000005)] 0 0 (1.0, 0.0, 0.0, 1.0, 498.0, 249.0) 1 1 [(0.0, 0.0), (124.0, 0.0), (124.0, 42.0), (0.0, 42.0)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 433.0, 420.0) 118.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 295.0, 421.0) 125.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 388.2621359223301, 310.0) 0 1 [(0.0, 0.0), (-0.762135922330117, 111.0)] 0 (1.0, 0.0, 0.0, 1.0, 456.0000000000001, 310.0) 0 1 [(0.0, 0.0), (-1.7053025658242404e-13, 110.0)] MultiplicityElement 1 0 upper UnlimitedNatural 1 Groups 0 0 0 (1.0, 0.0, 0.0, 1.0, 313.0, 51.0) 100.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 294.0, 186.0) 137.0 144.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 707.0, 172.0) 139.0 88.0 0 0 (1.0, 0.0, 0.0, 1.0, 367.0, 113.0) 0 1 [(0.0, 0.0), (-0.10091743119249941, 73.0)] 0 (1.0, 0.0, 0.0, 1.0, 294.0, 209.14285714285714) 1 1 [(0.0, 0.0), (-175.0, 0.0), (-175.0, 46.857142857142975), (0.0, 46.857142857142975)] 0 0 (1.0, 0.0, 0.0, 1.0, 431.0, 213.0) 0 1 [(0.0, 0.0), (276.0, -0.5199999999999534)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 566.0, 300.0) 129.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 20.0, 299.0) 127.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 294.0, 321.0) 0 1 [(0.0, 0.0), (-147.0, 5.684341886080802e-14)] 0 0 (1.0, 0.0, 0.0, 1.0, 431.0, 322.0) 0 1 [(0.0, 0.0), (135.0, 0.0)] 0 0 Root 0 0 0 (1.0, 0.0, 0.0, 1.0, 301.0, 158.0) 122.0 123.0 0 0 (1.0, 0.0, 0.0, 1.0, 423.0, 186.5535714285714) 1 1 [(0.0, 0.0), (133.0, 0.0), (133.0, -72.55357142857139), (-32.26446280991735, -72.55357142857139), (-32.26446280991735, -28.553571428571388)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 200.0, 378.0) 155.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 339.31404958677695, 281.0) 0 1 [(0.0, 0.0), (-47.314049586776946, 97.0)] 0 (1.0, 0.0, 0.0, 1.0, 200.0, 399.3125) 1 1 [(0.0, 0.0), (-64.0, 0.0), (-64.0, -123.80357142857144), (101.0, -123.80357142857144)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 392.0, 377.0) 471.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 393.7603305785125, 281.0) 0 1 [(0.0, 0.0), (209.135191809547, 96.0)] 0 (1.0, 0.0, 0.0, 1.0, 423.0, 249.0) 1 1 [(0.0, 0.0), (270.0, 0.0), (270.0, 157.953125), (440.0, 157.953125)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 166.0, 508.0) 219.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 277.0, 428.0) 0 1 [(0.0, 0.0), (-5.684341886080802e-14, 80.0)] 0 (1.0, 0.0, 0.0, 1.0, 166.0, 523.09375) 1 1 [(0.0, 0.0), (-101.0, 0.0), (-101.0, -301.7301136363636), (135.0, -301.7301136363636)] 0 0 (1.0, 0.0, 0.0, 1.0, 166.0, 540.3437499999999) 1 1 [(0.0, 0.0), (-141.0, 0.0), (-141.0, -377.9508928571428), (135.0, -377.9508928571428)] 0 0 importingNamespace 1 public 0 0 ActivityParameterNode 0 datatype 1 public PrimitiveType composite 0 value 1 InstanceValue 0 package 1 public DecisionNode mergingPackage 1 public Presentations are what you see in a diagram. 0 Profile 1 0 lower Integer 1 0 composite 0 constraint 1 public 0 Activity false isDerived Boolean redefinedInterface * public 1 isComposite Boolean 1 DirectedRelationship 1 Classifier Node 0 0 0 (1.0, 0.0, 0.0, 1.0, 217.0, 57.0) 100.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 218.0, 210.0) 100.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 260.99999999999994, 119.0) 0 1 [(0.0, 0.0), (5.684341886080802e-14, 91.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 121.0, 377.0) 100.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 249.00000000000003, 269.0) 0 1 [(0.0, 0.0), (-77.00000000000003, 108.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 254.0, 378.0) 206.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 279.0, 269.0) 0 1 [(0.0, 0.0), (72.0, 109.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 565.155737704918, 111.03846153846155) 157.0 72.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 550.0, 242.0) 186.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 640.5625534664136, 183.03846153846155) 0 1 [(0.0, 0.0), (0.437446533586467, 58.96153846153845)] (1.0, 0.0, 0.0, 1.0, 551.0, 359.0) 326.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 644.0, 359.0) 0 1 [(0.0, 0.0), (-1.0, -58.0)] 0 (1.0, 0.0, 0.0, 1.0, 236.0, 210.0) 0 0 [(0.0, 0.0), (0.0, -57.0), (-127.0, -55.0), (-127.0, 35.0), (-18.0, 34.0)] 0 composite guard 1 1 feature * typeValue is added for Gaphor target 1 isReentrant Boolean inGroup * 0 datatype 1 public composite metaclassReference * public 0 implementatingClassifier 1 public ValueSpecification importedElement 1 public 0 LiteralBoolean composite 0 appliedProfile * public 1 supplier * public outgoing * Interfaces 0 0 0 (1.0, 0.0, 0.0, 1.0, 432.0, 48.0) 156.0 72.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 458.0, 173.0) 102.0 143.0 0 0 (1.0, 0.0, 0.0, 1.0, 507.2142857142858, 120.0) 0 1 [(0.0, 0.0), (0.27571428571400247, 53.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 131.0, 169.0) 135.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 133.0, 232.0) 100.0 88.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 434.0, 429.0) 153.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 178.0, 421.0) 135.0 62.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 402.0, 586.0) 194.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 266.0, 193.0) 0 1 [(0.0, 0.0), (192.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 233.0, 268.9599999999998) 0 1 [(0.0, 0.0), (225.0, 0.040000000000190994)] 0 0 (1.0, 0.0, 0.0, 1.0, 560.0, 187.0) 0 1 [(0.0, 0.0), (159.0, 0.0), (159.0, 43.0), (0.0, 42.999999999999915)] 0 0 (1.0, 0.0, 0.0, 1.0, 560.0, 259.00000000000006) 0 1 [(0.0, 0.0), (161.0, 0.0), (161.0, 35.99999999999994), (0.0, 35.999999999999886)] 0 0 (1.0, 0.0, 0.0, 1.0, 505.99999999999983, 316.0) 0 1 [(0.0, 0.0), (1.7053025658242404e-13, 113.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 313.0, 454.2142857142857) 0 1 [(0.0, 0.0), (121.0, 0.7857142857142776)] 0 (1.0, 0.0, 0.0, 1.0, 496.0, 586.0) 0 1 [(0.0, 0.0), (0.0, -101.0)] 0 (1.0, 0.0, 0.0, 1.0, 698.0, 507.0) 161.0 44.0 0 (1.0, 0.0, 0.0, 1.0, 643.0, 583.0) 122.0 40.0 0 (1.0, 0.0, 0.0, 1.0, 655.0, 340.0) 118.0 42.0 0 Implementation Class contract 1 public out Packages 0 0 0 (1.0, 0.0, 0.0, 1.0, 74.0, 67.0) 121.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 228.0, 68.0) 197.0 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 133.0, 194.0) 154.0 354.0 0 0 (1.0, 0.0, 0.0, 1.0, 138.0, 117.0) 0 1 [(0.0, 0.0), (20.00000000000003, 77.0)] 0 (1.0, 0.0, 0.0, 1.0, 275.0, 118.0) 0 1 [(0.0, 0.0), (-20.0, 76.0)] 0 (1.0, 0.0, 0.0, 1.0, 287.0, 194.0) 1 1 [(0.0, 0.0), (392.0, 0.0), (392.0, -104.0), (138.0, -104.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 287.0, 277.0) 0 1 [(0.0, 0.0), (337.3960674157304, -2.400486092357596)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 580.3251028806585, 318.0) 198.0 50.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 606.0, 396.0) 146.0 83.0 0 0 (1.0, 0.0, 0.0, 1.0, 674.961373484007, 368.0) 0 1 [(0.0, 0.0), (1.9822884878240075, 28.0)] 0 (1.0, 0.0, 0.0, 1.0, 287.0, 415.0) 0 1 [(0.0, 0.0), (319.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 606.0, 461.21428571428623) 0 1 [(0.0, 0.0), (-319.0, -0.21428571428538135)] 0 0 (1.0, 0.0, 0.0, 1.0, 287.0, 513.0) 1 1 [(0.0, 0.0), (160.0, 0.0), (160.0, 78.0), (-60.999999999999915, 78.0), (-60.999999999999915, 35.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 624.3960674157304, 250.91304347826087) 100.0 50.0 0 1 BehavioredClassifier Node Parameter Presentation 1 required * public 0 1 0 opposite 1 public Operation ControlFlow Property 1 Namespace false isDerived Boolean Substitution value is added specially for Gaphor 1 0 upper UnlimitedNatural 1 1 general * public return redefinedElement * composite 1 subgroup * composite 0 typeValue 1 public Instances 0 0 0 (1.0, 0.0, 0.0, 1.0, 65.0, 86.0) 195.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 61.0, 218.0) 198.0 204.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 504.0, 256.0) 100.0 100.0 0 0 (1.0, 0.0, 0.0, 1.0, 259.0, 247.99999999999991) 1 1 [(0.0, 0.0), (163.0, 0.0), (163.0, 25.000000000000085), (245.0, 25.000000000000085)] 0 0 (1.0, 0.0, 0.0, 1.0, 154.87234042553183, 148.0) 0 1 [(0.0, 0.0), (-1.9437689969604435, 70.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 501.0, 374.0) 174.0 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 259.0, 384.0) 1 1 [(0.0, 0.0), (119.0, 0.0), (119.0, 10.127272727272782), (242.0, 10.127272727272782)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 501.0127388535032, 432.0) 472.0 81.0 0 0 (1.0, 0.0, 0.0, 1.0, 501.0127388535032, 474.3050847457627) 1 1 [(0.0, 0.0), (-310.70661640452363, 0.0), (-310.70661640452363, -52.30508474576271)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 506.51273885350315, 86.0) 100.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 555.0000000000001, 145.0) 0 1 [(0.0, 0.0), (-1.1368683772161603e-13, 111.0)] 0 (1.0, 0.0, 0.0, 1.0, 604.0, 262.0) 0 1 [(0.0, 0.0), (224.78899082568807, 3.3388235294117408)] 0 0 (1.0, 0.0, 0.0, 1.0, 604.0, 331.0) 1 1 [(0.0, 0.0), (146.0, 0.0), (146.0, 67.15272727272725), (71.0, 67.15272727272725)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 828.7889908256881, 245.30588235294118) 168.0 60.0 0 (1.0, 0.0, 0.0, 1.0, 675.9564220183486, 121.0) 170.0 112.0 0 0 (1.0, 0.0, 0.0, 1.0, 643.9614775712357, 262.5935536304283) 0 0 [(0.0, 0.0), (66.99494444711291, -29.59355363042829)] Behavior false isStatic Boolean LiteralString OutputPin subsets source ValuePin Dependencies 0 0 0 (1.0, 0.0, 0.0, 1.0, 47.0, 139.0) 166.0 98.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 476.0, 139.0) 168.0 67.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 363.0, 17.0) 197.0 59.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 581.0, 20.0) 194.0 55.0 0 0 (1.0, 0.0, 0.0, 1.0, 213.0, 154.0) 0 1 [(0.0, 0.0), (263.0, -1.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 213.0, 196.0) 0 1 [(0.0, 0.0), (263.0, -1.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 467.4696969696969, 76.0) 0 1 [(0.0, 0.0), (34.530303030303116, 63.0)] 0 (1.0, 0.0, 0.0, 1.0, 661.5846153846154, 75.0) 0 1 [(0.0, 0.0), (-60.584615384615404, 64.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 371.0, 341.0) 121.0 54.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 50.0, 328.0) 135.0 76.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 511.0, 341.0) 100.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 652.0, 341.0) 117.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 372.0, 451.0) 115.0 54.0 0 0 (1.0, 0.0, 0.0, 1.0, 421.0, 395.0) 0 1 [(0.0, 0.0), (-5.684341886080802e-14, 56.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 358.0, 568.0) 125.0 111.0 0 0 (1.0, 0.0, 0.0, 1.0, 420.99999999999994, 505.0) 0 1 [(0.0, 0.0), (-0.49999999999994316, 63.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 810.4983818770227, 561.5597014925373) 156.0 116.440298507 0 0 (1.0, 0.0, 0.0, 1.0, 483.0, 585.8392857142858) 0 1 [(0.0, 0.0), (327.49838187702267, 0.3724280357951102)] 0 0 (1.0, 0.0, 0.0, 1.0, 483.0, 667.0) 0 1 [(0.0, 0.0), (327.49838187702267, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 185.0, 369.04) 0 1 [(0.0, 0.0), (186.0, -2.0399999999999068)] 0 0 (1.0, 0.0, 0.0, 1.0, 561.0, 206.0) 0 1 [(0.0, 0.0), (0.0, 135.0)] 0 (1.0, 0.0, 0.0, 1.0, 507.0, 206.0) 0 1 [(0.0, 0.0), (-86.0, 135.0)] 0 (1.0, 0.0, 0.0, 1.0, 644.0, 205.99999999999997) 0 1 [(0.0, 0.0), (58.000000000000114, 135.00000000000003)] 0 useCase * public 0 BehavioralFeature composite edge * ParameterDirectionKind Package 1 1 featuringClassifier * public 1 redefinitionContext * public redefinedBehavior * 0 Action Presentations 1 0 0 (1.0, 0.0, 0.0, 1.0, 231.0, 178.0) 127.0 66.0 0 (1.0, 0.0, 0.0, 1.0, 231.0, 266.0) 289.0 44.0 0 0 (1.0, 0.0, 0.0, 1.0, 290.69000000000005, 244.0) 0 1 [(0.0, 0.0), (0.10562043795613363, 22.0)] 0 (1.0, 0.0, 0.0, 1.0, 382.0, 134.0) 0 1 [(0.0, 0.0), (-45.31468531468528, 44.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 277.0, 58.0) 195.0 76.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 135.0, 56.0) 121.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 190.0, 120.0) 0 1 [(0.0, 0.0), (41.0, 58.00000000000003)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 665.0, 171.0) 100.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 358.0, 199.0) 0 1 [(0.0, 0.0), (307.0, -3.680000000000007)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 659.88, 451.0) 100.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 205.0, 454.0) 131.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 336.0, 476.0000000000001) 0 1 [(0.0, 0.0), (323.88, -0.19999999999998863)] 0 (1.0, 0.0, 0.0, 1.0, 229.0, 552.0) 276.0 44.0 0 0 (1.0, 0.0, 0.0, 1.0, 243.63265306122452, 552.0) 0 1 [(0.0, 0.0), (10.492346938775427, -42.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 218.88, 331.0) 100.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 263.0, 393.0) 0 0 [(0.0, 0.0), (0.0, 61.0)] Namespaces 0 0 0 (1.0, 0.0, 0.0, 1.0, 387.66666666666663, 72.66666666666666) 122.0 64.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 340.66666666666663, 200.66666666666666) 471.0 111.0 0 0 (1.0, 0.0, 0.0, 1.0, 448.66666666666663, 136.66666666666666) 0 1 [(0.0, 0.0), (121.93528957528952, 64.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 648.6666666666665, 43.66666666666666) 472.0 131.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 180.66666666666663, 419.66666666666663) 471.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 340.66666666666663, 311.66666666666663) 0 1 [(0.0, 0.0), (69.01834862385323, 108.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 566.6666666666665, 421.3333333333333) 147.0 182.666666667 0 0 (1.0, 0.0, 0.0, 1.0, 811.6666666666666, 311.6666666666666) 0 1 [(0.0, 0.0), (-170.00000000000023, 109.66666666666674)] 0 (1.0, 0.0, 0.0, 1.0, 651.6666666666666, 445.0735294117648) 0 1 [(0.0, 0.0), (-85.00000000000011, -1.0895294118056995)] 0 0 (1.0, 0.0, 0.0, 1.0, 811.6666666666666, 260.23691460055085) 1 1 [(0.0, 0.0), (-52.999999999999886, 0.0), (-52.999999999999886, 184.55819243304603), (-98.00000000000011, 184.55819243304603)] 0 0 (1.0, 0.0, 0.0, 1.0, 811.6666666666666, 210.30303030303028) 1 1 [(0.0, 0.0), (20.999999999999886, 0.0), (20.999999999999886, 284.7673060882738), (-98.00000000000011, 284.7673060882738)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 520.8744588744588, 955.7841530054643) 219.0 64.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 374.33333333333326, 744.6666666666666) 472.0 91.0 0 0 (1.0, 0.0, 0.0, 1.0, 600.0000000000001, 955.7841530054643) 0 1 [(0.0, 0.0), (75.74157303370771, -120.11748633879768)] 0 (1.0, 0.0, 0.0, 1.0, 374.33333333333326, 764.8888888888885) 1 1 [(0.0, 0.0), (-193.66666666666663, 0.0), (-193.66666666666663, -274.2222222222219)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 717.3333333333334, 756.3333333333333) 470.0 71.0 0 0 (1.0, 0.0, 0.0, 1.0, 739.8744588744588, 955.7841530054644) 0 1 [(0.0, 0.0), (147.25403274565144, -128.45081967213116)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 833.6666666666664, 935.3333333333333) 124.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 1187.3333333333335, 827.3333333333333) 0 1 [(0.0, 0.0), (-308.00000000000045, 108.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 566.6666666666665, 604.0000000003333) 0 1 [(0.0, 0.0), (-95.66666666666657, 16.99999999966667), (-15.554307116105065, 140.6666666663333)] 0 0 (1.0, 0.0, 0.0, 1.0, 713.6666666666665, 587.5599999996999) 0 1 [(0.0, 0.0), (89.33333333333348, 31.440000000300074), (304.7467411545622, 168.77333333363333)] 0 VisibilityKind AssociationClasses 0 body String 1 Comment package ExecutionEnvironment composite nestedPackage * public redefinedElement * Extend 1 0 qualifiedName String 1 public 1 attribute * public ActivityGroup 0 0 interface_ 1 0 specification 1 0 0 incoming * 0 none definingFeature 1 0 false isReadOnly Boolean 1 0 type 1 public Constraint protected 1 superClass * public UseCases 0 0 0 (1.0, 0.0, 0.0, 1.0, 367.0, 75.0) 194.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 754.0, 75.0) 165.0 72.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 50.0, 84.0) 186.0 72.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 439.0, 551.0) 198.0 64.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 82.0, 517.0) 112.0 62.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 812.0, 221.0) 100.0 61.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 68.0, 231.0) 147.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 381.0, 218.0) 287.0 99.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 379.0, 406.0) 100.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 558.0, 403.0) 100.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 194.0, 546.7600000000001) 1 1 [(0.0, 0.0), (201.0, 0.0), (201.0, -84.7600000000001)] 0 0 (1.0, 0.0, 0.0, 1.0, 490.81208053691296, 551.0) 0 1 [(0.0, 0.0), (-58.81208053691296, -89.0)] 0 (1.0, 0.0, 0.0, 1.0, 536.0000000000002, 551.0) 0 1 [(0.0, 0.0), (71.99999999999977, -92.0)] 0 (1.0, 0.0, 0.0, 1.0, 379.0, 453.0) 1 1 [(0.0, 0.0), (-240.03448275862073, 0.0), (-240.03448275862073, -166.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 215.0, 251.0) 0 1 [(0.0, 0.0), (166.0, 0.5892857142856087)] 0 0 (1.0, 0.0, 0.0, 1.0, 381.0, 298.2025316455696) 1 1 [(0.0, 0.0), (-117.0, 0.0), (-117.0, 121.7974683544304), (-2.0, 121.7974683544304)] 0 0 (1.0, 0.0, 0.0, 1.0, 410.99999999999994, 406.0) 0 1 [(0.0, 0.0), (5.684341886080802e-14, -89.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 668.0, 317.0) 0 1 [(0.0, 0.0), (-45.0, 86.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 658.0, 419.00000000000017) 1 1 [(0.0, 0.0), (132.0, 0.0), (132.0, -121.00000000000017), (10.0, -121.00000000000017)] 0 0 (1.0, 0.0, 0.0, 1.0, 461.0, 125.0) 0 1 [(0.0, 0.0), (1.0, 93.0)] 0 (1.0, 0.0, 0.0, 1.0, 754.0, 102.36) 1 1 [(0.0, 0.0), (-162.9999999999999, 0.0), (-162.9999999999999, 115.64)] 0 0 (1.0, 0.0, 0.0, 1.0, 858.0, 147.0) 0 1 [(0.0, 0.0), (-0.10000000000002274, 74.0)] 0 (1.0, 0.0, 0.0, 1.0, 668.0, 248.00000000000003) 1 1 [(0.0, 0.0), (116.0, 0.0), (116.0, -101.00000000000003)] 0 0 (1.0, 0.0, 0.0, 1.0, 143.64137931034483, 156.0) 0 1 [(0.0, 0.0), (-0.8741379310344826, 75.0)] 0 package 1 public Nodes 0 0 0 (1.0, 0.0, 0.0, 1.0, 52.0, 166.0) 139.0 88.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 443.0, 54.0) 186.0 72.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 466.0, 163.0) 129.0 123.0 0 0 (1.0, 0.0, 0.0, 1.0, 531.5714285714286, 126.0) 0 1 [(0.0, 0.0), (0.4285714285714448, 37.0)] 0 (1.0, 0.0, 0.0, 1.0, 466.0, 239.0) 1 1 [(0.0, 0.0), (-160.0, 0.0), (-160.0, 34.0), (0.0, 34.0)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 625.0, 367.0) 129.0 56.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 389.0, 444.0) 472.0 85.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 379.0, 347.0) 140.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 455.936936936937, 411.0) 0 1 [(0.0, 0.0), (57.07874933757279, 33.0)] 0 (1.0, 0.0, 0.0, 1.0, 555.0, 286.0) 0 1 [(0.0, 0.0), (139.41176470588243, 158.0)] 0 (1.0, 0.0, 0.0, 1.0, 575.1538461538462, 286.0) 0 1 [(0.0, 0.0), (67.90615384615376, 81.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 165.0, 434.0) 157.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 475.9230769230769, 286.0) 0 1 [(0.0, 0.0), (-0.9230769230769056, 46.0), (-254.9230769230769, 44.0), (-254.92307692307693, 148.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 178.0, 567.0) 125.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 226.0, 490.0) 0 1 [(0.0, 0.0), (12.0, 77.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 150.0, 684.3943661971831) 156.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 239.25, 627.0) 0 1 [(0.0, 0.0), (-12.779411764705856, 57.3943661971831)] 0 0 (1.0, 0.0, 0.0, 1.0, 178.0, 593.4) 1 1 [(0.0, 0.0), (-71.0, 0.0), (-71.0, -339.4)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 366.0, 575.0) 125.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 458.6393442622951, 529.0) 0 1 [(0.0, 0.0), (-17.639344262295083, 46.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 485.0, 575.0) 211.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 753.643137254902, 529.0) 0 1 [(0.0, 0.0), (-166.64313725490194, 46.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 479.0, 690.0) 224.0 84.0 0 0 (1.0, 0.0, 0.0, 1.0, 589.1639344262295, 690.0) 0 1 [(0.0, 0.0), (0.06498123642154496, -59.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 191.0, 202.0) 0 1 [(0.0, 0.0), (275.0, 1.0)] 0 isRequired Boolean 1 0 namespace 1 public 1 Type Presentations none 1 1 source * public 1 1 relatedElement * public 0 type 1 public composite node * Classifiers 0 0 0 (1.0, 0.0, 0.0, 1.0, 275.0, 56.0) 166.0 84.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 256.0, 188.0) 472.0 92.0 0 0 (1.0, 0.0, 0.0, 1.0, 353.9931034482755, 140.0) 0 1 [(0.0, 0.0), (128.97957414735316, 48.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 433.0, 390.0) 472.0 237.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 467.9261744966443, 188.0) 121.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 659.0, 245.0) 0 1 [(0.0, 0.0), (98.96870486278306, 145.0)] 0 (1.0, 0.0, 0.0, 1.0, 524.8828658291744, 247.0) 0 1 [(0.0, 0.0), (112.64289247607371, 143.0)] 0 (1.0, 0.0, 0.0, 1.0, 485.55191256830597, 280.0) 0 1 [(0.0, 0.0), (24.71337390256508, 110.0)] 0 (1.0, 0.0, 0.0, 1.0, 433.0, 458.0) 1 1 [(0.0, 0.0), (-112.51912568306017, 0.0), (-112.51912568306017, -178.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 256.0, 259.5555555555556) 1 1 [(0.0, 0.0), (-188.0, 0.0), (-188.0, -53.38271604938282), (0.0, -53.38271604938282)] 0 0 (1.0, 0.0, 0.0, 1.0, 905.0, 518.0) 1 1 [(0.0, 0.0), (-102.0, 0.0), (-102.0, 57.0), (0.0, 57.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 905.0, 608.0) 1 1 [(0.0, 0.0), (-231.0, 0.0), (-231.0, 78.0), (-74.31102551757317, 78.0), (-74.31102551757317, 19.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 433.0, 532.7272727272726) 1 1 [(0.0, 0.0), (-382.0909090909091, 0.0), (-382.0909090909091, -447.09818181818173), (-158.0, -447.09818181818173)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 962.7272727272729, 396.3636363636363) 468.0 100.0 0 0 (1.0, 0.0, 0.0, 1.0, 905.0, 412.72727272727275) 0 1 [(0.0, 0.0), (57.72727272727286, -5.684341886080802e-14)] 0 0 (1.0, 0.0, 0.0, 1.0, 905.0, 457.27272727272725) 1 1 [(0.0, 0.0), (-65.90909090909099, 0.0), (-65.90909090909099, 18.636363636363626), (57.72727272727286, 18.636363636363626)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 955.0559440559439, 197.11384876805434) 198.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 1053.2336182336185, 247.11384876805434) 0 1 [(0.0, 0.0), (133.90923890923773, 149.24978759558198)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 610.1258741258741, 189.0) 100.0 56.0 0 PackageImport 0 composite 1 output * 0 context 1 inGroup * 0 ownerFormalParam 1 Abstraction 0 1 client * public composite slot * 0 composite ownedBehavior * Diagram contains a diacanvas.Canvas instance Stereotypes Stereotypes 0 0 0 (1.0, 0.0, 0.0, 1.0, 74.0, 150.0) 126.0 64.0 0 (1.0, 0.0, 0.0, 1.0, 61.0, 41.0) 232.0 78.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 370.0, 156.0) 100.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 252.0, 288.59322033898303) 0 1 [(0.0, 0.0), (118.0, -110.59322033898303)] 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 71.0, 253.0) 181.0 70.0 0 This diagram contains some additional relations that allow some elements to have a stereotype UseCases_extra 0 0 0 (1.0, 0.0, 0.0, 1.0, 195.0, 77.0) 100.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 470.0, 85.0) 100.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 136.0, 255.0) 223.0 68.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 415.0, 255.0) 222.0 68.0 0 0 (1.0, 0.0, 0.0, 1.0, 241.41818181818184, 255.0) 0 1 [(0.0, 0.0), (0.5818181818181642, -122.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 521.0, 141.0) 0 1 [(0.0, 0.0), (0.9636363636363967, 114.0)] 0 composite ownedAttribute * 0 0 useCase 1 0 0 actor 1 composite ownedAttribute * main (1.0, 0.0, 0.0, 1.0, 34.0, 62.0) 121.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 197.0, 265.0) 210.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 197.0, 160.0) 210.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 439.0, 161.0) 132.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 197.0, 62.0) 160.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 437.0, 62.0) 163.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 34.0, 160.0) 138.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 446.0, 266.0) 155.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 39.0, 372.0) 228.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 448.0, 374.0) 133.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 307.0, 372.0) 100.0 70.0 0 Classes AuxilaryConstructs CommonBehaviors CommonBehaviors (1.0, 0.0, 0.0, 1.0, 161.0, 127.0) 179.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 211.0, 311.0) 190.0 70.0 0 Communications Activities Activities (1.0, 0.0, 0.0, 1.0, 62.0, 64.0) 173.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 93.0, 344.0) 198.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 164.0, 134.0) 0 1 [(0.0, 0.0), (1.0, 81.0)] 1 (1.0, 0.0, 0.0, 1.0, 80.0, 215.0) 224.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 180.0, 285.0) 0 1 [(0.0, 0.0), (-2.0, 59.0)] 1 Classes (1.0, 0.0, 0.0, 1.0, 188.0, 46.0) 212.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 264.0, 192.0) 173.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 210.0, 329.0) 140.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 304.00000000000006, 262.0) 0 1 [(0.0, 0.0), (-12.000000000000057, 67.0)] 1 (1.0, 0.0, 0.0, 1.0, 100.0, 184.0) 120.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 194.5000000000001, 184.0) 0 1 [(0.0, 0.0), (32.999999999999886, -68.0)] 1 0 (1.0, 0.0, 0.0, 1.0, 220.0, 221.62000000000003) 0 1 [(0.0, 0.0), (44.0, -0.12000000000006139)] 1 (1.0, 0.0, 0.0, 1.0, 19.0, 41.0) 157.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 141.99999999999991, 184.0) 0 1 [(0.0, 0.0), (-48.17999999999991, -73.0)] 1 AuxilaryConstructs (1.0, 0.0, 0.0, 1.0, 243.0, 113.0) 170.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 201.0, 228.0) 120.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 130.0, 342.0) 157.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 254.0, 298.0) 0 1 [(0.0, 0.0), (-2.999999999999943, 44.0)] 1 (1.0, 0.0, 0.0, 1.0, 78.0, 95.0) 120.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 198.0, 138.55999999999995) 0 1 [(0.0, 0.0), (45.0, -0.5599999999999454)] 1 Interactions interactions (1.0, 0.0, 0.0, 1.0, 185.0, 193.0) 195.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 202.0, 339.0) 143.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 188.0, 31.0) 179.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 263.99999999999994, 101.0) 0 1 [(0.0, 0.0), (5.684341886080802e-14, 92.0)] 1 (1.0, 0.0, 0.0, 1.0, 15.0, 232.0) 173.0 70.0 0 0 (1.0, 0.0, 0.0, 1.0, 266.0, 263.0) 0 1 [(0.0, 0.0), (0.0, 76.0)] 1 0 (1.0, 0.0, 0.0, 1.0, 115.00000000000003, 302.0) 0 1 [(0.0, 0.0), (86.99999999999997, 73.00000000000006)] 1 BasicInteractions Fragments Interaction 0 0 0 (1.0, 0.0, 0.0, 1.0, -10.0, 164.0) 140.0 72.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 365.0, 35.0) 166.0 98.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 355.0, 167.0) 191.0 59.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 134.0, 355.0) 117.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 50.0, 236.0) 0 1 [(0.0, 0.0), (100.00000000000003, 119.0)] 0 (1.0, 0.0, 0.0, 1.0, 375.0, 226.0) 0 1 [(0.0, 0.0), (-158.0, 129.0)] 0 (1.0, 0.0, 0.0, 1.0, 433.8843537414968, 133.0) 0 1 [(0.0, 0.0), (0.11564625850326138, 34.0)] 0 (1.0, 0.0, 0.0, 1.0, 192.0, 355.0) 1 0 [(0.0, 0.0), (0.0, -161.0), (163.0, -161.0)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 273.0, 355.0) 192.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 408.0, 226.0) 0 1 [(0.0, 0.0), (-60.0, 129.0)] 1 1 0 (1.0, 0.0, 0.0, 1.0, 662.0, 354.0) 142.0 59.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 672.4363636363637, 474.0) 112.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 720.0000000000001, 413.0) 0 1 [(0.0, 0.0), (0.0, 61.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 546.0, 226.0) 0 1 [(0.0, 0.0), (172.0, 128.0)] 1 1 0 (1.0, 0.0, 0.0, 1.0, 477.5657142857143, 356.0) 203.0 59.0 0 0 (1.0, 0.0, 0.0, 1.0, 454.99999999999994, 226.0) 0 1 [(0.0, 0.0), (92.3810436270316, 130.0)] InteractionFragment Interaction 0 enclosingInteraction 1 fragment * 1 ExecutionOccurence StateInvariant composite invariant 1 Lifeline 0 0 0 (1.0, 0.0, 0.0, 1.0, 266.0, 165.0) 117.0 56.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 219.0, 333.0) 482.0 97.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 695.0, 358.0) 191.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 695.0, 381.99999999999994) 0 1 [(0.0, 0.0), (6.0, -1.3220338983051079)] 0 0 (1.0, 0.0, 0.0, 1.0, 322.0000000000001, 221.0) 0 1 [(0.0, 0.0), (78.18978102189772, 112.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, -4.0, 337.0) 166.0 98.0 0 0 (1.0, 0.0, 0.0, 1.0, 162.0, 376.1999999999999) 0 1 [(0.0, 0.0), (57.0, -0.4542372881354595)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 414.0, 512.0) 121.0 76.0 0 0 (1.0, 0.0, 0.0, 1.0, 646.4671532846716, 430.0) 0 1 [(0.0, 0.0), (-185.92869174621, 82.0)] 0 (1.0, 0.0, 0.0, 1.0, 3.0, 465.0) 384.0 44.0 0 0 (1.0, 0.0, 0.0, 1.0, 277.051094890511, 430.0) 0 1 [(0.0, 0.0), (-186.83619517704398, 35.0)] Lifeline coveredBy * covered 1 interaction 1 composite lifeline * composite 0 discriminator 1 Add relationship to connectableElement from InternalStructures. Messages 0 0 0 (1.0, 0.0, 0.0, 1.0, 298.0, 51.0) 180.0 60.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 316.0, 273.0) 472.0 97.0 0 0 (1.0, 0.0, 0.0, 1.0, 372.69387755102076, 111.0) 0 1 [(0.0, 0.0), (61.8259462375255, 162.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, -43.5, 282.0) 174.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 130.5, 316.69642857142867) 0 1 [(0.0, 0.0), (185.5, -2.696428571428669)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 64.0, 413.0) 471.0 125.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 64.0, 554.0) 472.0 161.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 369.0, 456.0) 166.0 98.0 0 0 (1.0, 0.0, 0.0, 1.0, 446.91836734693896, 456.0) 0 1 [(0.0, 0.0), (139.39000269711363, -86.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1157.0, 147.0) 191.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 485.5356041131105, 195.0) 0 1 [(0.0, 0.0), (181.86527694415827, 78.0)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 791.0, 282.0) 129.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 788.0, 294.99999999999994) 0 1 [(0.0, 0.0), (3.0, 3.000000000000057)] 0 0 (1.0, 0.0, 0.0, 1.0, 788.0, 340.0000000000001) 0 1 [(0.0, 0.0), (3.0, 2.9999999999998863)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 1117.0, 290.0) 203.0 56.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 440.55526992287923, 145.0) 117.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 1204.8049029622064, 197.0) 0 1 [(0.0, 0.0), (19.165156918033745, 93.0)] 0 (1.0, 0.0, 0.0, 1.0, 478.0, 97.046511627907) 1 1 [(0.0, 0.0), (359.0, 0.0), (359.0, 184.95348837209286)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 1482.0, 294.0) 162.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 478.0, 55.883720930232556) 1 1 [(0.0, 0.0), (1107.0000000000005, 0.0), (1107.0000000000005, 238.11627906976744)] 0 (1.0, 0.0, 0.0, 1.0, 1348.0, 165.0) 1 1 [(0.0, 0.0), (147.0, 0.0), (147.0, 129.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 1320.0, 299.0) 0 1 [(0.0, 0.0), (162.0, 2.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 1320.0, 340.0) 0 1 [(0.0, 0.0), (162.0, 0.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1107.1148936170212, 456.0) 192.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 1136.4491017964071, 346.0) 0 1 [(0.0, 0.0), (-4.449101796407149, 110.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 1240.9880239520958, 346.0) 0 1 [(0.0, 0.0), (-22.988023952095773, 110.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1113.1148936170212, 595.0) 139.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 1182.5514619163707, 595.0) 0 1 [(0.0, 0.0), (-5.551461916370954, -83.0)] 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 840.0, 461.0) 269.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 872.0, 354.0) 0 0 [(0.0, 0.0), (0.8382352941176805, 107.0)] 0 (1.0, 0.0, 0.0, 1.0, 1117.0, 306.0) 1 1 [(0.0, 0.0), (-93.55882352941171, 0.0), (-93.55882352941171, 155.0)] (1.0, 0.0, 0.0, 1.0, 89.0, 209.0) 190.0 78.0 0 0 (1.0, 0.0, 0.0, 1.0, 195.0, 287.0) 0 0 [(0.0, 0.0), (3.072418708435322, 27.72654651546611)] Message 1 messageKind MessageKind messageSort MessageSort composite 0 argument 1 CompositeStructures CompositeStructures (1.0, 0.0, 0.0, 1.0, 79.0, 79.0) 204.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 361.0, 78.0) 100.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 125.0, 253.0) 158.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 361.0, 253.0) 130.0 70.0 0 InternalStructures MessageKind complete lost found unknown MessageSort synchCall asynchCall asynchSignal 0 signature 1 MessageEnd 0 sendMessage 1 composite 0 sendEvent 1 0 receiveMessage 1 composite 0 receiveEvent 1 1 OccurrenceSpecification interaction 1 composite message * GeneralOrdering composite generalOrdering * before 1 toAfter * after 1 toBefore * finish 1 finishExec * start 1 startExec * behavior * parse return s String render return String StructuredClassifer 0 0 0 (1.0, 0.0, 0.0, 1.0, 94.0, 64.0) 156.0 72.0 0 0 1 0 (1.0, 0.0, 0.0, 1.0, 54.0, 177.0) 189.0 333.0 0 0 (1.0, 0.0, 0.0, 1.0, 174.27184466019418, 136.0) 0 1 [(0.0, 0.0), (-26.271844660194176, 41.0)] 0 1 0 (1.0, 0.0, 0.0, 1.0, 640.0, 190.0) 196.0 57.0 0 0 1 0 (1.0, 0.0, 0.0, 1.0, 691.0, 470.0) 128.0 58.0 0 0 (1.0, 0.0, 0.0, 1.0, 733.0, 135.0) 0 1 [(0.0, 0.0), (1.000000000000341, 55.0)] 0 (1.0, 0.0, 0.0, 1.0, 243.0, 177.0) 0 1 [(0.0, 0.0), (397.0, 54.99999999999994)] 0 0 (1.0, 0.0, 0.0, 1.0, 243.0, 308.0) 0 1 [(0.0, 0.0), (406.0, 2.6635514018690856)] 0 0 (1.0, 0.0, 0.0, 1.0, 243.0, 408.9999999999999) 0 1 [(0.0, 0.0), (406.0, 28.387096774193708)] 0 0 (1.0, 0.0, 0.0, 1.0, 691.0, 493.4035087719297) 0 1 [(0.0, 0.0), (-198.0, 0.5964912280703061), (-198.0, -35.403508771929694), (-448.0, -35.403508771929694)] 0 0 (1.0, 0.0, 0.0, 1.0, 819.0, 519.859649122807) 0 1 [(0.0, 0.0), (142.0, -0.8596491228070136), (142.0, -43.859649122807014), (0.0, -42.73684210526335)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 668.9680000000001, 71.0) 134.0 64.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 649.0, 299.0) 223.0 156.0 0 0 (1.0, 0.0, 0.0, 1.0, 727.0, 247.0) 0 1 [(0.0, 0.0), (17.20041104553104, 52.0)] 1 StructuredClassifier 1 ConnectableElement Connector * 1 role * composite ownedConnector * 0 1 redefinedConnector * * Connectors 1 1 0 (1.0, 0.0, 0.0, 1.0, 760.0, 213.0) 472.0 76.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 330.0, 78.0) 167.0 50.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 743.0, 93.0) 175.0 72.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 731.0, 354.0) 190.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 823.0, 165.0) 0 1 [(0.0, 0.0), (129.14159292035401, 48.0)] 0 (1.0, 0.0, 0.0, 1.0, 964.6725663716816, 289.0) 0 1 [(0.0, 0.0), (-137.67256637168134, 65.0)] 0 0 1 0 (1.0, 0.0, 0.0, 1.0, 328.0, 201.0) 149.0 74.0 0 0 (1.0, 0.0, 0.0, 1.0, 411.6022727272727, 128.0) 0 1 [(0.0, 0.0), (-18.545934699103668, 73.0)] 0 (1.0, 0.0, 0.0, 1.0, 395.1549295774648, 275.0) 0 1 [(0.0, 0.0), (-2.2977867203219375, 58.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 477.0, 227.0) 0 1 [(0.0, 0.0), (283.0, -0.7566718995290387)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1053.0246305418718, 213.0) 161.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 1232.0, 231.48648648648648) 0 1 [(0.0, 0.0), (-178.97536945812817, 6.436590436590478)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 1023.0, 351.0) 472.0 95.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 15.900928792569658, 210.0) 181.0 56.0 0 0 (1.0, 0.0, 0.0, 1.0, 196.90092879256966, 237.00000000000003) 0 1 [(0.0, 0.0), (131.09907120743034, -2.000000000000142)] 0 0 (1.0, 0.0, 0.0, 1.0, 117.0, 266.0) 0 1 [(0.0, 0.0), (0.9908256880734001, 66.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 63.0, 332.0) 111.0 62.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 345.0, 333.0) 100.0 50.0 0 * 0 type 1 1 ConnectorEnd composite 2 end * 1 kind ConnectorKind * contract * ConnectorKind assembly delegation 0 role 1 end * * 1 0 definingEnd 1 0 1 composite ownedAttribute * 0 1 1 part * FlowFinalNode JoinNode ForkNode CompleteActivities IntermediateActivities Object nodes 1 0 0 (1.0, 0.0, 0.0, 1.0, 92.0, 126.0) 472.0 98.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 483.0, 318.0) 472.0 123.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 597.0, 134.0) 165.0 67.0 0 0 (1.0, 0.0, 0.0, 1.0, 564.0, 158.66666666666666) 0 1 [(0.0, 0.0), (33.0, 1.1929824561403564)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 234.0, 310.49769585253455) 171.0 72.0 0 0 (1.0, 0.0, 0.0, 1.0, 485.33333333333337, 224.0) 0 1 [(0.0, 0.0), (-173.33333333333337, 86.49769585253455)] 0 FIFO ordering ObjectOrderingKind ObjectOrderingKind unordered ordered LIFO FIFO false isControlType Boolean 0 1 composite upperBound 1 * 0 selection 1 Control nodes 1 0 0 (1.0, 0.0, 0.0, 1.0, 8.0, 140.0) 472.0 79.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 516.0, 142.0) 165.0 87.0 0 0 (1.0, 0.0, 0.0, 1.0, 480.0, 162.21875) 0 1 [(0.0, 0.0), (36.0, 2.6759868421052317)] 0 true isCombineDuplicate Boolean composite joinSpec 1 State BehaviorStateMachines 0 0 0 (1.0, 0.0, 0.0, 1.0, 897.0, 15.0) 157.0 72.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 909.0, 133.0) 130.0 70.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 1461.0, 56.0) 471.0 104.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 1638.0, 54.0) 472.0 247.0 0 0 (1.0, 0.0, 0.0, 1.0, 969.9999999999999, 87.0) 0 1 [(0.0, 0.0), (5.684341886080801e-13, 46.0)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 885.0, 335.0) 152.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 966.0666666666665, 335.0) 0 1 [(0.0, 0.0), (-0.06666666666649235, -132.0)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1461.0, 255.0) 166.0 98.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 244.0, 217.0) 116.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 360.0, 240.84313725490196) 1 1 [(0.0, 0.0), (535.64, 0.0), (535.64, 94.15686274509804)] 1 0 0 (1.0, 0.0, 0.0, 1.0, 1370.0, 455.0) 472.0 74.0 0 0 (1.0, 0.0, 0.0, 1.0, 1536.4545454545453, 353.0) 0 1 [(0.0, 0.0), (142.9236058059589, 102.0)] 0 (1.0, 0.0, 0.0, 1.0, 1037.0, 370.99999999999994) 1 1 [(0.0, 0.0), (384.5630252100841, 0.0), (384.5630252100841, 84.00000000000006)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 225.0, 455.0) 173.0 68.0 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 182.0, 292.8253968253968) 166.0 98.0 0 0 (1.0, 0.0, 0.0, 1.0, 368.48212461695607, 455.0) 1 0 [(0.0, 0.0), (0.0, -80.0), (516.5178753830439, -80.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 247.00699300699307, 390.8253968253968) 0 1 [(0.0, 0.0), (0.4741101595033683, 64.17460317460319)] 0 (1.0, 0.0, 0.0, 1.0, 398.0, 482.39999999999986) 0 1 [(0.0, 0.0), (972.0, -2.3999999999998636)] 0 0 (1.0, 0.0, 0.0, 1.0, 398.0, 521.0) 0 1 [(0.0, 0.0), (972.0, -2.6000000000001364)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 833.0, 527.8253968253969) 116.0 64.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 214.0, 608.0) 472.0 66.0 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 230.0, 751.0) 225.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 294.0, 523.0) 0 1 [(0.0, 0.0), (147.1278195488723, 85.0)] 0 (1.0, 0.0, 0.0, 1.0, 398.0, 523.0) 0 0 [(0.0, 0.0), (-0.5188968335035611, 228.0)] 0 (1.0, 0.0, 0.0, 1.0, 287.0, 751.0) 0 1 [(0.0, 0.0), (-26.819548872180462, -77.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 380.0, 674.0) 0 1 [(0.0, 0.0), (-18.0, 77.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 909.0, 182.1228070175439) 0 1 [(0.0, 0.0), (-782.0, -0.12280701754389156), (-787.5035750766087, 452.0103937776847), (-695.0, 451.877192982456)] 0 1 1 0 (1.0, 0.0, 0.0, 1.0, 857.0, 634.0) 472.0 198.0 0 0 (1.0, 0.0, 0.0, 1.0, 398.0, 523.0) 1 0 [(0.0, 0.0), (0.0, 111.0), (510.91999999999996, 111.0), (510.91999999999996, 111.0)] 0 (1.0, 0.0, 0.0, 1.0, 920.4811031664965, 591.8253968253969) 0 1 [(0.0, 0.0), (47.38010679791637, 42.174603174603135)] 0 (1.0, 0.0, 0.0, 1.0, 977.9999999999997, 392.0) 0 1 [(0.0, 0.0), (87.28469750889724, 242.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 380.0, 674.0) 0 1 [(0.0, 0.0), (477.0, -19.999999999997044)] 0 0 (1.0, 0.0, 0.0, 1.0, 455.0, 768.9999999999999) 0 1 [(0.0, 0.0), (402.0, 1.0000000000001137)] 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 827.0, 931.0) 104.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 897.3131672597867, 832.0) 0 1 [(0.0, 0.0), (-18.313167259786724, 99.0)] 0 0 0 (1.0, 0.0, 0.0, 1.0, 1387.7, 632.4556962025316) 157.0 197.544303797 0 0 (1.0, 0.0, 0.0, 1.0, 1329.0, 680.0) 0 1 [(0.0, 0.0), (58.700000000000045, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 1329.0, 741.9999999999999) 0 1 [(0.0, 0.0), (58.700000000000045, 1.1368683772161603e-13)] 0 0 (1.0, 0.0, 0.0, 1.0, 1329.0, 809.0000000000003) 0 1 [(0.0, 0.0), (58.700000000000045, -3.410605131648481e-13)] 0 0 (1.0, 0.0, 0.0, 1.0, 1465.9999999999998, 632.4556962025316) 0 1 [(0.0, 0.0), (0.9999999999995453, -103.45569620253161)] 0 0 0 0 (1.0, 0.0, 0.0, 1.0, 1002.7, 998.4510385756677) 106.0 62.0 0 0 (1.0, 0.0, 0.0, 1.0, 1054.9999999999998, 998.4510385756677) 0 1 [(0.0, 0.0), (132.9039145907475, -166.45103857566767)] 0 0 (1.0, 0.0, 0.0, 1.0, 1108.7, 1030.1317507418396) 0 1 [(0.0, 0.0), (748.8270684371807, 2.999461981818513), (748.3, -549.1317507418396), (499.29999999999995, -575.1317507418396)] 0 0 (1.0, 0.0, 0.0, 1.0, 909.0, 139.140350877193) 0 1 [(0.0, 0.0), (-887.0, -0.14035087719298645), (-890.0, 686.859649122807), (-52.0, 684.8596491228071)] 0 StateMachine TransitionKind internal local external PseudostateKind initial deepHistory shallowHistory join fork junction choice entryPoint exitPoint terminate Region composite 1 region * 0 stateMachine 1 Transition container 1 composite transition kind TransitionKind Vertex composite subvertex * 0 container 1 source 1 outgoing * target 1 incoming * Pseudostate ConnectionPointReference kind PseudostateKind 0 1 entry * exit * 0 1 0 stateMachine 1 composite connectionPoint * State composite region * 0 state 1 composite 0 connectionPoint * 0 state 1 composite connection * 0 state 1 1 isComposite Boolean 1 isOrthogonal Boolean 1 isSimple Boolean 1 isSubmachineState Boolean FinalState 0 1 composite 0 entry 1 0 1 composite 0 exit 1 0 1 composite 0 doActivity 1 composite 0 effect 1 0 1 composite 0 statevariant 1 0 owningState 1 composite 0 guard 1 0 1 0 submachine 1 submachineState * StateMachineRedifinitions 0 0 0 (1.0, 0.0, 0.0, 1.0, 130.0, 331.0) 163.0 276.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 584.0, 185.0) 472.0 79.0 0 1 0 0 (1.0, 0.0, 0.0, 1.0, 572.8146551724137, 325.0) 100.0 80.0 0 0 1 0 (1.0, 0.0, 0.0, 1.0, 484.8146551724137, 708.0) 130.0 57.0 0 0 (1.0, 0.0, 0.0, 1.0, 293.0, 331.0) 0 1 [(0.0, 0.0), (267.0, 22.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 614.8146551724137, 765.0) 0 1 [(0.0, 0.0), (-14.814655172413723, 58.0), (-102.31465517241372, 58.00000000000006), (-102.81465517241372, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 649.9217877094975, 264.0) 0 1 [(0.0, 0.0), (-40.921787709497494, 61.0)] 0 1 0 (1.0, 0.0, 0.0, 1.0, 774.1550387596899, 433.0) 176.0 95.0 0 0 (1.0, 0.0, 0.0, 1.0, 659.9999999999999, 264.0) 1 1 [(0.0, 0.0), (187.3676445924117, 0.0), (187.3676445924117, 169.0), (187.3676445924117, 169.0)] 0 1 0 (1.0, 0.0, 0.0, 1.0, 1045.15503875969, 544.0) 146.0 74.0 0 0 (1.0, 0.0, 0.0, 1.0, 763.0, 264.0) 1 1 [(0.0, 0.0), (336.9999999999998, 0.0), (336.9999999999998, 280.0), (336.9999999999998, 280.0)] 0 (1.0, 0.0, 0.0, 1.0, 293.0, 594.0) 0 1 [(0.0, 0.0), (752.1550387596899, -5.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 293.0, 478.0) 0 1 [(0.0, 0.0), (465.0, -1.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 950.1550387596899, 441.01204819277115) 0 1 [(0.0, 0.0), (28.844961240310113, -2.0120481927711467), (28.844961240310113, 68.98795180722885), (0.0, 81.26506024096392)] 0 0 (1.0, 0.0, 0.0, 1.0, 672.8146551724137, 397.0) 0 1 [(0.0, 0.0), (76.18534482758628, -1.0), (78.18534482758628, -64.0), (0.0, -64.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 1191.15503875969, 616.0) 0 1 [(0.0, 0.0), (86.84496124031011, 0.0), (85.84496124031011, -62.0), (0.0, -62.0)] 0 0 1 0 extendedStateMachine 1 1 redefintionContext 1 * 0 redefinedState 1 0 1 extendedRegion 0 1 0 redefinedTransition 1 0 1 createMessage deleteMessage reply Ports Ports 0 0 1 (1.0, 0.0, 0.0, 1.0, 34.0, 42.0) 172.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 32.0, 150.0) 188.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 112.29850746268656, 92.0) 0 0 [(0.0, 0.0), (3.2570480928689847, 58.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 44.0, 239.0) 154.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 49.0, 343.0) 108.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 122.28333333333333, 289.0) 0 0 [(0.0, 0.0), (-16.28333333333333, 54.0)] 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 520.0, 40.0) 103.0 50.0 0 1 0 1 (1.0, 0.0, 0.0, 1.0, 504.0, 118.0) 471.0 85.0 0 0 (1.0, 0.0, 0.0, 1.0, 569.0, 90.0) 0 0 [(0.0, 0.0), (165.7900000000002, 28.0)] 0 (1.0, 0.0, 0.0, 1.0, 220.0, 164.0) 0 0 [(0.0, 0.0), (63.0, -17.0), (284.0, -7.750000000000028)] 0 EncapsulatedClassifer 0 * 0 partWithPort 1 Port isBehavior Boolean isService Boolean 0 1 composite 0 ownedPort * appliedStereotype extended Profiles Profiles diagram (1.0, 0.0, 0.0, 1.0, 76.0, 49.0) 100.0 70.0 0 Gaphor Gaphor diagram 1 1 1 (1.0, 0.0, 0.0, 1.0, 110.0, 44.0) 103.0 74.0 0 1 0 1 (1.0, 0.0, 0.0, 1.0, 98.0, 204.0) 141.0 64.0 0 0 (1.0, 0.0, 0.0, 1.0, 157.0, 118.0) 0 0 [(0.0, 0.0), (11.5, 86.0)] 1 0 1 (1.0, 0.0, 0.0, 1.0, 407.0, 203.0) 469.0 140.0 0 1 1 1 (1.0, 0.0, 0.0, 1.0, 408.0, 55.0) 147.0 74.0 0 0 (1.0, 0.0, 0.0, 1.0, 477.0, 129.0) 0 0 [(0.0, 0.0), (153.33333333333326, 74.0)] Class SimpleAttribute baseClass composite composite nestedNode * 0 1 StructuredClasses StructuredClasses 0 0 1 (1.0, 0.0, 0.0, 1.0, 232.0, 171.0) 188.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 249.0, 279.0) 100.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 323.3521126760564, 221.0) 0 0 [(0.0, 0.0), (-22.35211267605638, 58.0)] Deployments 0 0 1 (1.0, 0.0, 0.0, 1.0, 725.0, 263.0) 102.0 62.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 710.0, 144.0) 149.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 780.6465517241379, 194.0) 0 0 [(0.0, 0.0), (-14.646551724137908, 69.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 711.0, 42.0) 134.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 779.9142857142857, 92.0) 0 0 [(0.0, 0.0), (-1.0582251082252014, 52.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 414.0, 140.0) 111.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 414.0, 38.0) 135.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 490.0194174757282, 99.0) 0 0 [(0.0, 0.0), (-20.019417475728176, 41.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 40.0, 144.0) 159.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 54.0, 255.0) 100.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 119.5, 195.0) 0 0 [(0.0, 0.0), (-17.5, 60.0)] 0 (1.0, 0.0, 0.0, 1.0, 199.0, 158.0) 0 0 [(0.0, 0.0), (215.0, -3.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 525.0, 156.0) 0 0 [(0.0, 0.0), (185.0, 1.0)] 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 36.0, 26.0) 136.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 104.0, 76.0) 0 0 [(0.0, 0.0), (16.125984251968504, 68.0)] 1 DeployedArtifact Deployment 1 DeploymentTarget location 1 composite deployment * * deployedArtifact * Partitions 1 0 1 (1.0, 0.0, 0.0, 1.0, 285.0, 258.0) 469.0 85.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 194.0, 120.0) 134.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 268.88235294117646, 170.0) 0 0 [(0.0, 0.0), (163.76579520697186, 88.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 436.0, 415.0) 134.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 704.783950617284, 343.0) 0 0 [(0.0, 0.0), (-212.29375453885257, 72.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 322.6358024691359, 258.0) 0 0 [(0.0, 0.0), (-70.63580246913591, -38.0), (-129.6358024691359, 1.0), (-85.63580246913591, 59.0), (-37.63580246913591, 76.90476190476193)] 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 500.0, 183.0) 100.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 344.0, 122.0) 134.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 404.5913043478261, 172.0) 0 0 [(0.0, 0.0), (-15.389691444600317, 86.0)] 0 (1.0, 0.0, 0.0, 1.0, 500.0, 207.0) 0 0 [(0.0, 0.0), (-38.0, 51.0)] 0 (1.0, 0.0, 0.0, 1.0, 103.0, 382.0) 231.0 95.0 0 ActivityPartition false isExternal Boolean false isDimension Boolean inPartition * node * 0 represents 1 * superPartition * shared subpartition * ActivitiyPartition.subpartition cannot be composite, so we can rearrange swimlanes 0 1 navigableOwnedEnd * Tagged Property baseClass composite subsets str feature, ownedMember source owner source, owner, client ownedElement redefinedElement ownedElement node ordered bool true ownedMember ownedElement, clientDependency ownedElement members relatedElement namespace ownedMember target true namespace ownedMember redefines str NamedElement.clientDependency Dependency.supplier ownedElement true member context importedPackage target client, source feature, ownedMember Namespace.ownedMember ownedElement ownedElement target ownedElement context, namespace feature, ownedMember packageImport context, namespace ownedMember, ownedRule classifier, namespace target true parameter, ownedMember ownedMember member, ownedElement ownedElement Namespace.ownedMember ownedMember ownedElement attribute, ownedMember true ownedElement true parameter, ownedMember source source, owner ownedMember classifier, namespace, featuringClassifier ownedElement ownedElement attribute, ownedMember ownedElement, clientDependency true feature, ownedMember namespace, featuringClassifier ownedMember feature, ownedMember context, namespace namespace, redefinitionContext, featuringClassifier namespace redefinedElement namespace source, owner ownedElement redefinedElement target ownedMember supplier, target owner ownedElement redefinitionContext true member owner namespace, redefinitionContext owner attribute, ownedMember ownedElement Parameter.ownerFormalParam BehavioralFeature.formalParameter ownedMember member source, owner namespace, redefinitionContext, featuringClassifier true ownedElement namespace source, owner ownedMember redefinedElement ownedElement namespace, featuringClassifier, classifier elementImport Dependency.client target packageImport ownedElement, target RedefinableElement.redefinedElement ownedElement ownedElement ownedElement redefinedElement ownedMember RedefinableElement.redefinedElement feature namespace, featuringClassifier Classifier.general namespace owner relatedElement ownedElement ownedElement namespace ownedMember attribute, ownedMember classifier, namespace classifier, namespace attribute, ownedMember ownedElement ownedMember namespace ownedMember ownedElement ownedElement namespace ownedMember ownedElement union bool true member feature, subsets ownedMember redefinitionContext redefinedElement true ownedElement true true ownedElement classifier true role, subsets attribute, subsets ownedMember ownedElement ownedElement ownedMember namespace namespace ownedMember ownedMember namespace namespace ownedMember ownedMember namespace ownedElement owner ownedMember namespace ownedElement ownedElement ownedElement ownedElement ownedElement ownedElement redefinitionContext RedefinableElement.redefinedElement RedefinableElement.redefinedElement RedefinableElement.redefinedElement redefinitionContext ownedAttribute ownedMember client ownedElement, subsets clientDependency supplier inGroup containedNode superGroup subgroup ownedEnd 1 navigability Boolean Made this relationship an aggregate so Slots are removed as soon as the definingFeature is removed. MessageOccurrenceSpecification Multiplicity was '*'. Changed to accomodate simpleAttribute Actions Actions (1.0, 0.0, 0.0, 1.0, 334.0, 141.0) 147.0 70.0 0 (1.0, 0.0, 0.0, 1.0, 151.0, 148.0) 115.0 70.0 0 CompleteActions Accept event actions 0 0 1 (1.0, 0.0, 0.0, 1.0, 266.0, 143.0) 125.0 73.0 0 1 0 1 (1.0, 0.0, 0.0, 1.0, -4.0, 314.0) 471.0 66.0 0 0 (1.0, 0.0, 0.0, 1.0, 326.04901960784315, 216.0) 0 0 [(0.0, 0.0), (-101.90839460784315, 98.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 359.0, 315.0) 110.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 508.0, 316.0) 153.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 109.0, 437.0) 147.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 376.7600446428571, 380.0) 0 0 [(0.0, 0.0), (-237.34625153940883, 57.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 164.0, 553.0) 125.0 61.0 0 0 (1.0, 0.0, 0.0, 1.0, 54.875, 380.0) 1 0 [(0.0, 0.0), (0.0, 207.0), (109.125, 207.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 629.125, 366.0) 1 0 [(0.0, 0.0), (0.0, 228.0), (-340.125, 228.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 217.98275862068965, 488.0) 0 0 [(0.0, 0.0), (-15.99256254225827, 65.0)] 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 339.0, 503.0) 120.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 585.775, 366.0) 1 0 [(0.0, 0.0), (0.0, 166.0), (-126.77499999999998, 166.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 332.1764705882353, 216.0) 0 0 [(0.0, 0.0), (80.72352941176467, 99.0)] 0 (1.0, 0.0, 0.0, 1.0, 371.3921568627451, 216.0) 0 0 [(0.0, 0.0), (206.73284313725492, 100.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 373.0, 424.54098360655735) 125.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 537.325, 366.0) 1 0 [(0.0, 0.0), (0.0, 87.22950819672127), (-39.325000000000045, 87.22950819672127)] 0 0 (1.0, 0.0, 0.0, 1.0, 359.0, 348.0) 1 1 [(0.0, 0.0), (-62.0, 0.0), (-62.0, 100.31147540983602), (14.0, 100.31147540983602)] 0 0 (1.0, 0.0, 0.0, 1.0, 434.9, 366.0) 0 0 [(0.0, 0.0), (5.501960784313724, 58.54098360655735)] 0 BasicActions Basic actions 1 1 1 (1.0, 0.0, 0.0, 1.0, 138.0, 112.0) 472.0 76.0 0 AcceptEventAction ReplyAction UnmarshallAction AcceptCallAction false isUnmarshall Boolean composite result * composite result * composite returnInformation 1 unmarshallType 1 composite object 1 composite 0 replyValue 1 composite returnInformation 1 Basic invocation actions 0 0 1 (1.0, 0.0, 0.0, 1.0, 208.0, 84.0) 125.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 200.5, 209.0) 145.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 269.25, 134.0) 0 0 [(0.0, 0.0), (2.4891304347826235, 75.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 386.0, 356.0) 153.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 313.3478260869565, 259.0) 0 0 [(0.0, 0.0), (140.0759027266028, 97.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 348.0, 518.0) 125.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 413.228813559322, 407.0) 0 0 [(0.0, 0.0), (7.075108009305438, 111.0)] 0 InvocationAction SendSignalAction composite target input Collaborations Collaboration 0 0 1 (1.0, 0.0, 0.0, 1.0, 288.0, 127.0) 172.0 60.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 394.5, 241.0) 122.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 464.0, 127.0) 177.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 441.61832061068696, 187.0) 0 0 [(0.0, 0.0), (-34.30832061068696, 54.0)] 0 (1.0, 0.0, 0.0, 1.0, 482.4925373134328, 187.0) 0 0 [(0.0, 0.0), (13.877462686567185, 54.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 62.0, 241.0) 174.0 60.0 0 0 (1.0, 0.0, 0.0, 1.0, 236.0, 262.0) 0 0 [(0.0, 0.0), (158.5, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 122.0, 301.0) 0 0 [(0.0, 0.0), (0.0, 72.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 56.0, 373.0) 148.0 53.0 0 Collaboration collaborationRole * * role Events 0 0 1 (1.0, 0.0, 0.0, 1.0, 530.0, 101.0) 147.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 237.0, 247.0) 135.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 396.0, 247.0) 126.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 540.0, 247.0) 128.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 718.0, 247.0) 149.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 218.0, 352.0) 175.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 396.0, 352.0) 148.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 544.0, 351.0) 196.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 735.0, 351.0) 169.0 51.0 0 0 (1.0, 0.0, 0.0, 1.0, 530.0, 121.0) 0 0 [(0.0, 0.0), (-237.0, 0.0), (-221.67924528301887, 126.0)] 0 (1.0, 0.0, 0.0, 1.0, 530.0, 121.0) 0 0 [(0.0, 0.0), (-81.0, 0.0), (-67.22000000000003, 126.0)] 0 (1.0, 0.0, 0.0, 1.0, 609.9473684210526, 151.0) 0 0 [(0.0, 0.0), (-3.3873684210526562, 96.0)] 0 (1.0, 0.0, 0.0, 1.0, 677.0, 122.0) 0 0 [(0.0, 0.0), (103.0, 0.0), (119.9572649572649, 125.0)] 0 (1.0, 0.0, 0.0, 1.0, 600.16, 297.0) 0 0 [(0.0, 0.0), (-13.159999999999968, 27.0), (-310.15999999999997, 27.0), (-288.82666666666665, 55.0)] 0 (1.0, 0.0, 0.0, 1.0, 600.16, 297.0) 0 0 [(0.0, 0.0), (-13.159999999999968, 27.0), (-147.65999999999997, 27.0), (-129.50513274336276, 55.0)] 0 (1.0, 0.0, 0.0, 1.0, 600.16, 297.0) 0 0 [(0.0, 0.0), (0.5768421052631538, 54.0)] 0 (1.0, 0.0, 0.0, 1.0, 600.16, 297.0) 0 0 [(0.0, 0.0), (-13.159999999999968, 27.0), (202.84000000000003, 27.0), (223.24, 54.0)] 0 0 1 (1.0, 0.0, 0.0, 1.0, 235.5, 453.0) 100.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 570.0, 453.0) 100.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 395.0, 453.0) 147.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 743.0, 453.0) 147.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 284.0, 402.0) 0 0 [(0.0, 0.0), (0.0, 51.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 451.0, 403.0) 0 0 [(0.0, 0.0), (16.21052631578948, 50.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 618.0, 402.0) 0 0 [(0.0, 0.0), (0.0, 51.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 800.0, 402.0) 0 0 [(0.0, 0.0), (16.5, 51.0)] 0 Triggers 0 0 1 (1.0, 0.0, 0.0, 1.0, 319.0, 108.0) 134.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 509.0, 108.0) 178.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 4.0, 231.0) 177.0 50.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 324.0, 230.0) 100.0 51.0 0 0 0 1 (1.0, 0.0, 0.0, 1.0, 527.5, 234.0) 100.0 50.0 0 0 (1.0, 0.0, 0.0, 1.0, 599.948905109489, 158.0) 0 0 [(0.0, 0.0), (-20.948905109489033, 76.0)] 0 (1.0, 0.0, 0.0, 1.0, 387.9142857142857, 158.0) 0 0 [(0.0, 0.0), (-14.914285714285711, 72.0)] 0 (1.0, 0.0, 0.0, 1.0, 181.0, 256.0) 0 0 [(0.0, 0.0), (143.0, 0.0)] 0 0 (1.0, 0.0, 0.0, 1.0, 424.0, 257.0) 0 0 [(0.0, 0.0), (103.5, 0.0)] 0 Trigger 1 Event 0 1 composite ownedTrigger * ownedMember * event 1 ExecutionEvent CreationEvent 1 MessageEvent DestructionEvent SendOperationEvent SendSignalEvent ReceiveOperationEvent ReceiveSignalEvent Signal Reception 0 owningSignal 1 composite ownedAttribute * 0 signal 1 * composite ownedReception composite ownedReception namespace, classifier, featuringClassifier attribute, ownedMember true ownedMember, feature feature, ownedMember * operation 1 * signal 1 * operation 1 * signal 1 PK!̧.##gaphor/UML/uml2.overridecomment This is a file with custom definitions for Gaphor's data model. Parts are separated by '%%' (no training spaces) on a line. Comment parts start with 'comment' on the line below the percentage symbols, 'override' is used to define a overridden variable. Overrides may in their turn derive from other properties, in that case the 'derives' keyword may be used. It's only useful to declare the associations (and other derived properties) an overridden value depends on, since attributes have been written anyway. Note that no smart things wrt inheritance is done. %% override Element from gaphor.UML.element import Element %% override Diagram from gaphor.UML.diagram import Diagram %% override Transition class Transition(RedefinableElement, NamedElement): pass %% override MultiplicityElement.lower derives MultiplicityElement.lowerValue MultiplicityElement.lower = derived('lower', object, 0, 1, MultiplicityElement.lowerValue) MultiplicityElement.lower.filter = lambda obj: [ obj.lowerValue ] #MultiplicityElement.lower = MultiplicityElement.lowerValue %% override MultiplicityElement.upper derives MultiplicityElement.upperValue MultiplicityElement.upper = derived('upper', object, 0, 1, MultiplicityElement.upperValue) MultiplicityElement.upper.filter = lambda obj: [ obj.upperValue ] #MultiplicityElement.upper = MultiplicityElement.upperValue %% override NamedElement.qualifiedName def namedelement_qualifiedname(self): """ Returns the qualified name of the element as a tuple """ if self.namespace: return self.namespace.qualifiedName + (self.name,) else: return (self.name,) NamedElement.qualifiedName = property(namedelement_qualifiedname, doc=namedelement_qualifiedname.__doc__) del namedelement_qualifiedname %% override Association.endType derives Association.memberEnd Property.type # References the classifiers that are used as types of the ends of the # association. Association.endType = derived('endType', Type, 0, '*', Association.memberEnd, Property.type) Association.endType.filter = lambda self: [end.type for end in self.memberEnd if end] %% override Class.extension derives Extension.metaclass def class_extension(self): return list(self._factory.select(lambda e: e.isKindOf(Extension) and self is e.metaclass)) # TODO: use those as soon as Extension.metaclass can be used. #Class.extension = derived('extension', Extension, 0, '*', Extension.metaclass) #Class.extension.filter = class_extension Class.extension = property(class_extension, doc=\ """References the Extensions that specify additional properties of the metaclass. The property is derived from the extensions whose memberEnds are typed by the Class.""") del class_extension %% override Extension.metaclass derives Extension.ownedEnd Association.memberEnd def extension_metaclass(self): ownedEnd = self.ownedEnd metaend = [e for e in self.memberEnd if e is not ownedEnd] if metaend: return metaend[0].type # Don't use derived now, as derived() does not properly propagate the events # NOTE: let function return a list once this can be turned on #Extension.metaclass = derived('metaclass', Class, 0, 1, Extension.ownedEnd, Association.memberEnd) #Extension.metaclass.filter = extension_metaclass Extension.metaclass = property(extension_metaclass, doc=\ """References the Class that is extended through an Extension. The property is derived from the type of the memberEnd that is not the ownedEnd.""") del extension_metaclass %% override Classifier.inheritedMember Classifier.inheritedMember = derivedunion('inheritedMember', NamedElement, 0, '*') %% override Classifier.general def classifier_general(self): return [g.general for g in self.generalization] Classifier.general = property(classifier_general, doc=""" Return a list of all superclasses for class (iterating the Generalizations. """) del classifier_general %% override Class.superClass Class.superClass = Classifier.general %% override Namespace.importedMember Namespace.importedMember = derivedunion('importedMember', PackageableElement, 0, '*') %% override Property.opposite def property_opposite(self): """ In the case where the property is one navigable end of a binary association with both ends navigable, this gives the other end. For Gaphor the property on the other end is returned regardless the navigability. """ if self.association is not None and len(self.association.memberEnd) == 2: return self.association.memberEnd[0] is self \ and self.association.memberEnd[1] \ or self.association.memberEnd[0] return None Property.opposite = property(property_opposite, doc=property_opposite.__doc__) del property_opposite %% override Property.isComposite derives Property.aggregation #Property.isComposite = property(lambda self: self.aggregation == 'composite') Property.isComposite = derivedunion('isComposite', bool, 0, 1, Property.aggregation) Property.isComposite.filter = lambda obj: [obj.aggregation == 'composite'] %% override Constraint.context Constraint.context = derivedunion('context', Namespace, 0, 1) %% override Property.navigability def property_navigability(self): """ Get navigability of an association end. If no association is related to the property, then unknown navigability is assumed. """ assoc = self.association if not assoc or not self.opposite: return None # assume unknown owner = self.opposite.type if owner and ((type(self.type) in (Class, Interface) \ and self in owner.ownedAttribute) \ or self in assoc.navigableOwnedEnd): return True elif self in assoc.ownedEnd: return None else: return False Property.navigability = property(property_navigability, doc=property_navigability.__doc__) del property_navigability %% override Operation.type Operation.type = derivedunion('type', DataType, 0, 1) %% override Lifeline.parse from gaphor.UML.umllex import parse_lifeline Lifeline.parse = parse_lifeline del parse_lifeline %% override Lifeline.render from gaphor.UML.umllex import render_lifeline Lifeline.render = render_lifeline del render_lifeline %% override Extenstion.metaclass def extension_metaclass(self): """ References the Class that is extended through an Extension. The property is derived from the type of the memberEnd that is not the ownedEnd. """ for m in self.memberEnd: if m not in self.ownedEnd: return m return None Extenstion.metaclass = property(extension_metaclass, doc=extension_metaclass.__doc__) del extension_metaclass %% override Component.provided import itertools def _pr_interface_deps(classifier, dep_type): """ Return all interfaces, which are connected to a classifier with given dependency type. """ return (dep.supplier[0] for dep in classifier.clientDependency \ if dep.isKindOf(dep_type) and dep.supplier[0].isKindOf(Interface)) def _pr_rc_interface_deps(component, dep_type): """ Return all interfaces, which are connected to realizing classifiers of specified component. Returned interfaces are connected to realizing classifiers with given dependency type. Generator of generators is returned. Do not forget to flat it later. """ return (_pr_interface_deps(r.realizingClassifier, dep_type) for r in component.realization) def component_provided(self): implementations = (impl.contract[0] for impl in self.implementation if impl.isKindOf(Implementation)) realizations = _pr_interface_deps(self, Realization) # realizing classifiers realizations # this generator of generators, so flatten it later rc_realizations = _pr_rc_interface_deps(self, Realization) return tuple(set(itertools.chain(implementations, realizations, *rc_realizations))) Component.provided = property(component_provided, doc=""" Interfaces provided to component environment. """) del component_provided %% override Component.required def component_required(self): usages = _pr_interface_deps(self, Usage) # realizing classifiers usages # this generator of generators, so flatten it later rc_usages = _pr_rc_interface_deps(self, Usage) return tuple(set(itertools.chain(usages, *rc_usages))) Component.required = property(component_required, doc=""" Interfaces required by component. """) del component_required %% override Message.messageKind def message_messageKind(self): kind = 'unknown' if self.sendEvent: kind = 'lost' if self.receiveEvent: kind = 'complete' elif self.receiveEvent: kind = 'found' return kind Message.messageKind = property(message_messageKind, doc=""" MessageKind """) del message_messageKind %% override StructuredClassifier.part def structuredclassifier_part(self): return tuple(a for a in self.ownedAttribute if a.isComposite) StructuredClassifier.part = property(structuredclassifier_part, doc=""" Properties owned by a classifier by composition. """) del structuredclassifier_part PK!ANgaphor/UML/uml2.py# This file is generated by build_uml.py. DO NOT EDIT! from gaphor.UML.properties import ( association, attribute, enumeration, derived, derivedunion, redefine, ) # class 'ValueSpecification' has been stereotyped as 'SimpleAttribute' # class 'InstanceValue' has been stereotyped as 'SimpleAttribute' too # class 'Expression' has been stereotyped as 'SimpleAttribute' too # class 'LiteralSpecification' has been stereotyped as 'SimpleAttribute' too # class 'LiteralUnlimitedNatural' has been stereotyped as 'SimpleAttribute' too # class 'LiteralBoolean' has been stereotyped as 'SimpleAttribute' too # class 'LiteralInteger' has been stereotyped as 'SimpleAttribute' too # class 'LiteralString' has been stereotyped as 'SimpleAttribute' too # class 'LiteralNull' has been stereotyped as 'SimpleAttribute' too # class 'OpaqueExpression' has been stereotyped as 'SimpleAttribute' too # 14: override Element from gaphor.UML.element import Element class NamedElement(Element): pass class PackageableElement(NamedElement): pass class InstanceSpecification(PackageableElement): pass class EnumerationLiteral(InstanceSpecification): pass class Relationship(Element): pass class DirectedRelationship(Relationship): pass class PackageMerge(DirectedRelationship): pass class Namespace(NamedElement): pass class Type(PackageableElement): pass class RedefinableElement(NamedElement): pass class Classifier(Namespace, Type, RedefinableElement): pass class Association(Classifier, Relationship): pass class Extension(Association): pass class Actor(Classifier): pass class ActivityNode(RedefinableElement): pass class ControlNode(ActivityNode): pass class MergeNode(ControlNode): pass class Feature(RedefinableElement): pass class ActivityEdge(RedefinableElement): pass class ObjectFlow(ActivityEdge): pass class FinalNode(ControlNode): pass class ActivityFinalNode(FinalNode): pass class CommunicationPath(Association): pass class Dependency(DirectedRelationship, PackageableElement): pass class Permission(Dependency): pass class Abstraction(Dependency): pass class Realization(Abstraction): pass class TypedElement(NamedElement): pass class ObjectNode(TypedElement, ActivityNode): pass class Pin(ObjectNode): pass class Generalization(DirectedRelationship): pass class BehavioredClassifier(Classifier): pass class StructuredClassifier(Classifier): pass class EncapsulatedClassifer(StructuredClassifier): pass class Class(BehavioredClassifier, EncapsulatedClassifer): pass class DeploymentTarget(NamedElement): pass class Node(Class, DeploymentTarget): pass class Device(Node): pass class MultiplicityElement(Element): pass class StructuralFeature(MultiplicityElement, TypedElement, Feature): pass class UseCase(BehavioredClassifier): pass class InputPin(Pin): pass class Manifestation(Abstraction): pass class Component(Class): pass class ConnectableElement(TypedElement): pass class Interface(Classifier, ConnectableElement): pass class Include(DirectedRelationship): pass class PackageImport(DirectedRelationship): pass class ProfileApplication(PackageImport): pass class ExtensionPoint(RedefinableElement): pass class Usage(Dependency): pass class ElementImport(DirectedRelationship): pass class Property(StructuralFeature, ConnectableElement): pass class ExtensionEnd(Property): pass class DataType(Classifier): pass class Enumeration(DataType): pass class Slot(Element): pass class ExecutableNode(ActivityNode): pass class InitialNode(ControlNode): pass class Stereotype(Class): pass # 17: override Diagram from gaphor.UML.diagram import Diagram class DeployedArtifact(NamedElement): pass class Artifact(Classifier, DeployedArtifact): pass class ActivityParameterNode(ObjectNode): pass class PrimitiveType(DataType): pass class DecisionNode(ControlNode): pass class Package(Namespace, PackageableElement): pass class Profile(Package): pass class Behavior(Class): pass class Activity(Behavior): pass class Implementation(Realization): pass class Parameter(TypedElement, MultiplicityElement): pass class Presentation(Element): pass class BehavioralFeature(Feature, Namespace): pass class Operation(BehavioralFeature): pass class ControlFlow(ActivityEdge): pass class Substitution(Realization): pass class OutputPin(Pin): pass class ValuePin(InputPin): pass class Action(ExecutableNode): pass class Comment(Element): pass class ExecutionEnvironment(Node): pass class Extend(DirectedRelationship): pass class ActivityGroup(Element): pass class Constraint(PackageableElement): pass class InteractionFragment(NamedElement): pass class Interaction(Behavior, InteractionFragment): pass class ExecutionOccurence(InteractionFragment): pass class StateInvariant(InteractionFragment): pass class Lifeline(NamedElement): pass class Message(NamedElement): pass class MessageEnd(NamedElement): pass class OccurrenceSpecification(InteractionFragment): pass class GeneralOrdering(NamedElement): pass class Connector(Feature): pass class ConnectorEnd(MultiplicityElement): pass class FlowFinalNode(FinalNode): pass class JoinNode(ControlNode): pass class ForkNode(ControlNode): pass class StateMachine(Behavior): pass class Region(Namespace, RedefinableElement): pass # 20: override Transition class Transition(RedefinableElement, NamedElement): pass class Vertex(NamedElement): pass class Pseudostate(Vertex): pass class ConnectionPointReference(Vertex): pass class State(Vertex, Namespace, RedefinableElement): pass class FinalState(State): pass class Port(Property): pass class Deployment(Dependency): pass class ActivityPartition(ActivityGroup, NamedElement): pass class MessageOccurrenceSpecification(MessageEnd, OccurrenceSpecification): pass class AcceptEventAction(Action): pass class ReplyAction(Action): pass class UnmarshallAction(Action): pass class AcceptCallAction(AcceptEventAction): pass class InvocationAction(Action): pass class SendSignalAction(InvocationAction): pass class Collaboration(StructuredClassifier, BehavioredClassifier): pass class Trigger(NamedElement): pass class Event(PackageableElement): pass class ExecutionEvent(Event): pass class CreationEvent(Event): pass class MessageEvent(Event): pass class DestructionEvent(Event): pass class SendOperationEvent(MessageEvent): pass class SendSignalEvent(MessageEvent): pass class ReceiveOperationEvent(MessageEvent): pass class ReceiveSignalEvent(MessageEvent): pass class Signal(Classifier): pass class Reception(BehavioralFeature): pass Extension.isRequired = attribute("isRequired", int) Feature.isStatic = attribute("isStatic", int, default=False) RedefinableElement.isLeaf = attribute("isLeaf", int, default=True) Generalization.isSubstitutable = attribute("isSubstitutable", int) ObjectNode.ordering = enumeration( "ordering", ("unordered", "ordered", "LIFO", "FIFO"), "FIFO" ) ObjectNode.isControlType = attribute("isControlType", int, default=False) StructuralFeature.isReadOnly = attribute("isReadOnly", int, default=False) NamedElement.visibility = enumeration( "visibility", ("public", "private", "package", "protected"), "public" ) NamedElement.name = attribute("name", str) # 33: override NamedElement.qualifiedName def namedelement_qualifiedname(self): """ Returns the qualified name of the element as a tuple """ if self.namespace: return self.namespace.qualifiedName + (self.name,) else: return (self.name,) NamedElement.qualifiedName = property( namedelement_qualifiedname, doc=namedelement_qualifiedname.__doc__ ) del namedelement_qualifiedname Component.isIndirectlyInstantiated = attribute( "isIndirectlyInstantiated", int, default=True ) Association.isDerived = attribute("isDerived", int, default=False) PackageableElement.visibility = enumeration( "visibility", ("public", "private", "package", "protected"), "public" ) ElementImport.visibility = enumeration( "visibility", ("public", "private", "package", "protected"), "public" ) ElementImport.alias = attribute("alias", str) MultiplicityElement.isUnique = attribute("isUnique", int, default=True) MultiplicityElement.isOrdered = attribute("isOrdered", int, default=True) Activity.body = attribute("body", str) Activity.language = attribute("language", str) Classifier.isAbstract = attribute("isAbstract", int, default=False) Parameter.direction = enumeration("direction", ("inout", "in", "out", "return"), "in") Operation.isQuery = attribute("isQuery", int, default=False) Property.aggregation = enumeration( "aggregation", ("none", "shared", "composite"), "none" ) Property.isDerivedUnion = attribute("isDerivedUnion", int, default=False) Property.isDerived = attribute("isDerived", int, default=False) Property.isReadOnly = attribute("isReadOnly", int, default=False) # 135: override Property.navigability def property_navigability(self): """ Get navigability of an association end. If no association is related to the property, then unknown navigability is assumed. """ assoc = self.association if not assoc or not self.opposite: return None # assume unknown owner = self.opposite.type if owner and ( (type(self.type) in (Class, Interface) and self in owner.ownedAttribute) or self in assoc.navigableOwnedEnd ): return True elif self in assoc.ownedEnd: return None else: return False Property.navigability = property( property_navigability, doc=property_navigability.__doc__ ) del property_navigability Behavior.isReentrant = attribute("isReentrant", int) BehavioralFeature.isAbstract = attribute("isAbstract", int) Action.effect = attribute("effect", str) Comment.body = attribute("body", str) PackageImport.visibility = enumeration( "visibility", ("public", "private", "package", "protected"), "public" ) # 240: override Message.messageKind def message_messageKind(self): kind = "unknown" if self.sendEvent: kind = "lost" if self.receiveEvent: kind = "complete" elif self.receiveEvent: kind = "found" return kind Message.messageKind = property( message_messageKind, doc=""" MessageKind """, ) del message_messageKind Message.messageSort = enumeration( "messageSort", ( "synchCall", "asynchCall", "asynchSignal", "createMessage", "deleteMessage", "reply", ), "synchCall", ) Connector.kind = enumeration("kind", ("assembly", "delegation"), "assembly") JoinNode.isCombineDuplicate = attribute("isCombineDuplicate", int, default=True) Transition.kind = enumeration("kind", ("internal", "local", "external"), "internal") Pseudostate.kind = enumeration( "kind", ( "initial", "deepHistory", "shallowHistory", "join", "fork", "junction", "choice", "entryPoint", "exitPoint", "terminate", ), "initial", ) Port.isBehavior = attribute("isBehavior", int) Port.isService = attribute("isService", int) ActivityPartition.isDimension = attribute("isDimension", int, default=False) ActivityPartition.isExternal = attribute("isExternal", int, default=False) AcceptEventAction.isUnmarshall = attribute("isUnmarshall", int, default=False) Operation.precondition = association("precondition", Constraint, composite=True) Package.ownedDiagram = association( "ownedDiagram", Diagram, composite=True, opposite="package" ) Diagram.package = association("package", Package, upper=1, opposite="ownedDiagram") Package.nestedPackage = association( "nestedPackage", Package, composite=True, opposite="package" ) Package.package = association("package", Package, upper=1, opposite="nestedPackage") NamedElement.clientDependency = association( "clientDependency", Dependency, opposite="client" ) Dependency.client = association( "client", NamedElement, lower=1, opposite="clientDependency" ) DecisionNode.decisionInput = association("decisionInput", Behavior, upper=1) Activity.edge = association("edge", ActivityEdge, composite=True, opposite="activity") ActivityEdge.activity = association("activity", Activity, upper=1, opposite="edge") Substitution.contract = association("contract", Classifier, lower=1, upper=1) Operation.bodyCondition = association( "bodyCondition", Constraint, upper=1, composite=True ) # 'InstanceSpecification.specification' is a simple attribute InstanceSpecification.specification = attribute("specification", str) BehavioralFeature.method = association("method", Behavior) Property.datatype = association( "datatype", DataType, upper=1, opposite="ownedAttribute" ) DataType.ownedAttribute = association( "ownedAttribute", Property, composite=True, opposite="datatype" ) TypedElement.type = association("type", Type, upper=1) Element.presentation = association( "presentation", Presentation, composite=True, opposite="subject" ) Presentation.subject = association("subject", Element, upper=1, opposite="presentation") ActivityParameterNode.parameter = association("parameter", Parameter, lower=1, upper=1) Dependency.supplier = association( "supplier", NamedElement, lower=1, opposite="supplierDependency" ) NamedElement.supplierDependency = association( "supplierDependency", Dependency, opposite="supplier" ) Operation.redefinedOperation = association("redefinedOperation", Operation) Activity.group = association( "group", ActivityGroup, composite=True, opposite="activity" ) ActivityGroup.activity = association("activity", Activity, upper=1, opposite="group") Package.ownedClassifier = association( "ownedClassifier", Type, composite=True, opposite="package" ) Type.package = association("package", Package, upper=1, opposite="ownedClassifier") Property.subsettedProperty = association("subsettedProperty", Property) Property.classifier = association( "classifier", Classifier, upper=1, opposite="attribute" ) ExtensionEnd.type = association("type", Stereotype, lower=1, upper=1) Profile.metamodelReference = association( "metamodelReference", PackageImport, composite=True ) # 'ActivityEdge.guard' is a simple attribute ActivityEdge.guard = attribute("guard", str) Class.ownedOperation = association( "ownedOperation", Operation, composite=True, opposite="class_" ) Operation.class_ = association("class_", Class, upper=1, opposite="ownedOperation") Enumeration.literal = association( "literal", EnumerationLiteral, composite=True, opposite="enumeration" ) EnumerationLiteral.enumeration = association( "enumeration", Enumeration, upper=1, opposite="literal" ) ActivityEdge.source = association( "source", ActivityNode, lower=1, upper=1, opposite="outgoing" ) ActivityNode.outgoing = association("outgoing", ActivityEdge, opposite="source") Profile.ownedStereotype = association("ownedStereotype", Stereotype, composite=True) Property.redefinedProperty = association("redefinedProperty", Property) DataType.ownedOperation = association( "ownedOperation", Operation, composite=True, opposite="datatype" ) Operation.datatype = association( "datatype", DataType, upper=1, opposite="ownedOperation" ) Generalization.general = association("general", Classifier, lower=1, upper=1) Classifier.ownedUseCase = association("ownedUseCase", UseCase, composite=True) # 'MultiplicityElement.upperValue' is a simple attribute MultiplicityElement.upperValue = attribute("upperValue", str) PackageMerge.mergingPackage = association( "mergingPackage", Package, lower=1, upper=1, opposite="packageExtension" ) Package.packageExtension = association( "packageExtension", PackageMerge, composite=True, opposite="mergingPackage" ) Package.appliedProfile = association( "appliedProfile", ProfileApplication, composite=True ) Activity.node = association("node", ActivityNode, composite=True) # 'Parameter.defaultValue' is a simple attribute Parameter.defaultValue = attribute("defaultValue", str) Class.nestedClassifier = association("nestedClassifier", Classifier, composite=True) # 'Slot.value' is a simple attribute Slot.value = attribute("value", str) Include.addition = association("addition", UseCase, lower=1, upper=1) Realization.realizingClassifier = association( "realizingClassifier", Classifier, lower=1, upper=1 ) # 'TypedElement.typeValue' is a simple attribute TypedElement.typeValue = attribute("typeValue", str) Constraint.constrainedElement = association("constrainedElement", Element) PackageMerge.mergedPackage = association("mergedPackage", Package, lower=1, upper=1) BehavioralFeature.formalParameter = association( "formalParameter", Parameter, composite=True, opposite="ownerFormalParam" ) Parameter.ownerFormalParam = association( "ownerFormalParam", BehavioralFeature, upper=1, opposite="formalParameter" ) Class.ownedAttribute = association( "ownedAttribute", Property, composite=True, opposite="class_" ) Property.class_ = association("class_", Class, upper=1, opposite="ownedAttribute") Extend.extendedCase = association("extendedCase", UseCase, lower=1, upper=1) # 'Property.defaultValue' is a simple attribute Property.defaultValue = attribute("defaultValue", str) Namespace.ownedRule = association("ownedRule", Constraint, composite=True) Property.association = association( "association", Association, upper=1, opposite="memberEnd" ) Association.memberEnd = association( "memberEnd", Property, lower=2, composite=True, opposite="association" ) Classifier.generalization = association( "generalization", Generalization, composite=True, opposite="specific" ) Generalization.specific = association( "specific", Classifier, lower=1, upper=1, opposite="generalization" ) Realization.abstraction = association( "abstraction", Component, upper=1, opposite="realization" ) Component.realization = association( "realization", Realization, composite=True, opposite="abstraction" ) # 'ValuePin.value_' is a simple attribute ValuePin.value_ = attribute("value_", str) BehavioralFeature.raisedException = association("raisedException", Type) Activity.action = association("action", Action, composite=True) # 'Abstraction.mapping' is a simple attribute Abstraction.mapping = attribute("mapping", str) ActivityNode.incoming = association("incoming", ActivityEdge, opposite="target") ActivityEdge.target = association( "target", ActivityNode, lower=1, upper=1, opposite="incoming" ) Extend.extensionLocation = association("extensionLocation", ExtensionPoint, lower=1) Property.interface_ = association( "interface_", Interface, upper=1, opposite="ownedAttribute" ) Interface.ownedAttribute = association( "ownedAttribute", Property, composite=True, opposite="interface_" ) ActivityGroup.edgeContents = association( "edgeContents", ActivityEdge, opposite="inGroup" ) ActivityEdge.inGroup = association("inGroup", ActivityGroup, opposite="edgeContents") Slot.owningInstance = association( "owningInstance", InstanceSpecification, lower=1, upper=1, opposite="slot" ) InstanceSpecification.slot = association( "slot", Slot, composite=True, opposite="owningInstance" ) UseCase.subject = association("subject", Classifier) Property.owningAssociation = association( "owningAssociation", Association, upper=1, opposite="ownedEnd" ) Association.ownedEnd = association( "ownedEnd", Property, composite=True, opposite="owningAssociation" ) Interface.redefinedInterface = association("redefinedInterface", Interface) Artifact.manifestation = association("manifestation", Manifestation, composite=True) ExtensionPoint.useCase = association( "useCase", UseCase, lower=1, upper=1, opposite="extensionPoint" ) UseCase.extensionPoint = association( "extensionPoint", ExtensionPoint, opposite="useCase" ) Operation.postcondition = association("postcondition", Constraint, composite=True) Extension.ownedEnd = association( "ownedEnd", ExtensionEnd, lower=1, upper=1, composite=True ) # 'Constraint.specification' is a simple attribute Constraint.specification = attribute("specification", str) Profile.metaclassReference = association( "metaclassReference", ElementImport, composite=True ) Namespace.elementImport = association( "elementImport", ElementImport, composite=True, opposite="importingNamespace" ) ElementImport.importingNamespace = association( "importingNamespace", Namespace, upper=1, opposite="elementImport" ) # 'MultiplicityElement.lowerValue' is a simple attribute MultiplicityElement.lowerValue = attribute("lowerValue", str) Interface.nestedInterface = association("nestedInterface", Interface, composite=True) InstanceSpecification.classifier = association("classifier", Classifier) Interface.ownedOperation = association( "ownedOperation", Operation, composite=True, opposite="interface_" ) Operation.interface_ = association( "interface_", Interface, upper=1, opposite="ownedOperation" ) ElementImport.importedElement = association( "importedElement", PackageableElement, lower=1, upper=1 ) Parameter.ownerReturnParam = association( "ownerReturnParam", BehavioralFeature, upper=1, opposite="returnResult" ) BehavioralFeature.returnResult = association( "returnResult", Parameter, composite=True, opposite="ownerReturnParam" ) Classifier.redefinedClassifier = association("redefinedClassifier", Classifier) Substitution.substitutingClassifier = association( "substitutingClassifier", Classifier, lower=1, upper=1, opposite="substitution" ) Classifier.substitution = association( "substitution", Substitution, composite=True, opposite="substitutingClassifier" ) Operation.raisedException = association("raisedException", Type) PackageImport.importedPackage = association( "importedPackage", Package, lower=1, upper=1 ) StructuralFeature.slot = association( "slot", Slot, composite=True, opposite="definingFeature" ) Slot.definingFeature = association( "definingFeature", StructuralFeature, lower=1, upper=1, opposite="slot" ) Include.includingCase = association( "includingCase", UseCase, lower=1, upper=1, opposite="include" ) UseCase.include = association( "include", Include, composite=True, opposite="includingCase" ) Extend.extension = association( "extension", UseCase, lower=1, upper=1, opposite="extend" ) UseCase.extend = association("extend", Extend, composite=True, opposite="extension") Extend.constraint = association("constraint", Constraint, upper=1, composite=True) ProfileApplication.importedProfile = association( "importedProfile", Profile, lower=1, upper=1 ) Namespace.packageImport = association( "packageImport", PackageImport, composite=True, opposite="importingNamespace" ) PackageImport.importingNamespace = association( "importingNamespace", Namespace, upper=1, opposite="packageImport" ) Behavior.redefinedBehavior = association("redefinedBehavior", Behavior) Element.ownedComment = association("ownedComment", Comment, opposite="annotatedElement") Comment.annotatedElement = association( "annotatedElement", Element, opposite="ownedComment" ) Behavior.context = association( "context", BehavioredClassifier, upper=1, opposite="ownedBehavior" ) BehavioredClassifier.ownedBehavior = association( "ownedBehavior", Behavior, composite=True, opposite="context" ) ActivityGroup.nodeContents = association( "nodeContents", ActivityNode, opposite="inGroup" ) ActivityNode.inGroup = association("inGroup", ActivityGroup, opposite="nodeContents") UseCase.ownedAttribute = association( "ownedAttribute", Property, composite=True, opposite="useCase" ) Property.useCase = association("useCase", UseCase, upper=1, opposite="ownedAttribute") Property.actor = association("actor", Actor, upper=1, opposite="ownedAttribute") Actor.ownedAttribute = association( "ownedAttribute", Property, composite=True, opposite="actor" ) InteractionFragment.enclosingInteraction = association( "enclosingInteraction", Interaction, upper=1, opposite="fragment" ) Interaction.fragment = association( "fragment", InteractionFragment, opposite="enclosingInteraction" ) StateInvariant.invariant = association( "invariant", Constraint, lower=1, upper=1, composite=True ) Lifeline.coveredBy = association("coveredBy", InteractionFragment, opposite="covered") InteractionFragment.covered = association( "covered", Lifeline, lower=1, upper=1, opposite="coveredBy" ) Lifeline.interaction = association( "interaction", Interaction, lower=1, upper=1, opposite="lifeline" ) Interaction.lifeline = association( "lifeline", Lifeline, composite=True, opposite="interaction" ) # 'Lifeline.discriminator' is a simple attribute Lifeline.discriminator = attribute("discriminator", str) # 'Message.argument' is a simple attribute Message.argument = attribute("argument", str) Message.signature = association("signature", NamedElement, upper=1) MessageEnd.sendMessage = association( "sendMessage", Message, upper=1, opposite="sendEvent" ) Message.sendEvent = association( "sendEvent", MessageEnd, upper=1, composite=True, opposite="sendMessage" ) MessageEnd.receiveMessage = association( "receiveMessage", Message, upper=1, opposite="receiveEvent" ) Message.receiveEvent = association( "receiveEvent", MessageEnd, upper=1, composite=True, opposite="receiveMessage" ) Message.interaction = association( "interaction", Interaction, lower=1, upper=1, opposite="message" ) Interaction.message = association( "message", Message, composite=True, opposite="interaction" ) InteractionFragment.generalOrdering = association( "generalOrdering", GeneralOrdering, composite=True ) GeneralOrdering.before = association( "before", OccurrenceSpecification, lower=1, upper=1, opposite="toAfter" ) OccurrenceSpecification.toAfter = association( "toAfter", GeneralOrdering, opposite="before" ) GeneralOrdering.after = association( "after", OccurrenceSpecification, lower=1, upper=1, opposite="toBefore" ) OccurrenceSpecification.toBefore = association( "toBefore", GeneralOrdering, opposite="after" ) ExecutionOccurence.finish = association( "finish", OccurrenceSpecification, lower=1, upper=1, opposite="finishExec" ) OccurrenceSpecification.finishExec = association( "finishExec", ExecutionOccurence, opposite="finish" ) ExecutionOccurence.start = association( "start", OccurrenceSpecification, lower=1, upper=1, opposite="startExec" ) OccurrenceSpecification.startExec = association( "startExec", ExecutionOccurence, opposite="start" ) ExecutionOccurence.behavior = association("behavior", Behavior) StructuredClassifier.ownedConnector = association( "ownedConnector", Connector, composite=True ) Connector.redefinedConnector = association("redefinedConnector", Connector) Connector.type = association("type", Association, upper=1) Connector.end = association("end", ConnectorEnd, lower=2, composite=True) Connector.contract = association("contract", Behavior) ConnectorEnd.role = association("role", ConnectableElement, upper=1, opposite="end") ConnectableElement.end = association("end", ConnectorEnd, opposite="role") StructuredClassifier.ownedAttribute = association( "ownedAttribute", Property, composite=True ) # 'ObjectNode.upperBound' is a simple attribute ObjectNode.upperBound = attribute("upperBound", str) ObjectNode.selection = association("selection", Behavior, upper=1) # 'JoinNode.joinSpec' is a simple attribute JoinNode.joinSpec = attribute("joinSpec", str) StateMachine.region = association( "region", Region, lower=1, composite=True, opposite="stateMachine" ) Region.stateMachine = association( "stateMachine", StateMachine, upper=1, opposite="region" ) Transition.container = association("container", Region, lower=1, upper=1) Region.subvertex = association( "subvertex", Vertex, composite=True, opposite="container" ) Vertex.container = association("container", Region, upper=1, opposite="subvertex") Transition.source = association("source", Vertex, lower=1, upper=1, opposite="outgoing") Vertex.outgoing = association("outgoing", Transition, opposite="source") Transition.target = association("target", Vertex, lower=1, upper=1, opposite="incoming") Vertex.incoming = association("incoming", Transition, opposite="target") ConnectionPointReference.entry = association("entry", Pseudostate) ConnectionPointReference.exit = association("exit", Pseudostate) Pseudostate.stateMachine = association("stateMachine", StateMachine, upper=1) Region.state = association("state", State, upper=1) Pseudostate.state = association("state", State, upper=1) ConnectionPointReference.state = association("state", State, upper=1) State.entry = association("entry", Behavior, upper=1, composite=True) State.exit = association("exit", Behavior, upper=1, composite=True) State.doActivity = association("doActivity", Behavior, upper=1, composite=True) Transition.effect = association("effect", Behavior, upper=1, composite=True) State.statevariant = association( "statevariant", Constraint, upper=1, composite=True, opposite="owningState" ) Constraint.owningState = association( "owningState", State, upper=1, opposite="statevariant" ) Transition.guard = association("guard", Constraint, upper=1, composite=True) State.submachine = association("submachine", StateMachine, upper=1) StateMachine.extendedStateMachine = association( "extendedStateMachine", StateMachine, upper=1 ) ConnectorEnd.partWithPort = association("partWithPort", Property, upper=1) EncapsulatedClassifer.ownedPort = association("ownedPort", Port, composite=True) Element.appliedStereotype = association( "appliedStereotype", InstanceSpecification, opposite="extended" ) InstanceSpecification.extended = association( "extended", Element, opposite="appliedStereotype" ) Node.nestedNode = association("nestedNode", Node, composite=True) DeploymentTarget.deployment = association("deployment", Deployment, composite=True) Deployment.deployedArtifact = association("deployedArtifact", DeployedArtifact) ActivityNode.inPartition = association( "inPartition", ActivityPartition, opposite="node" ) ActivityPartition.node = association("node", ActivityNode, opposite="inPartition") ActivityPartition.represents = association("represents", Element, upper=1) ActivityPartition.subpartition = association("subpartition", ActivityPartition) Association.navigableOwnedEnd = association("navigableOwnedEnd", Property) AcceptEventAction.result = association("result", OutputPin, composite=True) UnmarshallAction.result = association("result", OutputPin, composite=True) AcceptCallAction.returnInformation = association( "returnInformation", OutputPin, lower=1, upper=1, composite=True ) UnmarshallAction.unmarshallType = association( "unmarshallType", Classifier, lower=1, upper=1 ) UnmarshallAction.object = association( "object", InputPin, lower=1, upper=1, composite=True ) ReplyAction.replyValue = association("replyValue", InputPin, upper=1, composite=True) ReplyAction.returnInformation = association( "returnInformation", InputPin, lower=1, upper=1, composite=True ) SendSignalAction.target = association("target", InputPin, composite=True) Collaboration.collaborationRole = association("collaborationRole", ConnectableElement) BehavioredClassifier.ownedTrigger = association("ownedTrigger", Trigger, composite=True) Trigger.event = association("event", Event, lower=1, upper=1) Signal.ownedAttribute = association("ownedAttribute", Property, composite=True) Reception.signal = association("signal", Signal, upper=1) Class.ownedReception = association("ownedReception", Reception, composite=True) Interface.ownedReception = association("ownedReception", Reception, composite=True) SendOperationEvent.operation = association("operation", Operation, lower=1, upper=1) SendSignalEvent.signal = association("signal", Signal, lower=1, upper=1) ReceiveOperationEvent.operation = association("operation", Operation, lower=1, upper=1) ReceiveSignalEvent.signal = association("signal", Signal, lower=1, upper=1) # 23: override MultiplicityElement.lower derives MultiplicityElement.lowerValue MultiplicityElement.lower = derived( "lower", object, 0, 1, MultiplicityElement.lowerValue ) MultiplicityElement.lower.filter = lambda obj: [obj.lowerValue] # MultiplicityElement.lower = MultiplicityElement.lowerValue # 28: override MultiplicityElement.upper derives MultiplicityElement.upperValue MultiplicityElement.upper = derived( "upper", object, 0, 1, MultiplicityElement.upperValue ) MultiplicityElement.upper.filter = lambda obj: [obj.upperValue] # MultiplicityElement.upper = MultiplicityElement.upperValue # 127: override Property.isComposite derives Property.aggregation # Property.isComposite = property(lambda self: self.aggregation == 'composite') Property.isComposite = derivedunion("isComposite", bool, 0, 1, Property.aggregation) Property.isComposite.filter = lambda obj: [obj.aggregation == "composite"] RedefinableElement.redefinedElement = derivedunion( "redefinedElement", RedefinableElement, 0, "*", Property.redefinedProperty, Classifier.redefinedClassifier, Operation.redefinedOperation, Interface.redefinedInterface, Behavior.redefinedBehavior, Connector.redefinedConnector, ) Classifier.attribute = derivedunion( "attribute", Property, 0, "*", Class.ownedAttribute, DataType.ownedAttribute, Interface.ownedAttribute, UseCase.ownedAttribute, Actor.ownedAttribute, Signal.ownedAttribute, ) Classifier.feature = derivedunion( "feature", Feature, 0, "*", Interface.ownedOperation, UseCase.extensionPoint, DataType.ownedOperation, Class.ownedOperation, Association.ownedEnd, Classifier.attribute, StructuredClassifier.ownedConnector, Class.ownedReception, Interface.ownedReception, ) Feature.featuringClassifier = derivedunion( "featuringClassifier", Classifier, 1, "*", Property.class_, Property.owningAssociation, Operation.class_, Operation.datatype, Property.datatype, Operation.interface_, ) # 106: override Property.opposite def property_opposite(self): """ In the case where the property is one navigable end of a binary association with both ends navigable, this gives the other end. For Gaphor the property on the other end is returned regardless the navigability. """ if self.association is not None and len(self.association.memberEnd) == 2: return ( self.association.memberEnd[0] is self and self.association.memberEnd[1] or self.association.memberEnd[0] ) return None Property.opposite = property(property_opposite, doc=property_opposite.__doc__) del property_opposite BehavioralFeature.parameter = derivedunion( "parameter", Parameter, 0, "*", BehavioralFeature.returnResult, BehavioralFeature.formalParameter, ) Action.output = derivedunion("output", OutputPin, 0, "*") Transition.redefintionContext = derivedunion("redefintionContext", Classifier, 1, 1) RedefinableElement.redefinitionContext = derivedunion( "redefinitionContext", Classifier, 0, "*", Operation.class_, Property.classifier, Operation.datatype, Transition.redefintionContext, ) NamedElement.namespace = derivedunion( "namespace", Namespace, 0, 1, Parameter.ownerReturnParam, Property.interface_, Property.class_, Property.owningAssociation, Operation.class_, EnumerationLiteral.enumeration, Diagram.package, Operation.datatype, Type.package, Property.datatype, Operation.interface_, Package.package, Parameter.ownerFormalParam, Property.useCase, Property.actor, Lifeline.interaction, Message.interaction, Region.stateMachine, Transition.container, Vertex.container, Pseudostate.stateMachine, Region.state, ConnectionPointReference.state, ) Namespace.ownedMember = derivedunion( "ownedMember", NamedElement, 0, "*", Interface.ownedOperation, Enumeration.literal, Package.ownedDiagram, Namespace.ownedRule, UseCase.extensionPoint, DataType.ownedOperation, Operation.precondition, BehavioralFeature.returnResult, Profile.ownedStereotype, Class.nestedClassifier, Class.ownedAttribute, BehavioralFeature.formalParameter, Classifier.ownedUseCase, DataType.ownedAttribute, Class.ownedOperation, Operation.postcondition, Association.ownedEnd, Package.ownedClassifier, Interface.ownedAttribute, Operation.bodyCondition, Extend.constraint, Package.nestedPackage, BehavioredClassifier.ownedBehavior, UseCase.ownedAttribute, Actor.ownedAttribute, StateInvariant.invariant, Interaction.lifeline, Interaction.message, StateMachine.region, Region.subvertex, Node.nestedNode, BehavioredClassifier.ownedTrigger, Signal.ownedAttribute, Class.ownedReception, Interface.ownedReception, ) # 91: override Classifier.general def classifier_general(self): return [g.general for g in self.generalization] Classifier.general = property( classifier_general, doc=""" Return a list of all superclasses for class (iterating the Generalizations. """, ) del classifier_general # 48: override Association.endType derives Association.memberEnd Property.type # References the classifiers that are used as types of the ends of the # association. Association.endType = derived( "endType", Type, 0, "*", Association.memberEnd, Property.type ) Association.endType.filter = lambda self: [end.type for end in self.memberEnd if end] Classifier.attribute = derivedunion( "attribute", Property, 0, "*", Class.ownedAttribute, DataType.ownedAttribute, Interface.ownedAttribute, UseCase.ownedAttribute, Actor.ownedAttribute, Signal.ownedAttribute, ) # 132: override Constraint.context Constraint.context = derivedunion("context", Namespace, 0, 1) # 160: override Operation.type Operation.type = derivedunion("type", DataType, 0, 1) # 71: override Extension.metaclass derives Extension.ownedEnd Association.memberEnd def extension_metaclass(self): ownedEnd = self.ownedEnd metaend = [e for e in self.memberEnd if e is not ownedEnd] if metaend: return metaend[0].type # Don't use derived now, as derived() does not properly propagate the events # NOTE: let function return a list once this can be turned on # Extension.metaclass = derived('metaclass', Class, 0, 1, Extension.ownedEnd, Association.memberEnd) # Extension.metaclass.filter = extension_metaclass Extension.metaclass = property( extension_metaclass, doc="""References the Class that is extended through an Extension. The property is derived from the type of the memberEnd that is not the ownedEnd.""", ) del extension_metaclass # 57: override Class.extension derives Extension.metaclass def class_extension(self): return list( self._factory.select(lambda e: e.isKindOf(Extension) and self is e.metaclass) ) # TODO: use those as soon as Extension.metaclass can be used. # Class.extension = derived('extension', Extension, 0, '*', Extension.metaclass) # Class.extension.filter = class_extension Class.extension = property( class_extension, doc="""References the Extensions that specify additional properties of the metaclass. The property is derived from the extensions whose memberEnds are typed by the Class.""", ) del class_extension DirectedRelationship.target = derivedunion( "target", Element, 1, "*", PackageImport.importedPackage, PackageMerge.mergedPackage, Generalization.general, Include.addition, Extend.extendedCase, Realization.realizingClassifier, ElementImport.importedElement, Substitution.contract, ) DirectedRelationship.source = derivedunion( "source", Element, 1, "*", Extend.extension, Realization.abstraction, Substitution.substitutingClassifier, Include.includingCase, ElementImport.importingNamespace, Generalization.specific, PackageImport.importingNamespace, PackageMerge.mergingPackage, ) Action.context_ = derivedunion("context_", Classifier, 0, 1) Relationship.relatedElement = derivedunion( "relatedElement", Element, 1, "*", DirectedRelationship.target, DirectedRelationship.source, ) ActivityGroup.superGroup = derivedunion("superGroup", ActivityGroup, 0, 1) ActivityGroup.subgroup = derivedunion( "subgroup", ActivityGroup, 0, "*", ActivityPartition.subpartition ) # 88: override Classifier.inheritedMember Classifier.inheritedMember = derivedunion("inheritedMember", NamedElement, 0, "*") StructuredClassifier.role = derivedunion( "role", ConnectableElement, 0, "*", StructuredClassifier.ownedAttribute, Collaboration.collaborationRole, ) Namespace.member = derivedunion( "member", NamedElement, 0, "*", BehavioralFeature.parameter, Namespace.ownedMember, Association.memberEnd, Classifier.inheritedMember, StructuredClassifier.role, ) # 225: override Component.required def component_required(self): usages = _pr_interface_deps(self, Usage) # realizing classifiers usages # this generator of generators, so flatten it later rc_usages = _pr_rc_interface_deps(self, Usage) return tuple(set(itertools.chain(usages, *rc_usages))) Component.required = property( component_required, doc=""" Interfaces required by component. """, ) del component_required # 103: override Namespace.importedMember Namespace.importedMember = derivedunion("importedMember", PackageableElement, 0, "*") Action.input = derivedunion("input", InputPin, 0, "*", SendSignalAction.target) # 189: override Component.provided import itertools def _pr_interface_deps(classifier, dep_type): """ Return all interfaces, which are connected to a classifier with given dependency type. """ return ( dep.supplier[0] for dep in classifier.clientDependency if dep.isKindOf(dep_type) and dep.supplier[0].isKindOf(Interface) ) def _pr_rc_interface_deps(component, dep_type): """ Return all interfaces, which are connected to realizing classifiers of specified component. Returned interfaces are connected to realizing classifiers with given dependency type. Generator of generators is returned. Do not forget to flat it later. """ return ( _pr_interface_deps(r.realizingClassifier, dep_type) for r in component.realization ) def component_provided(self): implementations = ( impl.contract[0] for impl in self.implementation if impl.isKindOf(Implementation) ) realizations = _pr_interface_deps(self, Realization) # realizing classifiers realizations # this generator of generators, so flatten it later rc_realizations = _pr_rc_interface_deps(self, Realization) return tuple(set(itertools.chain(implementations, realizations, *rc_realizations))) Component.provided = property( component_provided, doc=""" Interfaces provided to component environment. """, ) del component_provided # 88: override Classifier.inheritedMember Classifier.inheritedMember = derivedunion("inheritedMember", NamedElement, 0, "*") Element.owner = derivedunion( "owner", Element, 0, 1, Slot.owningInstance, Realization.abstraction, ElementImport.importingNamespace, Generalization.specific, ActivityEdge.activity, ActivityGroup.superGroup, ActivityGroup.activity, PackageImport.importingNamespace, PackageMerge.mergingPackage, NamedElement.namespace, Pseudostate.state, ) Element.ownedElement = derivedunion( "ownedElement", Element, 0, "*", Artifact.manifestation, Element.ownedComment, Action.input, Classifier.generalization, Namespace.ownedMember, Namespace.elementImport, Activity.group, Component.realization, Namespace.packageImport, Package.packageExtension, Substitution.contract, ActivityGroup.subgroup, Activity.edge, Activity.node, Action.output, Interaction.fragment, InteractionFragment.generalOrdering, Connector.end, State.entry, State.exit, State.doActivity, Transition.effect, State.statevariant, Transition.guard, DeploymentTarget.deployment, ) StructuredClassifier.role = derivedunion( "role", ConnectableElement, 0, "*", StructuredClassifier.ownedAttribute, Collaboration.collaborationRole, ) ConnectorEnd.definingEnd = derivedunion("definingEnd", Property, 0, 1) # 259: override StructuredClassifier.part def structuredclassifier_part(self): return tuple(a for a in self.ownedAttribute if a.isComposite) StructuredClassifier.part = property( structuredclassifier_part, doc=""" Properties owned by a classifier by composition. """, ) del structuredclassifier_part Transition.redefintionContext = derivedunion("redefintionContext", Classifier, 1, 1) # 100: override Class.superClass Class.superClass = Classifier.general ActivityNode.redefinedElement = redefine( ActivityNode, "redefinedElement", ActivityNode, RedefinableElement.redefinedElement ) Implementation.contract = redefine( Implementation, "contract", Interface, Dependency.supplier ) BehavioredClassifier.implementation = redefine( BehavioredClassifier, "implementation", Implementation, NamedElement.clientDependency, ) Implementation.implementatingClassifier = redefine( Implementation, "implementatingClassifier", BehavioredClassifier, Dependency.client ) Parameter.operation = redefine( Parameter, "operation", Operation, Parameter.ownerFormalParam ) Operation.formalParameter = redefine( Operation, "formalParameter", Parameter, BehavioralFeature.formalParameter ) ActivityEdge.redefinedElement = redefine( ActivityEdge, "redefinedElement", ActivityEdge, RedefinableElement.redefinedElement ) Package.ownedMember = redefine( Package, "ownedMember", PackageableElement, Namespace.ownedMember ) Component.ownedMember = redefine( Component, "ownedMember", PackageableElement, Namespace.ownedMember ) Region.extendedRegion = redefine( Region, "extendedRegion", Region, RedefinableElement.redefinedElement ) State.redefinedState = redefine( State, "redefinedState", State, RedefinableElement.redefinedElement ) Transition.redefinedTransition = redefine( Transition, "redefinedTransition", Transition, RedefinableElement.redefinedElement ) # 163: override Lifeline.parse from gaphor.UML.umllex import parse_lifeline Lifeline.parse = parse_lifeline del parse_lifeline # 168: override Lifeline.render from gaphor.UML.umllex import render_lifeline Lifeline.render = render_lifeline del render_lifeline PK!FY((gaphor/UML/umlfmt.py""" Formatting of UML elements like attributes, operations, stereotypes, etc. """ import io import re from simplegeneric import generic from gaphor.UML import uml2 as UML @generic def format(el, pattern=None): """ Format an UML element. """ raise NotImplementedError( "Format routine for type %s not implemented yet" % type(el) ) @format.when_type(UML.Property) def format_property(el, pattern=None, *args, **kwargs): """ Format property or an association end. """ if el.association and not (args or kwargs): return format_association_end(el) else: return format_attribute(el, *args, **kwargs) def compile(regex): return re.compile(regex, re.MULTILINE | re.S) # Do not render if the name still contains a visibility element no_render_pat = compile(r"^\s*[+#-]") vis_map = {"public": "+", "protected": "#", "package": "~", "private": "-"} def format_attribute( el, visibility=False, is_derived=False, type=False, multiplicity=False, default=False, tags=False, ): """ Create a OCL representation of the attribute, Returns the attribute as a string. If one or more of the parameters (visibility, is_derived, type, multiplicity, default and/or tags) is set, only that field is rendered. Note that the name of the attribute is always rendered, so a parseable string is returned. Note that, when some of those parameters are set, parsing the string will not give you the same result. """ name = el.name if not name: name = "" if no_render_pat.match(name): name = "" # Render all fields if they all are set to False if not (visibility or is_derived or type or multiplicity or default): visibility = is_derived = type = multiplicity = default = True s = io.StringIO() if visibility: s.write(vis_map[el.visibility]) s.write(" ") if is_derived: if el.isDerived: s.write("/") s.write(name) if type and el.typeValue: s.write(": %s" % el.typeValue) if multiplicity and el.upperValue: if el.lowerValue: s.write("[%s..%s]" % (el.lowerValue, el.upperValue)) else: s.write("[%s]" % el.upperValue) if default and el.defaultValue: s.write(" = %s" % el.defaultValue) if tags: slots = [] for slot in el.appliedStereotype[:].slot: if slot: slots.append("%s=%s" % (slot.definingFeature.name, slot.value)) if slots: s.write(" { %s }" % ", ".join(slots)) s.seek(0) return s.read() def format_association_end(el): """ Format association end. """ name = "" n = io.StringIO() if el.name: n.write(vis_map[el.visibility]) n.write(" ") if el.isDerived: n.write("/") if el.name: n.write(el.name) n.seek(0) name = n.read() m = io.StringIO() if el.upperValue: if el.lowerValue: m.write("%s..%s" % (el.lowerValue, el.upperValue)) else: m.write("%s" % el.upperValue) slots = [] for slot in el.appliedStereotype[:].slot: if slot: slots.append("%s=%s" % (slot.definingFeature.name, slot.value)) if slots: m.write(" { %s }" % ",\n".join(slots)) m.seek(0) mult = m.read() return name, mult @format.when_type(UML.Operation) def format_operation( el, pattern=None, visibility=False, type=False, multiplicity=False, default=False, tags=False, direction=False, ): """ Create a OCL representation of the operation, Returns the operation as a string. """ name = el.name if not name: return "" if no_render_pat.match(name): return name # Render all fields if they all are set to False if not (visibility or type or multiplicity or default or tags or direction): visibility = type = multiplicity = default = tags = direction = True s = io.StringIO() if visibility: s.write(vis_map[el.visibility]) s.write(" ") s.write(name) s.write("(") for p in el.formalParameter: if direction: s.write(p.direction) s.write(" ") s.write(p.name) if type and p.typeValue: s.write(": %s" % p.typeValue) if multiplicity and p.upperValue: if p.lowerValue: s.write("[%s..%s]" % (p.lowerValue, p.upperValue)) else: s.write("[%s]" % p.upperValue) if default and p.defaultValue: s.write(" = %s" % p.defaultValue) # if p.taggedValue: # tvs = ', '.join(filter(None, map(getattr, p.taggedValue, # ['value'] * len(p.taggedValue)))) # s.write(' { %s }' % tvs) if p is not el.formalParameter[-1]: s.write(", ") s.write(")") rr = el.returnResult and el.returnResult[0] if rr: if type and rr.typeValue: s.write(": %s" % rr.typeValue) if multiplicity and rr.upperValue: if rr.lowerValue: s.write("[%s..%s]" % (rr.lowerValue, rr.upperValue)) else: s.write("[%s]" % rr.upperValue) # if rr.taggedValue: # tvs = ', '.join(filter(None, map(getattr, rr.taggedValue, # ['value'] * len(rr.taggedValue)))) # if tvs: # s.write(' { %s }' % tvs) s.seek(0) return s.read() @format.when_type(UML.Slot) def format_slot(el, pattern=None): return '%s = "%s"' % (el.definingFeature.name, el.value) @format.when_type(UML.NamedElement) def format_namedelement(el, pattern="%s"): """ Format named element. """ return pattern % el.name PK!G&&gaphor/UML/umllex.py""" Lexical analizer for attributes and operations. In this module some parse functions are added for attributes and operations. The regular expressions are constructed based on a series of "sub-patterns". This makes it easy to identify the autonomy of an attribute/operation. """ __all__ = ["parse_property", "parse_operation"] import re from simplegeneric import generic from gaphor.UML.uml2 import Property, NamedElement, Operation, Parameter @generic def parse(el, text): """ Parser for an UML element. """ raise NotImplementedError( "Parsing routine for type %s not implemented yet" % type(el) ) # Visibility (optional) ::= '+' | '-' | '#' vis_subpat = r"\s*(?P[-+#])?" # Derived value (optional) ::= [/] derived_subpat = r"\s*(?P/)?" # name (required) ::= name name_subpat = r"\s*(?P[a-zA-Z_]\w*)" # Multiplicity (added to type_subpat) ::= '[' [mult_l ..] mult_u ']' mult_subpat = r"\s*(\[\s*((?P[0-9]+)\s*\.\.)?\s*(?P([0-9]+|\*))\s*\])?" multa_subpat = r"\s*(\[?((?P[0-9]+)\s*\.\.)?\s*(?P([0-9]+|\*))\]?)?" # Type and multiplicity (optional) ::= ':' type [mult] type_subpat = r"\s*(:\s*(?P\w+)\s*" + mult_subpat + r")?" # default value (optional) ::= '=' default default_subpat = r"\s*(=\s*(?P\S+))?" # tagged values (optional) ::= '{' tags '}' tags_subpat = r"\s*(\{\s*(?P.*?)\s*\})?" # Parameters (required) ::= '(' [params] ')' params_subpat = r"\s*\(\s*(?P[^)]+)?\)" # Possible other parameters (optional) ::= ',' rest rest_subpat = r"\s*(,\s*(?P.*))?" # Direction of a parameter (optional, default in) ::= 'in' | 'out' | 'inout' dir_subpat = r"\s*((?Pin|out|inout)\s)?" # Some trailing garbage => no valid syntax... garbage_subpat = r"\s*(?P.*)" def compile(regex): return re.compile(regex, re.MULTILINE | re.S) # Attribute: # [+-#] [/] name [: type[\[mult\]]] [= default] [{ tagged values }] attribute_pat = compile( r"^" + vis_subpat + derived_subpat + name_subpat + type_subpat + default_subpat + tags_subpat + garbage_subpat ) # Association end name: # [[+-#] [/] name [\[mult\]]] [{ tagged values }] association_end_name_pat = compile( r"^" + "(" + vis_subpat + derived_subpat + name_subpat + mult_subpat + ")?" + tags_subpat + garbage_subpat ) # Association end multiplicity: # [mult] [{ tagged values }] association_end_mult_pat = compile(r"^" + multa_subpat + tags_subpat + garbage_subpat) # Operation: # [+|-|#] name ([parameters]) [: type[\[mult\]]] [{ tagged values }] operation_pat = compile( r"^" + vis_subpat + name_subpat + params_subpat + type_subpat + tags_subpat + garbage_subpat ) # One parameter supplied with an operation: # [in|out|inout] name [: type[\[mult\]] [{ tagged values }] parameter_pat = compile( r"^" + dir_subpat + name_subpat + type_subpat + default_subpat + tags_subpat + rest_subpat ) # Lifeline: # [name] [: type] lifeline_pat = compile("^" + name_subpat + type_subpat + garbage_subpat) def _set_visibility(el, vis): if vis == "+": el.visibility = "public" elif vis == "#": el.visibility = "protected" elif vis == "~": el.visibility = "package" elif vis == "-": el.visibility = "private" else: try: del el.visibility except AttributeError: pass def parse_attribute(el, s): """ Parse string s in the property. Tagged values, multiplicity and stuff like that is altered to reflect the data in the property string. """ m = attribute_pat.match(s) if not m or m.group("garbage"): el.name = s del el.visibility del el.isDerived if el.typeValue: el.typeValue = None if el.lowerValue: el.lowerValue = None if el.upperValue: el.upperValue = None if el.defaultValue: el.defaultValue = None else: g = m.group create = el._factory.create _set_visibility(el, g("vis")) el.isDerived = g("derived") and True or False el.name = g("name") el.typeValue = g("type") el.lowerValue = g("mult_l") el.upperValue = g("mult_u") el.defaultValue = g("default") # Skip tags: should do something with stereotypes? # tags = g('tags') # if tags: # for t in map(str.strip, tags.split(',')): # tv = create(UML.LiteralSpecification) # tv.value = t # el.taggedValue = tv def parse_association_end(el, s): """ Parse the text at one end of an association. The association end holds two strings. It is automatically figured out which string is fed to the parser. """ create = el._factory.create # if no name, then clear as there could be some garbage # due to previous parsing (i.e. '[1' m = association_end_name_pat.match(s) if m and not m.group("name"): el.name = None # clear also multiplicity if no characters in ``s`` m = association_end_mult_pat.match(s) if m and not m.group("mult_u"): if el.upperValue: el.upperValue = None if m and m.group("mult_u") or m.group("tags"): g = m.group el.lowerValue = g("mult_l") el.upperValue = g("mult_u") # tags = g('tags') # if tags: # for t in map(str.strip, tags.split(',')): # tv = create(UML.LiteralSpecification) # tv.value = t # el.taggedValue = tv else: m = association_end_name_pat.match(s) g = m.group if g("garbage"): el.name = s del el.visibility del el.isDerived else: _set_visibility(el, g("vis")) el.isDerived = g("derived") and True or False el.name = g("name") # Optionally, the multiplicity and tagged values may be defined: if g("mult_l"): el.lowerValue = g("mult_l") if g("mult_u"): if not g("mult_l"): el.lowerValue = None el.upperValue = g("mult_u") # tags = g('tags') # if tags: # while el.taggedValue: # el.taggedValue[0].unlink() # for t in map(str.strip, tags.split(',')): # tv = create(UML.LiteralSpecification) # tv.value = t # el.taggedValue = tv @parse.when_type(Property) def parse_property(el, s): if el.association: parse_association_end(el, s) else: parse_attribute(el, s) @parse.when_type(Operation) def parse_operation(el, s): """ Parse string s in the operation. Tagged values, parameters and visibility is altered to reflect the data in the operation string. """ m = operation_pat.match(s) if not m or m.group("garbage"): el.name = s del el.visibility list(map(Parameter.unlink, list(el.returnResult))) list(map(Parameter.unlink, list(el.formalParameter))) else: g = m.group create = el._factory.create _set_visibility(el, g("vis")) el.name = g("name") if not el.returnResult: el.returnResult = create(Parameter) p = el.returnResult[0] p.direction = "return" p.typeValue = g("type") p.lowerValue = g("mult_l") p.upperValue = g("mult_u") # FIXME: Maybe add to Operation.ownedRule? # tags = g('tags') # if tags: # for t in map(str.strip, tags.split(',')): # tv = create(UML.LiteralSpecification) # tv.value = t # p.taggedValue = tv pindex = 0 params = g("params") while params: m = parameter_pat.match(params) if not m: break g = m.group try: p = el.formalParameter[pindex] except IndexError: p = create(Parameter) p.direction = g("dir") or "in" p.name = g("name") p.typeValue = g("type") p.lowerValue = g("mult_l") p.upperValue = g("mult_u") p.defaultValue = g("default") # tags = g('tags') # if tags: # for t in map(str.strip, tags.split(',')): # tv = create(UML.LiteralSpecification) # tv.value = t # p.taggedValue = tv el.formalParameter = p # Do the next parameter: params = g("rest") pindex += 1 # Remove remaining parameters: for fp in el.formalParameter[pindex:]: fp.unlink() def parse_lifeline(el, s): """ Parse string s in a lifeline. If a class is defined and can be found in the datamodel, then a class is connected to the lifelines 'represents' property. """ m = lifeline_pat.match(s) g = m.group if not m or g("garbage"): el.name = s if hasattr(el, "represents"): del el.represents else: el.name = g("name") + ": " t = g("type") if t: el.name += ": " + t # In the near future the data model should be extended with # Lifeline.represents: ConnectableElement def render_lifeline(el): """ """ return el.name @parse.when_type(NamedElement) def parse_namedelement(el, text): """ Parse named element by simply assigning text to its name. """ el.name = text PK!y gaphor/__init__.py#!/usr/bin/env python """Gaphor is the simple modeling tool written in Python. This module allows Gaphor to be launched from the command line. The main() function sets up the command-line options and arguments and passes them to the main Application instance. """ # Examples of valid version strings # __version__ = '1.2.3.dev1' # Development release 1 # __version__ = '1.2.3a1' # Alpha Release 1 # __version__ = '1.2.3b1' # Beta Release 1 # __version__ = '1.2.3rc1' # RC Release 1 # __version__ = '1.2.3' # Final Release # __version__ = '1.2.3.post1' # Post Release 1 __version__ = "1.0.0" __all__ = ["main"] from optparse import OptionParser import logging import gi from gaphor.application import Application gi.require_version("Gtk", "3.0") LOG_FORMAT = "%(name)s %(levelname)s %(message)s" def launch(model=None): """Start the main application by initiating and running Application. The file_manager service is used here to load a Gaphor model if one was specified on the command line. Otherwise, a new model is created and the Gaphor GUI is started.""" # Make sure gui is loaded ASAP. # This prevents menu items from appearing at unwanted places. Application.essential_services.append("main_window") Application.init() main_window = Application.get_service("main_window") main_window.open() file_manager = Application.get_service("file_manager") if model: file_manager.load(model) else: file_manager.action_new() Application.run() def main(): """Start Gaphor from the command line. This function creates an option parser for retrieving arguments and options from the command line. This includes a Gaphor model to load. The application is then initialized, passing along the option parser. This provides plugins and services with access to the command line options and may add their own.""" parser = OptionParser() parser.add_option("-p", "--profiler", action="store_true", help="Run in profiler") parser.add_option( "-q", "--quiet", dest="quiet", help="Quiet output", default=False, action="store_true", ) parser.add_option( "-v", "--verbose", dest="verbose", help="Verbose output", default=False, action="store_true", ) options, args = parser.parse_args() if options.verbose: logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) elif options.quiet: logging.basicConfig(level=logging.WARNING, format=LOG_FORMAT) else: logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) try: model = args[0] except IndexError: model = None if options.profiler: import cProfile import pstats cProfile.run("import gaphor; gaphor.launch()", "gaphor.prof") profile_stats = pstats.Stats("gaphor.prof") profile_stats.strip_dirs().sort_stats("time").print_stats(50) else: launch(model) PK! @@gaphor/__main__.pyfrom gaphor import main if __name__ == "__main__": main() PK!Nܹgaphor/action.py"""Support for actions in generic files. See also gaphor/service/actionmanager.py for the management module. """ from gaphor.application import Application class action(object): """ Decorator. Turns a regular function (/method) into a full blown Action class. >>> class A(object): ... @action(name="my_action", label="my action") ... def myaction(self): ... print('action called') >>> a = A() >>> a.myaction() action called >>> is_action(a.myaction) True >>> for method in dir(A): ... if is_action(getattr(A, method, None)): ... print(method) myaction >>> A.myaction.__action__.name 'my_action' >>> A.myaction.__action__.label 'my action' """ def __init__( self, name, label=None, tooltip=None, stock_id=None, accel=None, **kwargs ): self.name = name self.label = label self.tooltip = tooltip self.stock_id = stock_id self.accel = accel self.__dict__.update(kwargs) def __call__(self, func): func.__action__ = self return func class toggle_action(action): """ A toggle button can be switched on and off. An extra 'active' attribute is provided than gives the initial status. """ def __init__( self, name, label=None, tooltip=None, stock_id=None, accel=None, active=False ): super(toggle_action, self).__init__( name, label, tooltip, stock_id, accel=accel, active=active ) class radio_action(action): """ Radio buttons take a list of names, a list of labels and a list of tooltips (and optionally, a list of stock_ids). The callback function should have an extra value property, which is given the index number of the activated radio button action. """ def __init__( self, names, labels=None, tooltips=None, stock_ids=None, accels=None, active=0 ): super(radio_action, self).__init__( names[0], names=names, labels=labels, tooltips=tooltips, stock_ids=stock_ids, accels=accels, active=active, ) def is_action(func): return bool(getattr(func, "__action__", False)) def build_action_group(obj, name=None): """ Build actions and a Gtk.ActionGroup for each Action instance found in obj() (that's why Action is a class ;) ). This function requires GTK+. >>> class A(object): ... @action(name='bar') ... def bar(self): print('Say bar') ... @toggle_action(name='foo') ... def foo(self, active): print('Say foo', active) ... @radio_action(names=('baz', 'beer'), labels=('Baz', 'Beer')) ... def baz(self, value): ... print('Say', value, (value and "beer" or "baz")) >>> group = build_action_group(A()) Say 0 baz >>> len(group.list_actions()) 4 >>> a = group.get_action('bar') >>> a.activate() Say bar >>> group.get_action('foo').activate() Say foo True >>> group.get_action('beer').activate() Say 1 beer >>> group.get_action('baz').activate() Say 0 baz """ from gi.repository import Gtk group = Gtk.ActionGroup.new(name or str(obj)) objtype = type(obj) for attrname in dir(obj): try: # Fetch the methods from the object's type instead of the object # itself. This prevents some descriptors (mainly gaphor.core.inject) # from executing. # Otherwise stuff like dependency resolving (=inject) may kick in # too early. method = getattr(objtype, attrname) except: continue act = getattr(method, "__action__", None) if isinstance(act, radio_action): actgroup = None if not act.labels: act.labels = [None] * len(act.names) if not act.tooltips: act.tooltips = [None] * len(act.names) if not act.stock_ids: act.stock_ids = [None] * len(act.names) if not act.accels: act.accels = [None] * len(act.names) assert len(act.names) == len(act.labels) assert len(act.names) == len(act.tooltips) assert len(act.names) == len(act.stock_ids) assert len(act.names) == len(act.accels) for i, n in enumerate(act.names): gtkact = Gtk.RadioAction.new( n, act.labels[i], act.tooltips[i], act.stock_ids[i], value=i ) if not actgroup: actgroup = gtkact else: gtkact.props.group = actgroup group.add_action_with_accel(gtkact, act.accels[i]) actgroup.connect("changed", _radio_action_changed, obj, attrname) actgroup.set_current_value(act.active) elif isinstance(act, toggle_action): gtkact = Gtk.ToggleAction.new( act.name, act.label, act.tooltip, act.stock_id ) gtkact.set_property("active", act.active) gtkact.connect("activate", _toggle_action_activate, obj, attrname) group.add_action_with_accel(gtkact, act.accel) elif isinstance(act, action): gtkact = Gtk.Action.new(act.name, act.label, act.tooltip, act.stock_id) gtkact.connect("activate", _action_activate, obj, attrname) group.add_action_with_accel(gtkact, act.accel) elif act is not None: raise TypeError("Invalid action type: %s" % action) return group def _action_activate(action, obj, name): method = getattr(obj, name) method() def _toggle_action_activate(action, obj, name): method = getattr(obj, name) method(action.props.active) def _radio_action_changed(action, current_action, obj, name): method = getattr(obj, name) method(current_action.props.value) if __name__ == "__main__": import doctest doctest.testmod() PK!sgaphor/adapters/__init__.pyimport gaphor.adapters.connectors import gaphor.adapters.editors import gaphor.adapters.grouping import gaphor.adapters.actions.flowconnect import gaphor.adapters.actions.partitionpage import gaphor.adapters.classes.classconnect import gaphor.adapters.classes.interfaceconnect import gaphor.adapters.components.connectorconnect import gaphor.adapters.interactions.messageconnect import gaphor.adapters.profiles.extensionconnect import gaphor.adapters.profiles.stereotypepage import gaphor.adapters.profiles.metaclasseditor import gaphor.adapters.usecases.usecaseconnect import gaphor.adapters.states.vertexconnect import gaphor.adapters.states.propertypages PK!#gaphor/adapters/actions/__init__.pyPK!k B&gaphor/adapters/actions/flowconnect.py""" Flow item adapter connections. """ import logging from zope import component from gaphor import UML from gaphor.adapters.connectors import UnaryRelationshipConnect from gaphor.diagram import items from gaphor.diagram.interfaces import IConnect log = logging.getLogger(__name__) class FlowConnect(UnaryRelationshipConnect): """ Connect FlowItem and Action/ObjectNode, initial/final nodes. """ def allow(self, handle, port): line = self.line subject = self.element.subject if ( handle is line.head and isinstance(subject, UML.FinalNode) or handle is line.tail and isinstance(subject, UML.InitialNode) ): return None return super(FlowConnect, self).allow(handle, port) def reconnect(self, handle, port): line = self.line log.debug("Reconnection of %s (guard %s)" % (line.subject, line.subject.guard)) old_flow = line.subject # Secure properties before old_flow is removed: name = old_flow.name guard_value = old_flow.guard self.connect_subject(handle) relation = line.subject if old_flow: relation.name = name if guard_value: relation.guard = guard_value log.debug("unlinking old flow instance %s" % old_flow) # old_flow.unlink() def connect_subject(self, handle): line = self.line element = self.element # TODO: connect opposite side again (in case it's a join/fork or # decision/merge node) c1 = self.get_connected(line.head) c2 = self.get_connected(line.tail) if isinstance(c1, items.ObjectNodeItem) or isinstance(c2, items.ObjectNodeItem): relation = self.relationship_or_new( UML.ObjectFlow, UML.ObjectFlow.source, UML.ObjectFlow.target ) else: relation = self.relationship_or_new( UML.ControlFlow, UML.ControlFlow.source, UML.ControlFlow.target ) line.subject = relation opposite = line.opposite(handle) otc = self.get_connected(opposite) if opposite and isinstance(otc, (items.ForkNodeItem, items.DecisionNodeItem)): adapter = component.queryMultiAdapter((otc, line), IConnect) adapter.combine_nodes() def disconnect_subject(self, handle): log.debug("Performing disconnect for handle %s" % handle) super(FlowConnect, self).disconnect_subject(handle) line = self.line opposite = line.opposite(handle) otc = self.get_connected(opposite) if opposite and isinstance(otc, (items.ForkNodeItem, items.DecisionNodeItem)): adapter = component.queryMultiAdapter((otc, line), IConnect) adapter.decombine_nodes() component.provideAdapter(factory=FlowConnect, adapts=(items.ActionItem, items.FlowItem)) component.provideAdapter( factory=FlowConnect, adapts=(items.ActivityNodeItem, items.FlowItem) ) component.provideAdapter( factory=FlowConnect, adapts=(items.ObjectNodeItem, items.FlowItem) ) component.provideAdapter( factory=FlowConnect, adapts=(items.SendSignalActionItem, items.FlowItem) ) component.provideAdapter( factory=FlowConnect, adapts=(items.AcceptEventActionItem, items.FlowItem) ) class FlowForkDecisionNodeConnect(FlowConnect): """ Abstract class with common behaviour for Fork/Join node and Decision/Merge node. """ def allow(self, handle, port): # No cyclic connect is possible on a Flow/Decision node: head, tail = self.line.head, self.line.tail subject = self.element.subject hct = self.get_connected(head) tct = self.get_connected(tail) if ( handle is head and tct and tct.subject is subject or handle is tail and hct and hct.subject is subject ): return None return super(FlowForkDecisionNodeConnect, self).allow(handle, port) def combine_nodes(self): """ Combine join/fork or decision/merge nodes into one diagram item. """ fork_node_cls = self.fork_node_cls join_node_cls = self.join_node_cls line = self.line element = self.element subject = element.subject if len(subject.incoming) > 1 and len(subject.outgoing) < 2: self.element_factory.swap_element(subject, join_node_cls) element.request_update() elif len(subject.incoming) < 2 and len(subject.outgoing) > 1: self.element_factory.swap_element(subject, fork_node_cls) element.request_update() elif ( not element.combined and len(subject.incoming) > 1 and len(subject.outgoing) > 1 ): join_node = subject # determine flow class: if [f for f in join_node.incoming if isinstance(f, UML.ObjectFlow)]: flow_class = UML.ObjectFlow else: flow_class = UML.ControlFlow self.element_factory.swap_element(join_node, join_node_cls) fork_node = self.element_factory.create(fork_node_cls) for flow in list(join_node.outgoing): flow.source = fork_node flow = self.element_factory.create(flow_class) flow.source = join_node flow.target = fork_node element.combined = fork_node def decombine_nodes(self): """ Decombine join/fork or decision/merge nodes. """ fork_node_cls = self.fork_node_cls join_node_cls = self.join_node_cls line = self.line element = self.element if element.combined: join_node = element.subject cflow = join_node.outgoing[0] # combining flow fork_node = cflow.target assert fork_node is element.combined assert isinstance(join_node, join_node_cls) assert isinstance(fork_node, fork_node_cls) if len(join_node.incoming) < 2 or len(fork_node.outgoing) < 2: # Move all outgoing edges to the first node (the join node): for f in list(fork_node.outgoing): f.source = join_node cflow.unlink() fork_node.unlink() # swap subject to fork node if outgoing > 1 if len(join_node.outgoing) > 1: assert len(join_node.incoming) < 2 self.element_factory.swap_element(join_node, fork_node_cls) element.combined = None def connect_subject(self, handle): """ In addition to a subject connect, the subject of the element may be changed. For readability, parameters are named after the classes used by Join/Fork nodes. """ super(FlowForkDecisionNodeConnect, self).connect_subject(handle) # Switch class for self.element Join/Fork depending on the number # of incoming/outgoing edges. self.combine_nodes() def disconnect_subject(self, handle): super(FlowForkDecisionNodeConnect, self).disconnect_subject(handle) if self.element.combined: self.decombine_nodes() @component.adapter(items.ForkNodeItem, items.FlowItem) class FlowForkNodeConnect(FlowForkDecisionNodeConnect): """Connect Flow to a ForkNode.""" fork_node_cls = UML.ForkNode join_node_cls = UML.JoinNode component.provideAdapter(FlowForkNodeConnect) @component.adapter(items.DecisionNodeItem, items.FlowItem) class FlowDecisionNodeConnect(FlowForkDecisionNodeConnect): """Connect Flow to a DecisionNode.""" fork_node_cls = UML.DecisionNode join_node_cls = UML.MergeNode component.provideAdapter(FlowDecisionNodeConnect) PK!P3ii(gaphor/adapters/actions/partitionpage.py"""Activity partition property page.""" from zope import component from gi.repository import Gtk from zope.interface import implementer from gaphor import UML from gaphor.adapters.propertypages import NamedItemPropertyPage from gaphor.core import _, inject, transactional from gaphor.diagram import items from gaphor.ui.interfaces import IPropertyPage @implementer(IPropertyPage) @component.adapter(items.PartitionItem) class PartitionPropertyPage(NamedItemPropertyPage): """Partition property page.""" element_factory = inject("element_factory") def construct(self): item = self.item page = super(PartitionPropertyPage, self).construct() if item.subject: if not item._toplevel: hbox = Gtk.HBox(spacing=12) button = Gtk.CheckButton(_("External")) button.set_active(item.subject.isExternal) button.connect("toggled", self._on_external_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) else: pass # hbox = Gtk.HBox(spacing=12) # button = Gtk.CheckButton(_('Dimension')) # button.set_active(item.subject.isDimension) # button.connect('toggled', self._on_dimension_change) return page @transactional def _on_external_change(self, button): item = self.item if item.subject: item.subject.isExternal = button.get_active() item.request_update() component.provideAdapter(PartitionPropertyPage, name="Properties") PK!)gaphor/adapters/actions/tests/__init__.pyPK!'0C0C*gaphor/adapters/actions/tests/test_flow.py""" Flow item connection adapters tests. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class FlowItemBasicNodesConnectionTestCase(TestCase): """ Tests for flow item connecting to basic activity nodes. """ def test_initial_node_glue(self): """Test flow item gluing to initial node item.""" flow = self.create(items.FlowItem) node = self.create(items.InitialNodeItem, UML.InitialNode) # tail may not connect to initial node item allowed = self.allow(flow, flow.tail, node) self.assertFalse(allowed) allowed = self.allow(flow, flow.head, node) self.assertTrue(allowed) def test_flow_final_node_glue(self): """Test flow item gluing to flow final node item.""" flow = self.create(items.FlowItem) node = self.create(items.FlowFinalNodeItem, UML.FlowFinalNode) # head may not connect to flow final node item allowed = self.allow(flow, flow.head, node) self.assertFalse(allowed) allowed = self.allow(flow, flow.tail, node) self.assertTrue(allowed) def test_activity_final_node_glue(self): """Test flow item gluing to activity final node item """ flow = self.create(items.FlowItem) node = self.create(items.ActivityFinalNodeItem, UML.ActivityFinalNode) # head may not connect to activity final node item glued = self.allow(flow, flow.head, node) self.assertFalse(glued) glued = self.allow(flow, flow.tail, node) self.assertTrue(glued) class FlowItemObjectNodeTestCase(TestCase): """ Flow item connecting to object node item tests. """ def test_glue(self): """Test gluing to object node.""" flow = self.create(items.FlowItem) onode = self.create(items.ObjectNodeItem, UML.ObjectNode) glued = self.allow(flow, flow.head, onode) self.assertTrue(glued) def test_connection(self): """Test connection to object node """ flow = self.create(items.FlowItem) anode = self.create(items.ActionItem, UML.Action) onode = self.create(items.ObjectNodeItem, UML.ObjectNode) self.connect(flow, flow.head, anode) self.connect(flow, flow.tail, onode) self.assertTrue(flow.subject) self.assertTrue(isinstance(flow.subject, UML.ObjectFlow)) self.disconnect(flow, flow.head) self.disconnect(flow, flow.tail) # opposite connection self.connect(flow, flow.head, onode) self.connect(flow, flow.tail, anode) self.assertTrue(flow.subject) self.assertTrue(isinstance(flow.subject, UML.ObjectFlow)) def test_reconnection(self): """Test object flow reconnection """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) o1 = self.create(items.ObjectNodeItem, UML.ObjectNode) o2 = self.create(items.ObjectNodeItem, UML.ObjectNode) # connect: a1 -> o1 self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, o1) f = flow.subject f.name = "tname" f.guard = "tguard" # reconnect: a1 -> o2 self.connect(flow, flow.tail, o2) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(1, len(a1.subject.outgoing)) # no connections to o1 self.assertEqual(0, len(o1.subject.incoming)) self.assertEqual(0, len(o1.subject.outgoing)) # connections to o2 instead self.assertEqual(1, len(o2.subject.incoming)) self.assertEqual(0, len(o2.subject.outgoing)) self.assertEqual(1, len(self.kindof(UML.ObjectFlow))) # one guard self.assertEqual("tname", flow.subject.name) self.assertEqual("tguard", flow.subject.guard) def test_control_flow_reconnection(self): """Test control flow becoming object flow due to reconnection """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) o1 = self.create(items.ObjectNodeItem, UML.ObjectNode) # connect with control flow: a1 -> a2 self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, a2) f = flow.subject f.name = "tname" f.guard = "tguard" # reconnect with object flow: a1 -> o1 self.connect(flow, flow.tail, o1) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(1, len(a1.subject.outgoing)) # no connections to a2 self.assertEqual(0, len(a2.subject.incoming)) self.assertEqual(0, len(a2.subject.outgoing)) # connections to o1 instead self.assertEqual(1, len(o1.subject.incoming)) self.assertEqual(0, len(o1.subject.outgoing)) self.assertEqual(0, len(self.kindof(UML.ControlFlow))) self.assertEqual(1, len(self.kindof(UML.ObjectFlow))) # one guard, not changed self.assertEqual("tname", flow.subject.name) self.assertEqual("tguard", flow.subject.guard) class FlowItemActionTestCase(TestCase): """ Flow item connecting to action item tests. """ def test_glue(self): """Test flow item gluing to action items.""" flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) glued = self.allow(flow, flow.head, a1) self.assertTrue(glued) self.connect(flow, flow.head, a1) glued = self.allow(flow, flow.tail, a2) self.assertTrue(glued) def test_connect(self): """Test flow item connecting to action items """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, a2) self.assertTrue(isinstance(flow.subject, UML.ControlFlow)) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(1, len(a2.subject.incoming)) self.assertEqual(1, len(a1.subject.outgoing)) self.assertEqual(0, len(a2.subject.outgoing)) self.assertTrue(flow.subject in a1.subject.outgoing) self.assertTrue(flow.subject.source is a1.subject) self.assertTrue(flow.subject in a2.subject.incoming) self.assertTrue(flow.subject.target is a2.subject) def test_disconnect(self): """Test flow item disconnection from action items """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, a2) self.disconnect(flow, flow.head) self.assertTrue(flow.subject is None) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(0, len(a2.subject.incoming)) self.assertEqual(0, len(a1.subject.outgoing)) self.assertEqual(0, len(a2.subject.outgoing)) def test_reconnect(self): """Test flow item reconnection """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) a3 = self.create(items.ActionItem, UML.Action) # a1 -> a2 self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, a2) f = flow.subject f.name = "tname" f.guard = "tguard" # reconnect: a1 -> a3 self.connect(flow, flow.tail, a3) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(1, len(a1.subject.outgoing)) # no connections to a2 self.assertEqual(0, len(a2.subject.incoming)) self.assertEqual(0, len(a2.subject.outgoing)) # connections to a3 instead self.assertEqual(1, len(a3.subject.incoming)) self.assertEqual(0, len(a3.subject.outgoing)) self.assertEqual(1, len(self.kindof(UML.ControlFlow))) # one guard self.assertEqual("tname", flow.subject.name) self.assertEqual("tguard", flow.subject.guard) def test_object_flow_reconnection(self): """Test object flow becoming control flow due to reconnection """ flow = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) o1 = self.create(items.ObjectNodeItem, UML.ObjectNode) # connect with control flow: a1 -> o1 self.connect(flow, flow.head, a1) self.connect(flow, flow.tail, o1) f = flow.subject f.name = "tname" f.guard = "tguard" # reconnect with object flow: a1 -> a2 self.connect(flow, flow.tail, a2) self.assertEqual(0, len(a1.subject.incoming)) self.assertEqual(1, len(a1.subject.outgoing)) # no connections to o1 self.assertEqual(0, len(o1.subject.incoming)) self.assertEqual(0, len(o1.subject.outgoing)) # connections to a2 instead self.assertEqual(1, len(a2.subject.incoming)) self.assertEqual(0, len(a2.subject.outgoing)) self.assertEqual(1, len(self.kindof(UML.ControlFlow))) self.assertEqual(0, len(self.kindof(UML.ObjectFlow))) # one guard, not changed self.assertEqual("tname", flow.subject.name) self.assertEqual("tguard", flow.subject.guard) class FlowItemDesisionAndForkNodes(object): """ Base class for flow connecting to decision and fork nodes. See `FlowItemDecisionNodeTestCase` and `FlowItemForkNodeTestCase` test cases. Not tested yet - If a join node has an incoming object flow, it must have an outgoing object flow, otherwise, it must have an outgoing control flow. - The edges coming into and out of a fork node must be either all object flows or all control flows. """ item_cls = None fork_node_cls = None join_node_cls = None def test_glue(self): """Test decision/fork nodes glue """ flow = self.create(items.FlowItem) action = self.create(items.ActionItem, UML.Action) node = self.create(self.item_cls, self.join_node_cls) glued = self.allow(flow, flow.head, node) self.assertTrue(glued) self.connect(flow, flow.head, action) glued = self.allow(flow, flow.tail, node) self.assertTrue(glued) def test_node_class_change(self): """ Test node incoming edges Connection scheme is presented below:: head tail [ a1 ]--flow1--> | [ jn ] --flow3--> [ a3 ] [ a2 ]--flow2--> | Node class changes due to two incoming edges and one outgoing edge. """ flow1 = self.create(items.FlowItem) flow2 = self.create(items.FlowItem) flow3 = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) a3 = self.create(items.ActionItem, UML.Action) jn = self.create(self.item_cls, self.fork_node_cls) assert isinstance(jn.subject, self.fork_node_cls) # connect actions first self.connect(flow1, flow1.head, a1) self.connect(flow2, flow2.head, a2) self.connect(flow3, flow3.tail, a2) # connect to the node self.connect(flow1, flow1.tail, jn) self.connect(flow2, flow2.tail, jn) self.connect(flow3, flow3.head, jn) # node class changes self.assertTrue(isinstance(jn.subject, self.join_node_cls)) def test_outgoing_edges(self): """Test outgoing edges Connection scheme is presented below:: head tail | --flow2-->[ a2 ] [ a1 ] --flow1--> [ jn ] | --flow3-->[ a3 ] """ flow1 = self.create(items.FlowItem) flow2 = self.create(items.FlowItem) flow3 = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) a3 = self.create(items.ActionItem, UML.Action) jn = self.create(self.item_cls, self.join_node_cls) # connect actions first self.connect(flow1, flow1.head, a1) self.connect(flow2, flow2.tail, a2) self.connect(flow3, flow3.tail, a2) # connect to the node self.connect(flow1, flow1.tail, jn) self.assertTrue(isinstance(jn.subject, self.join_node_cls)) self.connect(flow2, flow2.head, jn) self.assertTrue(isinstance(jn.subject, self.join_node_cls)) self.assertEqual(1, len(jn.subject.incoming)) self.assertEqual(1, len(jn.subject.outgoing)) self.assertTrue(flow1.subject in jn.subject.incoming) self.assertTrue(flow2.subject in jn.subject.outgoing) self.connect(flow3, flow3.head, jn) self.assertEqual(2, len(jn.subject.outgoing)) self.assertTrue(isinstance(jn.subject, self.fork_node_cls), "%s" % jn.subject) def test_combined_nodes_connection(self): """Test combined nodes connection Connection scheme is presented below:: head tail | --flow2--> [ a2 ] [ a1 ] --flow1--> [ jn ] [ a4 ] --flow4--> | --flow3--> [ a3 ] Flow `flow4` will force the node to become a combined node. """ flow1 = self.create(items.FlowItem) flow2 = self.create(items.FlowItem) flow3 = self.create(items.FlowItem) flow4 = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) a3 = self.create(items.ActionItem, UML.Action) a4 = self.create(items.ActionItem, UML.Action) jn = self.create(self.item_cls, self.join_node_cls) # connect actions first self.connect(flow1, flow1.head, a1) self.connect(flow2, flow2.tail, a2) self.connect(flow3, flow3.tail, a2) self.connect(flow4, flow4.head, a4) # connect to the node self.connect(flow1, flow1.tail, jn) self.connect(flow2, flow2.head, jn) self.connect(flow3, flow3.head, jn) self.connect(flow4, flow4.tail, jn) self.assertTrue(isinstance(jn.subject, self.join_node_cls)) self.assertTrue(jn.combined is not None) # check node combination self.assertTrue(1, len(jn.subject.outgoing)) self.assertTrue(1, len(jn.combined.incoming)) self.assertTrue(jn.subject.outgoing[0] is jn.combined.incoming[0]) def test_combined_node_disconnection(self): """Test combined nodes disconnection Connection scheme is presented below:: head tail | --flow2--> [ a2 ] [ a1 ] --flow1--> [ jn ] [ a4 ] --flow4--> | --flow3--> [ a3 ] Flow `flow4` will force the node to become a combined node. """ canvas = self.diagram.canvas flow1 = self.create(items.FlowItem) flow2 = self.create(items.FlowItem) flow3 = self.create(items.FlowItem) flow4 = self.create(items.FlowItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) a3 = self.create(items.ActionItem, UML.Action) a4 = self.create(items.ActionItem, UML.Action) jn = self.create(self.item_cls, self.join_node_cls) # connect actions first self.connect(flow1, flow1.head, a1) self.connect(flow2, flow2.tail, a2) self.connect(flow3, flow3.tail, a2) self.connect(flow4, flow4.head, a4) # connect to the node self.connect(flow1, flow1.tail, jn) self.connect(flow2, flow2.head, jn) self.connect(flow3, flow3.head, jn) self.connect(flow4, flow4.tail, jn) # needed for tests below cflow = jn.subject.outgoing[0] cnode = jn.combined assert cflow in self.kindof(UML.ControlFlow) assert cnode in self.kindof(self.fork_node_cls) # test disconnection self.disconnect(flow4, flow4.head) assert self.get_connected(flow4.head) is None self.assertTrue(jn.combined is None) flows = self.kindof(UML.ControlFlow) nodes = self.kindof(self.fork_node_cls) self.assertTrue(cnode not in nodes, "%s in %s" % (cnode, nodes)) self.assertTrue(cflow not in flows, "%s in %s" % (cflow, flows)) class FlowItemForkNodeTestCase(FlowItemDesisionAndForkNodes, TestCase): item_cls = items.ForkNodeItem fork_node_cls = UML.ForkNode join_node_cls = UML.JoinNode class FlowItemDecisionNodeTestCase(FlowItemDesisionAndForkNodes, TestCase): item_cls = items.DecisionNodeItem fork_node_cls = UML.DecisionNode join_node_cls = UML.MergeNode PK!#gaphor/adapters/classes/__init__.pyPK!)``'gaphor/adapters/classes/classconnect.py"""Classes related (dependency, implementation) adapter connections.""" import logging from zope import component from gaphor import UML from gaphor.adapters.connectors import UnaryRelationshipConnect, RelationshipConnect from gaphor.diagram import items log = logging.getLogger(__name__) @component.adapter(items.NamedItem, items.DependencyItem) class DependencyConnect(RelationshipConnect): """Connect two NamedItem elements using a Dependency.""" def allow(self, handle, port): line = self.line element = self.element # Element should be a NamedElement if not element.subject or not isinstance(element.subject, UML.NamedElement): return False return super(DependencyConnect, self).allow(handle, port) def reconnect(self, handle, port): line = self.line dep = line.subject if handle is line.head: for s in dep.supplier: del dep.supplier[s] elif handle is line.tail: for c in dep.client: del dep.client[c] self.reconnect_relationship( handle, line.dependency_type.supplier, line.dependency_type.client ) def connect_subject(self, handle): """ TODO: check for existing relationships (use self.relation()) """ line = self.line if line.auto_dependency: canvas = line.canvas opposite = line.opposite(handle) if handle is line.head: client = self.get_connected(opposite).subject supplier = self.element.subject else: client = self.element.subject supplier = self.get_connected(opposite).subject line.dependency_type = UML.model.dependency_type(client, supplier) relation = self.relationship_or_new( line.dependency_type, line.dependency_type.supplier, line.dependency_type.client, ) line.subject = relation component.provideAdapter(DependencyConnect) @component.adapter(items.ClassifierItem, items.GeneralizationItem) class GeneralizationConnect(RelationshipConnect): """Connect Classifiers with a Generalization relationship.""" def reconnect(self, handle, port): self.reconnect_relationship( handle, UML.Generalization.general, UML.Generalization.specific ) def connect_subject(self, handle): log.debug("connect_subject: %s" % (handle,)) relation = self.relationship_or_new( UML.Generalization, UML.Generalization.general, UML.Generalization.specific ) log.debug("found: %s" % (relation,)) self.line.subject = relation component.provideAdapter(GeneralizationConnect) @component.adapter(items.ClassifierItem, items.AssociationItem) class AssociationConnect(UnaryRelationshipConnect): """Connect association to classifier.""" def allow(self, handle, port): element = self.element # Element should be a Classifier if not isinstance(element.subject, UML.Classifier): return None return super(AssociationConnect, self).allow(handle, port) def connect_subject(self, handle): element = self.element line = self.line c1 = self.get_connected(line.head) c2 = self.get_connected(line.tail) if c1 and c2: head_type = c1.subject tail_type = c2.subject # First check if we do not already contain the right subject: if line.subject: end1 = line.subject.memberEnd[0] end2 = line.subject.memberEnd[1] if (end1.type is head_type and end2.type is tail_type) or ( end2.type is head_type and end1.type is tail_type ): return # Create new association relation = UML.model.create_association( self.element_factory, head_type, tail_type ) relation.package = element.canvas.diagram.namespace line.head_end.subject = relation.memberEnd[0] line.tail_end.subject = relation.memberEnd[1] # Do subject itself last, so event handlers can trigger line.subject = relation def reconnect(self, handle, port): line = self.line c = self.get_connected(handle) if handle is line.head: end = line.tail_end oend = line.head_end elif handle is line.tail: end = line.head_end oend = line.tail_end else: raise ValueError("Incorrect handle passed to adapter") nav = oend.subject.navigability UML.model.set_navigability(line.subject, end.subject, None) # clear old data oend.subject.type = c.subject UML.model.set_navigability(line.subject, oend.subject, nav) def disconnect_subject(self, handle): """ Disconnect model element. Disconnect property (memberEnd) too, in case of end of life for Extension """ opposite = self.line.opposite(handle) c1 = self.get_connected(handle) c2 = self.get_connected(opposite) if c1 and c2: old = self.line.subject del self.line.subject del self.line.head_end.subject del self.line.tail_end.subject if old and len(old.presentation) == 0: for e in list(old.memberEnd): e.unlink() old.unlink() component.provideAdapter(AssociationConnect) @component.adapter(items.NamedItem, items.ImplementationItem) class ImplementationConnect(RelationshipConnect): """Connect Interface and a BehavioredClassifier using an Implementation.""" def allow(self, handle, port): line = self.line element = self.element # Element at the head should be an Interface if handle is line.head and not isinstance(element.subject, UML.Interface): return None # Element at the tail should be a BehavioredClassifier if handle is line.tail and not isinstance( element.subject, UML.BehavioredClassifier ): return None return super(ImplementationConnect, self).allow(handle, port) def reconnect(self, handle, port): line = self.line impl = line.subject if handle is line.head: for s in impl.contract: del impl.contract[s] elif handle is line.tail: for c in impl.implementatingClassifier: del impl.implementatingClassifier[c] self.reconnect_relationship( handle, UML.Implementation.contract, UML.Implementation.implementatingClassifier, ) def connect_subject(self, handle): """ Perform implementation relationship connection. """ relation = self.relationship_or_new( UML.Implementation, UML.Implementation.contract, UML.Implementation.implementatingClassifier, ) self.line.subject = relation component.provideAdapter(ImplementationConnect) PK!7y +gaphor/adapters/classes/interfaceconnect.py""" Interface item related connections. The connectors implemented in this module check if connection is possible to folded interface, see `gaphor.diagram.classes.interface` documentation for details. """ from zope import component from zope import interface from gaphor import UML from gaphor.diagram import items from gaphor.adapters.classes.classconnect import ( DependencyConnect, ImplementationConnect, ) @component.adapter(items.InterfaceItem, items.ImplementationItem) class ImplementationInterfaceConnect(ImplementationConnect): """Connect interface item and a behaviored classifier using an implementation. """ def connect(self, handle, port): """ Implementation item can be changed to draw in solid mode, when connected to folded interface. """ super(ImplementationInterfaceConnect, self).connect(handle, port) if handle is self.line.head: self.line._solid = self.element.folded != self.element.FOLDED_NONE def disconnect(self, handle): """ If implementation item is no longer connected to an interface, then draw it in non-solid mode. """ super(ImplementationInterfaceConnect, self).disconnect(handle) if handle is self.line.head: self.line._solid = False component.provideAdapter(ImplementationInterfaceConnect) @component.adapter(items.InterfaceItem, items.DependencyItem) class DependencyInterfaceConnect(DependencyConnect): """Connect interface item with dependency item.""" def connect(self, handle, port): """ Dependency item is changed to draw in solid mode, when connected to folded interface. """ super(DependencyInterfaceConnect, self).connect(handle, port) line = self.line # connecting to the interface, which is supplier - assuming usage # dependency if handle is line.head: if self.element.folded != self.element.FOLDED_NONE: line._solid = True self.element.folded = self.element.FOLDED_REQUIRED # change interface angle even when it is unfolded, this way # required interface will be rotated properly when folded by # user self.element._angle = port.angle def disconnect(self, handle): """ If dependency item is no longer connected to an interface, then draw it in non-solid mode. Interface's folded mode changes to provided (ball) notation. """ super(DependencyInterfaceConnect, self).disconnect(handle) if handle is self.line.head: iface = self.element self.line._solid = False # don't change folding notation when interface is unfolded, see # test_unfolded_interface_disconnection as well if iface.folded: iface.folded = iface.FOLDED_PROVIDED component.provideAdapter(DependencyInterfaceConnect) PK!)gaphor/adapters/classes/tests/__init__.pyPK!WN00+gaphor/adapters/classes/tests/test_class.py""" Classes related adapter connection tests. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class DependencyTestCase(TestCase): """ Dependency item connection adapter tests. """ def test_dependency_glue(self): """Test dependency glue to two actor items """ actor1 = self.create(items.ActorItem, UML.Actor) actor2 = self.create(items.ActorItem, UML.Actor) dep = self.create(items.DependencyItem) glued = self.allow(dep, dep.head, actor1) self.assertTrue(glued) self.connect(dep, dep.head, actor1) glued = self.allow(dep, dep.tail, actor2) self.assertTrue(glued) def test_dependency_connect(self): """Test dependency connecting to two actor items """ actor1 = self.create(items.ActorItem, UML.Actor) actor2 = self.create(items.ActorItem, UML.Actor) dep = self.create(items.DependencyItem) self.connect(dep, dep.head, actor1) self.connect(dep, dep.tail, actor2) self.assertTrue(dep.subject is not None) self.assertTrue(isinstance(dep.subject, UML.Dependency)) self.assertTrue(dep.subject in self.element_factory.select()) hct = self.get_connected(dep.head) tct = self.get_connected(dep.tail) self.assertTrue(hct is actor1) self.assertTrue(tct is actor2) self.assertTrue(actor1.subject in dep.subject.supplier) self.assertTrue(actor2.subject in dep.subject.client) def test_dependency_reconnection(self): """Test dependency reconnection """ a1 = self.create(items.ActorItem, UML.Actor) a2 = self.create(items.ActorItem, UML.Actor) a3 = self.create(items.ActorItem, UML.Actor) dep = self.create(items.DependencyItem) # connect: a1 -> a2 self.connect(dep, dep.head, a1) self.connect(dep, dep.tail, a2) d = dep.subject # reconnect: a1 -> a3 self.connect(dep, dep.tail, a3) self.assertSame(d, dep.subject) self.assertEqual(1, len(dep.subject.supplier)) self.assertEqual(1, len(dep.subject.client)) self.assertTrue(a1.subject in dep.subject.supplier) self.assertTrue(a3.subject in dep.subject.client) self.assertTrue(a2.subject not in dep.subject.client, dep.subject.client) def test_dependency_disconnect(self): """Test dependency disconnecting using two actor items """ actor1 = self.create(items.ActorItem, UML.Actor) actor2 = self.create(items.ActorItem, UML.Actor) dep = self.create(items.DependencyItem) self.connect(dep, dep.head, actor1) self.connect(dep, dep.tail, actor2) dep_subj = dep.subject self.disconnect(dep, dep.tail) self.assertTrue(dep.subject is None) self.assertTrue(self.get_connected(dep.tail) is None) self.assertTrue(dep_subj not in self.element_factory.select()) self.assertTrue(dep_subj not in actor1.subject.supplierDependency) self.assertTrue(dep_subj not in actor2.subject.clientDependency) def test_dependency_reconnect(self): """Test dependency reconnection using two actor items """ actor1 = self.create(items.ActorItem, UML.Actor) actor2 = self.create(items.ActorItem, UML.Actor) dep = self.create(items.DependencyItem) self.connect(dep, dep.head, actor1) self.connect(dep, dep.tail, actor2) dep_subj = dep.subject self.disconnect(dep, dep.tail) # reconnect self.connect(dep, dep.tail, actor2) self.assertTrue(dep.subject is not None) self.assertTrue(dep.subject is not dep_subj) # the old subject has been deleted self.assertTrue(dep.subject in actor1.subject.supplierDependency) self.assertTrue(dep.subject in actor2.subject.clientDependency) # TODO: test with interface (usage) and component (realization) # TODO: test with multiple diagrams (should reuse existing relationships first) def test_multi_dependency(self): """Test multiple dependencies Dependency should appear in a new diagram, bound on a new dependency item. """ actoritem1 = self.create(items.ActorItem, UML.Actor) actoritem2 = self.create(items.ActorItem, UML.Actor) actor1 = actoritem1.subject actor2 = actoritem2.subject dep = self.create(items.DependencyItem) self.connect(dep, dep.head, actoritem1) self.connect(dep, dep.tail, actoritem2) self.assertTrue(dep.subject) self.assertEqual(1, len(actor1.supplierDependency)) self.assertTrue(actor1.supplierDependency[0] is dep.subject) self.assertEqual(1, len(actor2.clientDependency)) self.assertTrue(actor2.clientDependency[0] is dep.subject) # Do the same thing, but now on a new diagram: diagram2 = self.element_factory.create(UML.Diagram) actoritem3 = diagram2.create(items.ActorItem, subject=actor1) actoritem4 = diagram2.create(items.ActorItem, subject=actor2) dep2 = diagram2.create(items.DependencyItem) self.connect(dep2, dep2.head, actoritem3) cinfo = diagram2.canvas.get_connection(dep2.head) self.assertNotSame(None, cinfo) self.assertSame(cinfo.connected, actoritem3) self.connect(dep2, dep2.tail, actoritem4) self.assertNotSame(dep2.subject, None) self.assertEqual(1, len(actor1.supplierDependency)) self.assertTrue(actor1.supplierDependency[0] is dep.subject) self.assertEqual(1, len(actor2.clientDependency)) self.assertTrue(actor2.clientDependency[0] is dep.subject) self.assertSame(dep.subject, dep2.subject) def test_dependency_type_auto(self): """Test dependency type automatic determination """ cls = self.create(items.ClassItem, UML.Class) iface = self.create(items.InterfaceItem, UML.Interface) dep = self.create(items.DependencyItem) assert dep.auto_dependency self.connect(dep, dep.tail, cls) # connect client self.connect(dep, dep.head, iface) # connect supplier self.assertTrue(dep.subject is not None) self.assertTrue(isinstance(dep.subject, UML.Usage), dep.subject) self.assertTrue(dep.subject in self.element_factory.select()) class GeneralizationTestCase(TestCase): """ Generalization item connection adapter tests. """ def test_glue(self): """Test generalization item gluing using two classes.""" gen = self.create(items.GeneralizationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) glued = self.allow(gen, gen.tail, c1) self.assertTrue(glued) self.connect(gen, gen.tail, c1) self.assertTrue(self.get_connected(gen.tail) is c1) self.assertTrue(gen.subject is None) glued = self.allow(gen, gen.head, c2) self.assertTrue(glued) def test_connection(self): """Test generalization item connection using two classes """ gen = self.create(items.GeneralizationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) self.connect(gen, gen.tail, c1) assert self.get_connected(gen.tail) is c1 self.connect(gen, gen.head, c2) self.assertTrue(gen.subject is not None) self.assertTrue(gen.subject.general is c2.subject) self.assertTrue(gen.subject.specific is c1.subject) def test_reconnection(self): """Test generalization item connection using two classes On reconnection a new Generalization is created. """ gen = self.create(items.GeneralizationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) self.connect(gen, gen.tail, c1) assert self.get_connected(gen.tail) is c1 self.connect(gen, gen.head, c2) self.assertTrue(gen.subject is not None) self.assertTrue(gen.subject.general is c2.subject) self.assertTrue(gen.subject.specific is c1.subject) # Now do the same on a new diagram: diagram2 = self.element_factory.create(UML.Diagram) c3 = diagram2.create(items.ClassItem, subject=c1.subject) c4 = diagram2.create(items.ClassItem, subject=c2.subject) gen2 = diagram2.create(items.GeneralizationItem) self.connect(gen2, gen2.head, c3) cinfo = diagram2.canvas.get_connection(gen2.head) self.assertNotSame(None, cinfo) self.assertSame(cinfo.connected, c3) self.connect(gen2, gen2.tail, c4) self.assertNotSame(gen.subject, gen2.subject) self.assertEqual(1, len(c1.subject.generalization)) self.assertSame(c1.subject.generalization[0], gen.subject) # self.assertEqual(1, len(actor2.clientDependency)) # self.assertTrue(actor2.clientDependency[0] is dep.subject) def test_reconnection2(self): """Test reconnection of generalization """ c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) c3 = self.create(items.ClassItem, UML.Class) gen = self.create(items.GeneralizationItem) # connect: c1 -> c2 self.connect(gen, gen.head, c1) self.connect(gen, gen.tail, c2) s = gen.subject # reconnect: c2 -> c3 self.connect(gen, gen.tail, c3) self.assertSame(s, gen.subject) self.assertSame(c1.subject, gen.subject.general) self.assertSame(c3.subject, gen.subject.specific) self.assertNotSame(c2.subject, gen.subject.specific) class AssociationConnectorTestCase(TestCase): """ Association item connection adapters tests. """ def test_glue(self): """Test association item glue """ asc = self.create(items.AssociationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) glued = self.allow(asc, asc.head, c1) self.assertTrue(glued) self.connect(asc, asc.head, c1) glued = self.allow(asc, asc.tail, c2) self.assertTrue(glued) def test_connect(self): """Test association item connection """ asc = self.create(items.AssociationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) self.connect(asc, asc.head, c1) self.assertTrue(asc.subject is None) # no UML metaclass yet self.connect(asc, asc.tail, c2) self.assertTrue(asc.subject is not None) # Diagram, Class *2, Property *2, Association self.assertEqual(6, len(list(self.element_factory.select()))) self.assertTrue(asc.head_end.subject is not None) self.assertTrue(asc.tail_end.subject is not None) def test_reconnect(self): """Test association item reconnection """ asc = self.create(items.AssociationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) c3 = self.create(items.ClassItem, UML.Class) self.connect(asc, asc.head, c1) self.connect(asc, asc.tail, c2) UML.model.set_navigability(asc.subject, asc.tail_end.subject, True) a = asc.subject self.connect(asc, asc.tail, c3) self.assertSame(a, asc.subject) ends = [p.type for p in asc.subject.memberEnd] self.assertTrue(c1.subject in ends) self.assertTrue(c3.subject in ends) self.assertTrue(c2.subject not in ends) self.assertTrue(asc.tail_end.subject.navigability) def test_disconnect(self): """Test association item disconnection """ asc = self.create(items.AssociationItem) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) self.connect(asc, asc.head, c1) self.assertTrue(asc.subject is None) # no UML metaclass yet self.connect(asc, asc.tail, c2) assert asc.subject is not None self.disconnect(asc, asc.head) # after disconnection: one diagram and two classes self.assertEqual(3, len(list(self.element_factory.select()))) PK! } } 4gaphor/adapters/classes/tests/test_implementation.py""" Test implementation (interface realization) item connectors. """ from gaphor import UML from gaphor.diagram import items from gaphor.tests import TestCase class ImplementationTestCase(TestCase): def test_non_interface_glue(self): """Test non-interface gluing with implementation.""" impl = self.create(items.ImplementationItem) clazz = self.create(items.ClassItem, UML.Class) glued = self.allow(impl, impl.head, clazz) # connecting head to non-interface item is disallowed self.assertFalse(glued) def test_interface_glue(self): """Test interface gluing with implementation """ iface = self.create(items.InterfaceItem, UML.Interface) impl = self.create(items.ImplementationItem) glued = self.allow(impl, impl.head, iface) self.assertTrue(glued) def test_classifier_glue(self): """Test classifier gluing with implementation """ impl = self.create(items.ImplementationItem) clazz = self.create(items.ClassItem, UML.Class) glued = self.allow(impl, impl.tail, clazz) self.assertTrue(glued) def test_connection(self): """Test connection of class and interface with implementation """ iface = self.create(items.InterfaceItem, UML.Interface) impl = self.create(items.ImplementationItem) clazz = self.create(items.ClassItem, UML.Class) self.connect(impl, impl.head, iface) self.connect(impl, impl.tail, clazz) # check the datamodel self.assertTrue(isinstance(impl.subject, UML.Implementation)) ct = self.get_connected(impl.head) self.assertTrue(ct is iface) self.assertTrue(impl.subject is not None) self.assertTrue(impl.subject.contract[0] is iface.subject) self.assertTrue(impl.subject.implementatingClassifier[0] is clazz.subject) def test_reconnection(self): """Test reconnection of class and interface with implementation """ iface = self.create(items.InterfaceItem, UML.Interface) c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) impl = self.create(items.ImplementationItem) # connect: iface -> c1 self.connect(impl, impl.head, iface) self.connect(impl, impl.tail, c1) s = impl.subject # reconnect: iface -> c2 self.connect(impl, impl.tail, c2) self.assertSame(s, impl.subject) self.assertEqual(1, len(impl.subject.contract)) self.assertEqual(1, len(impl.subject.implementatingClassifier)) self.assertTrue(iface.subject in impl.subject.contract) self.assertTrue(c2.subject in impl.subject.implementatingClassifier) self.assertTrue( c1.subject not in impl.subject.implementatingClassifier, impl.subject.implementatingClassifier, ) PK!W886gaphor/adapters/classes/tests/test_interfaceconnect.py""" Test connections to folded interface. """ from gaphor import UML from gaphor.diagram import items from gaphor.tests import TestCase class ImplementationTestCase(TestCase): def test_folded_interface_connection(self): """Test connecting implementation to folded interface """ iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_PROVIDED impl = self.create(items.ImplementationItem) assert not impl._solid self.connect(impl, impl.head, iface, iface.ports()[0]) self.assertTrue(impl._solid) def test_folded_interface_disconnection(self): """Test disconnection implementation from folded interface """ iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_PROVIDED impl = self.create(items.ImplementationItem) assert not impl._solid self.connect(impl, impl.head, iface, iface.ports()[0]) assert impl._solid self.disconnect(impl, impl.head) self.assertTrue(not impl._solid) class DependencyTestCase(TestCase): def test_folded_interface_connection(self): """Test connecting dependency to folded interface """ iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_PROVIDED dep = self.create(items.DependencyItem) assert not dep._solid self.connect(dep, dep.head, iface, iface.ports()[0]) self.assertTrue(dep._solid) # at the end, interface folded notation shall be `required' one self.assertEqual(iface.folded, iface.FOLDED_REQUIRED) def test_folded_interface_disconnection(self): """Test disconnection dependency from folded interface """ iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_PROVIDED dep = self.create(items.DependencyItem) assert not dep._solid self.connect(dep, dep.head, iface, iface.ports()[0]) assert dep._solid self.disconnect(dep, dep.head) self.assertTrue(not dep._solid) # after disconnection, interface folded notation shall be `provided' one self.assertEqual(iface.folded, iface.FOLDED_PROVIDED) def test_unfolded_interface_disconnection(self): """Test disconnection dependency from unfolded interface """ iface = self.create(items.InterfaceItem, UML.Interface) dep = self.create(items.DependencyItem) self.connect(dep, dep.head, iface, iface.ports()[0]) assert not dep._solid self.disconnect(dep, dep.head) self.assertTrue(not dep._solid) # after disconnection, interface folded property shall be # `FOLDED_NONE' self.assertEqual(iface.folded, iface.FOLDED_NONE) LINES = ( items.ImplementationItem, items.DependencyItem, items.GeneralizationItem, items.AssociationItem, items.CommentLineItem, ) class FoldedInterfaceMultipleLinesTestCase(TestCase): """ Test connection of additional diagram lines to folded interface, which has already usage dependency or implementation connected. """ def setUp(self): super(FoldedInterfaceMultipleLinesTestCase, self).setUp() self.iface = self.create(items.InterfaceItem, UML.Interface) self.iface.folded = self.iface.FOLDED_PROVIDED def test_interface_with_implementation(self): """Test gluing different lines to folded interface with implementation.""" impl = self.create(items.ImplementationItem) self.connect(impl, impl.head, self.iface, self.iface.ports()[0]) for cls in LINES: line = self.create(cls) glued = self.allow(line, line.head, self.iface) # no additional lines (specified above) can be glued self.assertFalse(glued, "gluing of %s should not be allowed" % cls) def test_interface_with_dependency(self): """Test gluing different lines to folded interface with dependency.""" dep = self.create(items.DependencyItem) self.connect(dep, dep.head, self.iface, self.iface.ports()[0]) for cls in LINES: line = self.create(cls) glued = self.allow(line, line.head, self.iface) # no additional lines (specified above) can be glued self.assertFalse(glued, "gluing of %s should not be allowed" % cls) class FoldedInterfaceSingleLineTestCase(TestCase): """ Test connection of diagram lines to folded interface. Any lines beside implementation and dependency should be forbidden to connect. """ def test_interface_with_forbidden_lines(self): """Test gluing forbidden lines to folded interface.""" iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_PROVIDED for cls in LINES[2:]: line = self.create(cls) glued = self.allow(line, line.head, iface) # no additional lines (specified above) can be glued self.assertFalse(glued, "gluing of %s should not be allowed" % cls) PK!&gaphor/adapters/components/__init__.pyPK! =&=&.gaphor/adapters/components/connectorconnect.py""" Connector connections. Implemented using interface item in assembly connector mode, see `gaphor.diagram.connector` module for details. """ from zope import component from gaphor import UML from gaphor.diagram import items from gaphor.adapters.connectors import AbstractConnect class ConnectorConnectBase(AbstractConnect): def _get_interfaces(self, c1, c2): """ Return list of common interfaces provided by first component and required by second component. :Parameters: c1 Component providing interfaces. c2 Component requiring interfaces. """ provided = set(c1.subject.provided) required = set(c2.subject.required) interfaces = list(provided.intersection(required)) interfaces.sort(key=operator.attrgetter("name")) return interfaces def get_connecting(self, iface, both=False): """ Get items connecting to interface. :Parameters: iface Interface in question. both If true, then filter out one-side connections. """ canvas = iface.canvas connected = canvas.get_connections(connected=iface) if both: connected = [ c for c in connected if canvas.get_connection(c.item.opposite(c.handle)) ] return connected @staticmethod def get_component(connector): """ Get component connected by connector. """ canvas = connector.canvas c1 = canvas.get_connection(connector.head) c2 = canvas.get_connection(connector.tail) component = None if c1 and isinstance(c1.connected, items.ComponentItem): component = c1.connected elif c2 and isinstance(c2.connected, items.ComponentItem): component = c2.connected return component def create_uml(self, connector, component, assembly, iface): """ Create assembly connector UML metamodel for given connector item and component. :Parameters: connector Connector item. component Component item. assembly Instance of Connector UML metaclass. iface Instance of Interface UML metaclass. """ connector.subject = assembly end = self.element_factory.create(UML.ConnectorEnd) end.role = iface end.partWithPort = self.element_factory.create(UML.Port) assembly.end = end component.subject.ownedPort = end.partWithPort def drop_uml(self, connector, component): """ Destroy assembly connector UML metamodel existing between connector item and component item. :Parameters: connector Connector item. component Component item. """ p = component.subject.ownedPort[0] p.unlink() connector.subject = None def allow(self, handle, port): glue_ok = super(ConnectorConnectBase, self).allow(handle, port) iface = self.element component = self.get_connected(self.line.opposite(handle)) if isinstance(component, items.InterfaceItem): component, iface = iface, component port = self.get_connected_port(self.line.opposite(handle)) # connect only components and interfaces but not two interfaces nor # two components glue_ok = not ( isinstance(component, items.ComponentItem) and isinstance(iface, items.ComponentItem) or isinstance(component, items.InterfaceItem) and isinstance(iface, items.InterfaceItem) ) # if port type is known, then allow connection to proper port only if ( glue_ok and component is not None and iface is not None and (port.required or port.provided) ): assert isinstance(component, items.ComponentItem) assert isinstance(iface, items.InterfaceItem) glue_ok = ( port.provided and iface.subject in component.subject.provided or port.required and iface.subject in component.subject.required ) return glue_ok return glue_ok def connect(self, handle, port): super(ConnectorConnectBase, self).connect(handle, port) line = self.line canvas = line.canvas c1 = self.get_connected(line.head) c2 = self.get_connected(line.tail) if c1 and c2: # reference interface and component correctly iface = c1 component = c2 if isinstance(component, items.InterfaceItem): assert isinstance(iface, items.ComponentItem) component, iface = iface, component connections = self.get_connecting(iface, both=True) ports = set(c.port for c in connections) # to make an assembly at least two connector ends need to exist # also, two different ports of interface need to be connected if len(connections) > 1 and len(ports) == 2: # find assembly connector assembly = None for c in connections: if c.item.subject: assembly = c.item.subject assert assembly.kind == "assembly" break if assembly is None: assembly = self.element_factory.create(UML.Connector) assembly.kind = "assembly" for c in connections: connector = c.item self.create_uml( connector, self.get_component(connector), assembly, iface.subject, ) else: self.create_uml(line, component, assembly, iface.subject) def disconnect(self, handle): super(ConnectorConnectBase, self).disconnect(handle) line = self.line if line.subject is None: return iface = self.get_connected(line.head) if not isinstance(iface, items.InterfaceItem): iface = self.get_connected(line.tail) connections = list(self.get_connecting(iface, both=True)) # find ports, which will stay connected after disconnection ports = set(c.port for c in connections if c.item is not self.line) # destroy whole assembly if one connected item stays # or only one port will stay connected if len(connections) == 2 or len(ports) == 1: connector = line.subject for ci in connections: c = self.get_component(ci.item) self.drop_uml(ci.item, c) line.request_update(matrix=False) connector.unlink() else: c = self.get_component(line) self.drop_uml(line, c) @component.adapter(items.ComponentItem, items.ConnectorItem) class ComponentConnectorConnect(ConnectorConnectBase): """Connection of connector item to a component.""" pass component.provideAdapter(ComponentConnectorConnect) @component.adapter(items.InterfaceItem, items.ConnectorItem) class InterfaceConnectorConnect(ConnectorConnectBase): """Connect connector to an interface to maintain assembly connection. See also `AbstractConnect` class for exception of interface item connections. """ def allow(self, handle, port): """Allow gluing to folded interface. Only allow gluing when connectors are connected. """ glue_ok = super(InterfaceConnectorConnect, self).allow(handle, port) iface = self.element glue_ok = glue_ok and iface.folded != iface.FOLDED_NONE if glue_ok: # find connected items, which are not connectors canvas = self.element.canvas connections = self.get_connecting(self.element) lines = [ c.item for c in connections if not isinstance(c.item, items.ConnectorItem) ] glue_ok = len(lines) == 0 return glue_ok def connect(self, handle, port): super(InterfaceConnectorConnect, self).connect(handle, port) iface = self.element iface.folded = iface.FOLDED_ASSEMBLY # determine required and provided ports pport = port ports = iface.ports() index = ports.index(port) rport = ports[(index + 2) % 4] if not port.provided and not port.required: component = self.get_connected(self.line.opposite(handle)) if component is not None and iface.subject in component.subject.required: pport, rport = rport, pport pport.provided = True rport.required = True iface._angle = rport.angle ports[(index + 1) % 4].connectable = False ports[(index + 3) % 4].connectable = False def disconnect(self, handle): super(InterfaceConnectorConnect, self).disconnect(handle) iface = self.element # about to disconnect last connector if len(list(self.get_connecting(iface))) == 1: ports = iface.ports() iface.folded = iface.FOLDED_PROVIDED iface._angle = ports[0].angle for p in ports: p.connectable = True p.provided = False p.required = False component.provideAdapter(InterfaceConnectorConnect) PK!,gaphor/adapters/components/tests/__init__.pyPK!RR2gaphor/adapters/components/tests/test_connector.py""" Test connector item connectors. """ import logging from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items from gaphor.adapters.components.connectorconnect import ConnectorConnectBase log = logging.getLogger(__name__) class ComponentConnectTestCase(TestCase): """ Test connection of connector item to a component. """ def test_glue(self): """Test gluing connector to component.""" component = self.create(items.ComponentItem, UML.Component) line = self.create(items.ConnectorItem) glued = self.allow(line, line.head, component) self.assertTrue(glued) def test_connection(self): """Test connecting connector to a component """ component = self.create(items.ComponentItem, UML.Component) line = self.create(items.ConnectorItem) self.connect(line, line.head, component) self.assertTrue(line.subject is None) # self.assertTrue(line.end is None) def test_glue_both(self): """Test gluing connector to component when one is connected.""" c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) line = self.create(items.ConnectorItem) self.connect(line, line.head, c1) glued = self.allow(line, line.tail, c2) self.assertFalse(glued) class InterfaceConnectTestCase(TestCase): """ Test connection with interface acting as assembly connector. """ def test_non_folded_glue(self): """Test non-folded interface gluing.""" iface = self.create(items.InterfaceItem, UML.Component) line = self.create(items.ConnectorItem) glued = self.allow(line, line.head, iface) self.assertFalse(glued) def test_folded_glue(self): """Test folded interface gluing.""" iface = self.create(items.InterfaceItem, UML.Component) line = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_REQUIRED glued = self.allow(line, line.head, iface) self.assertTrue(glued) def test_glue_when_dependency_connected(self): """Test interface gluing, when dependency connected.""" iface = self.create(items.InterfaceItem, UML.Component) dep = self.create(items.DependencyItem) line = self.create(items.ConnectorItem) self.connect(dep, dep.head, iface) iface.folded = iface.FOLDED_REQUIRED glued = self.allow(line, line.head, iface) self.assertFalse(glued) def test_glue_when_implementation_connected(self): """Test interface gluing, when implementation connected.""" iface = self.create(items.InterfaceItem, UML.Component) impl = self.create(items.ImplementationItem) line = self.create(items.ConnectorItem) self.connect(impl, impl.head, iface) iface.folded = iface.FOLDED_REQUIRED glued = self.allow(line, line.head, iface) self.assertFalse(glued) def test_glue_when_connector_connected(self): """Test interface gluing, when connector connected.""" iface = self.create(items.InterfaceItem, UML.Component) iface.folded = iface.FOLDED_REQUIRED line1 = self.create(items.ConnectorItem) line2 = self.create(items.ConnectorItem) self.connect(line1, line1.head, iface) self.assertEqual(iface.FOLDED_ASSEMBLY, iface.folded) glued = self.allow(line2, line2.head, iface) self.assertTrue(glued) def test_simple_connection(self): """Test simple connection to an interface """ iface = self.create(items.InterfaceItem, UML.Component) line = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_PROVIDED pport = iface.ports()[0] rport = iface.ports()[2] # test preconditions assert not pport.provided and not pport.required assert not rport.provided and not rport.required self.connect(line, line.head, iface, pport) # interface goes into assembly mode self.assertEqual(iface.FOLDED_ASSEMBLY, iface.folded) self.assertFalse(iface._name.is_visible()) # no UML metamodel yet self.assertTrue(line.subject is None) # self.assertTrue(line.end is None) # check port status self.assertTrue(pport.provided and not pport.required and pport.connectable) self.assertTrue(rport.required and not rport.provided and rport.connectable) p1 = iface.ports()[1] p2 = iface.ports()[3] self.assertTrue(not p1.required and not p1.provided and not p1.connectable) self.assertTrue(not p2.required and not p2.provided and not p2.connectable) def test_connection_angle_change(self): """Test angle after connection to an interface """ iface = self.create(items.InterfaceItem, UML.Component) line = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_PROVIDED pport = iface.ports()[1] rport = iface.ports()[3] # test preconditions assert not pport.provided and not pport.required assert not rport.provided and not rport.required assert iface._angle == 0.0 self.connect(line, line.head, iface, pport) self.assertEqual(rport.angle, iface._angle) def test_connection_of_two_connectors_one_side(self): """Test connection of two connectors to required port of an interface """ iface = self.create(items.InterfaceItem, UML.Component) c1 = self.create(items.ConnectorItem) c2 = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_PROVIDED pport = iface.ports()[0] rport = iface.ports()[2] # connect to the same interface self.connect(c1, c1.head, iface, pport) self.connect(c2, c2.head, iface, pport) # no UML metamodel yet self.assertTrue(c1.subject is None) # self.assertTrue(c1.end is None) self.assertTrue(c2.subject is None) # self.assertTrue(c2.end is None) # check port status self.assertTrue(pport.provided and not pport.required) self.assertTrue(rport.required and not rport.provided) p1 = iface.ports()[1] p2 = iface.ports()[3] self.assertTrue(not p1.required and not p1.provided) self.assertTrue(not p2.required and not p2.provided) def test_connection_of_two_connectors_two_sides(self): """Test connection of two connectors to required and provided ports of an interface """ iface = self.create(items.InterfaceItem, UML.Component) c1 = self.create(items.ConnectorItem) c2 = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_PROVIDED pport = iface.ports()[0] rport = iface.ports()[2] self.connect(c1, c1.head, iface, pport) self.connect(c2, c2.head, iface, rport) # no UML metamodel yet self.assertTrue(c1.subject is None) # self.assertTrue(c1.end is None) self.assertTrue(c2.subject is None) # self.assertTrue(c2.end is None) # check port status self.assertTrue(pport.provided and not pport.required) self.assertTrue(rport.required and not rport.provided) p1 = iface.ports()[1] p2 = iface.ports()[3] self.assertTrue(not p1.required and not p1.provided) self.assertTrue(not p2.required and not p2.provided) def test_simple_disconnection(self): """Test disconnection of simple connection to an interface """ iface = self.create(items.InterfaceItem, UML.Component) line = self.create(items.ConnectorItem) iface.folded = iface.FOLDED_PROVIDED pport = iface.ports()[1] self.connect(line, line.head, iface, pport) # test preconditions assert pport.provided and not pport.required and pport.connectable self.disconnect(line, line.head) self.assertEqual(iface.FOLDED_PROVIDED, iface.folded) self.assertEqual(iface._angle, 0) self.assertTrue(iface._name.is_visible()) self.assertFalse(any(p.provided for p in iface.ports())) self.assertFalse(any(p.required for p in iface.ports())) self.assertTrue(all(p.connectable for p in iface.ports())) class AssemblyConnectorTestCase(TestCase): """ Test assembly connector. It is assumed that interface and component connection tests defined above are working correctly. """ def create_interfaces(self, *args): """ Generate interfaces with names specified by arguments. :Paramters: args List of interface names. """ for name in args: interface = self.element_factory.create(UML.Interface) interface.name = name yield interface def provide(self, component, interface): """ Change component's data so it implements interfaces. """ impl = self.element_factory.create(UML.Implementation) component.implementation = impl impl.contract = interface def require(self, component, interface): """ Change component's data so it requires interface. """ usage = self.element_factory.create(UML.Usage) component.clientDependency = usage usage.supplier = interface def test_getting_component(self): """Test getting component """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) # connect component self.connect(conn1, conn1.tail, c1) self.connect(conn2, conn2.head, c2) self.assertTrue(c1 is ConnectorConnectBase.get_component(conn1)) self.assertTrue(c2 is ConnectorConnectBase.get_component(conn2)) def test_connection(self): """Test basic assembly connection """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # first component provides interface # and the second one requires it self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) # connect component self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) # make an assembly self.connect(conn1, conn1.tail, iface, pport) self.connect(conn2, conn2.tail, iface, rport) # test UML data model self.assertTrue( conn1.subject is conn2.subject, "%s is not %s" % (conn1.subject, conn2.subject), ) assembly = conn1.subject self.assertTrue(isinstance(assembly, UML.Connector)) self.assertEqual("assembly", assembly.kind) # there should be two connector ends self.assertEqual(2, len(assembly.end)) # interface is on both ends # end1 = conn1.end # end2 = conn2.end # self.assertTrue(end1 in assembly.end, # '%s not in %s' % (end1, assembly.end)) # self.assertTrue(end2 in assembly.end, # '%s not in %s' % (end2, assembly.end)) # self.assertEqual(end1.role, iface.subject) # self.assertEqual(end2.role, iface.subject) # ends of connector point to components # p1 = end1.partWithPort # p2 = end2.partWithPort # self.assertEqual(p1, c1.subject.ownedPort[0], # '%s != %s' % (p1, c1.subject.ownedPort)) # self.assertEqual(p2, c2.subject.ownedPort[0], # '%s != %s' % (p2, c2.subject.ownedPort)) def test_required_port_glue(self): """Test if required port gluing works.""" conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) # connect components self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) self.connect(conn1, conn1.tail, iface, pport) glued = self.allow(conn2, conn2.tail, iface, rport) self.assertTrue(glued) def test_wrong_port_glue(self): """Test if incorrect port gluing is forbidden.""" conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) conn3 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) c3 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) self.require(c3.subject, iface.subject) # connect first two components self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) self.connect(conn1, conn1.tail, iface, pport) self.connect(conn3, conn3.tail, iface, pport) # cannot allow to provided port of interface, which is required glued = self.allow(conn2, conn2.tail, iface, pport) self.assertFalse(glued) # cannot allow component, which requires an interface, when # connector is connected to to provided port glued = self.allow(conn3, conn3.head, c3) self.assertFalse(glued) def test_port_status(self): """Test if port type is set properly """ conn1 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # component requires interface self.require(c1.subject, iface.subject) # connect component self.connect(conn1, conn1.head, c1) # first step to make an assembly self.connect(conn1, conn1.tail, iface, rport) # check port type self.assertTrue(pport.provided) self.assertTrue(rport.required) def test_connection_order(self): """Test connection order of assembly connection """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # both components provide interface only self.provide(c1.subject, iface.subject) self.provide(c2.subject, iface.subject) # connect components self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) # connect to provided port self.connect(conn1, conn1.tail, iface, pport) self.connect(conn2, conn2.tail, iface, pport) # no UML data model yet (no connection on required port) self.assertTrue(conn1.subject is None) self.assertTrue(conn2.subject is None) # self.assertTrue(conn1.end is None) # self.assertTrue(conn2.end is None) def test_addtional_connections(self): """Test additional connections to assembly connection """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) conn3 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) c3 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # provide and require interface by components self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) self.require(c3.subject, iface.subject) # connect components self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) self.connect(conn3, conn3.head, c3) # create assembly self.connect(conn1, conn1.tail, iface, pport) self.connect(conn2, conn2.tail, iface, rport) # test precondition assert conn1.subject and conn2.subject # additional connection self.connect(conn3, conn3.tail, iface, rport) # test UML data model self.assertTrue(conn3.subject is conn1.subject) # self.assertTrue(conn3.end is not None) assembly = conn1.subject self.assertEqual(3, len(assembly.end)) # end3 = conn3.end # self.assertTrue(end3 in assembly.end, # '%s not in %s' % (end3, assembly.end)) # self.assertEqual(end3.role, iface.subject) # ends of connector point to components # p3 = end3.partWithPort # self.assertEqual(p3, c3.subject.ownedPort[0], # '%s != %s' % (p3, c3.subject.ownedPort)) def test_disconnection(self): """Test assembly connector disconnection """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # first component provides interface # and the second one requires it self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) # connect component self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) # make an assembly self.connect(conn1, conn1.tail, iface, pport) self.connect(conn2, conn2.tail, iface, rport) # test precondition assert conn1.subject is conn2.subject self.disconnect(conn1, conn1.head) self.assertTrue(conn1.subject is None) self.assertTrue(conn2.subject is None) self.assertEqual(0, len(self.kindof(UML.Connector))) self.assertEqual(0, len(self.kindof(UML.ConnectorEnd))) self.assertEqual(0, len(self.kindof(UML.Port))) def test_disconnection_order(self): """Test assembly connector disconnection order """ conn1 = self.create(items.ConnectorItem) conn2 = self.create(items.ConnectorItem) conn3 = self.create(items.ConnectorItem) c1 = self.create(items.ComponentItem, UML.Component) c2 = self.create(items.ComponentItem, UML.Component) c3 = self.create(items.ComponentItem, UML.Component) iface = self.create(items.InterfaceItem, UML.Interface) iface.folded = iface.FOLDED_ASSEMBLY pport = iface.ports()[0] rport = iface.ports()[2] # provide and require interface self.provide(c1.subject, iface.subject) self.require(c2.subject, iface.subject) self.require(c3.subject, iface.subject) # connect components self.connect(conn1, conn1.head, c1) self.connect(conn2, conn2.head, c2) self.connect(conn3, conn3.head, c3) # make assembly self.connect(conn1, conn1.tail, iface, pport) self.connect(conn2, conn2.tail, iface, rport) self.connect(conn3, conn3.tail, iface, rport) # test precondition assert conn1.subject is conn2.subject is conn3.subject # disconnect from provided port # assembly should be destroyed log.debug("Perform disconnect from here") self.disconnect(conn1, conn1.head) log.debug("Disconnect done") self.assertTrue(conn1.subject is None) self.assertTrue(conn2.subject is None) self.assertTrue(conn3.subject is None) self.assertEqual(0, len(self.kindof(UML.Connector))) self.assertEqual(0, len(self.kindof(UML.ConnectorEnd))) self.assertEqual(0, len(self.kindof(UML.Port))) PK! o1@@gaphor/adapters/connectors.py""" Connector adapters. To register connectors implemented in this module, it is imported in gaphor.adapter package. """ import logging from zope import component from zope.interface import implementer from gaphor import UML from gaphor.core import inject from gaphor.diagram import items from gaphor.diagram.interfaces import IConnect logger = logging.getLogger(__name__) @implementer(IConnect) class AbstractConnect(object): """ Connection adapter for Gaphor diagram items. Line item ``line`` connects with a handle to a connectable item ``element``. Attributes: - line: connecting item - element: connectable item The following methods are required to make this work: - `allow()`: is the connection allowed at all (during mouse movement for example). - `connect()`: Establish a connection between element and line. Also takes care of disconnects, if required (e.g. 1:1 relationships) - `disconnect()`: Break connection, called when dropping a handle on a point where it can not connect. - `reconnect()`: Connect to another item (only used if present) By convention the adapters are registered by (element, line) -- in that order. """ element_factory = inject("element_factory") def __init__(self, element, line): self.element = element self.line = line self.canvas = self.element.canvas assert self.canvas == self.element.canvas == self.line.canvas def get_connection(self, handle): """ Get connection information """ return self.canvas.get_connection(handle) def get_connected(self, handle): """ Get item connected to a handle. """ cinfo = self.canvas.get_connection(handle) if cinfo is not None: return cinfo.connected def get_connected_port(self, handle): """ Get port of item connected to connecting item via specified handle. """ cinfo = self.canvas.get_connection(handle) if cinfo is not None: return cinfo.port def allow(self, handle, port): """ Determine if items can be connected. The method contains a hack for folded interfaces, see `gaphor.diagram.classes.interface` module documentation for connection to folded interface rules. Returns `True` by default. """ iface = self.element if isinstance(iface, items.InterfaceItem) and iface.folded: canvas = self.canvas count = any(canvas.get_connections(connected=iface)) return not count and isinstance( self.line, (items.DependencyItem, items.ImplementationItem) ) return True def connect(self, handle, port): """ Connect to an element. Note that at this point the line may be connected to some other, or the same element. Also the connection at UML level still exists. Returns `True` if a connection is established. """ return True def disconnect(self, handle): """Disconnect UML model level connections.""" pass @component.adapter(items.ElementItem, items.CommentLineItem) class CommentLineElementConnect(AbstractConnect): """Connect a comment line to any element item.""" def allow(self, handle, port): """ In addition to the normal check, both line ends may not be connected to the same element. Same goes for subjects. One of the ends should be connected to a UML.Comment element. """ opposite = self.line.opposite(handle) connected_to = self.get_connected(opposite) element = self.element if connected_to is element: return None # Same goes for subjects: if ( connected_to and (not (connected_to.subject or element.subject)) and connected_to.subject is element.subject ): return None # One end should be connected to a CommentItem: cls = items.CommentItem glue_ok = isinstance(connected_to, cls) ^ isinstance(self.element, cls) if connected_to and not glue_ok: return None # Do not allow links between the comment and the element if ( connected_to and element and ( ( isinstance(connected_to.subject, UML.Comment) and self.element.subject in connected_to.subject.annotatedElement ) or ( isinstance(self.element.subject, UML.Comment) and connected_to.subject in self.element.subject.annotatedElement ) ) ): return None return super(CommentLineElementConnect, self).allow(handle, port) def connect(self, handle, port): if super(CommentLineElementConnect, self).connect(handle, port): opposite = self.line.opposite(handle) connected_to = self.get_connected(opposite) if connected_to: if isinstance(connected_to.subject, UML.Comment): connected_to.subject.annotatedElement = self.element.subject else: self.element.subject.annotatedElement = connected_to.subject def disconnect(self, handle): opposite = self.line.opposite(handle) oct = self.get_connected(opposite) hct = self.get_connected(handle) if hct and oct: logger.debug("Disconnecting %s and %s" % (hct, oct)) try: if hct.subject and isinstance(oct.subject, UML.Comment): del oct.subject.annotatedElement[hct.subject] elif hct.subject and oct.subject: del hct.subject.annotatedElement[oct.subject] except ValueError: logger.debug( "Invoked CommentLineElementConnect.disconnect() for nonexistent relationship" ) super(CommentLineElementConnect, self).disconnect(handle) component.provideAdapter(CommentLineElementConnect) class CommentLineLineConnect(AbstractConnect): """Connect a comment line to any diagram line.""" def allow(self, handle, port): """ In addition to the normal check, both line ends may not be connected to the same element. Same goes for subjects. One of the ends should be connected to a UML.Comment element. """ opposite = self.line.opposite(handle) element = self.element connected_to = self.get_connected(opposite) # do not connect to the same item nor connect to other comment line if ( connected_to is element or not element.subject or isinstance(element, items.CommentLineItem) ): return None # Same goes for subjects: if ( connected_to and (not (connected_to.subject or element.subject)) and connected_to.subject is element.subject ): return None # One end should be connected to a CommentItem: cls = items.CommentItem glue_ok = isinstance(connected_to, cls) ^ isinstance(self.element, cls) if connected_to and not glue_ok: return None return super(CommentLineLineConnect, self).allow(handle, port) def connect(self, handle, port): if super(CommentLineLineConnect, self).connect(handle, port): opposite = self.line.opposite(handle) c = self.get_connected(opposite) if c and self.element.subject: if isinstance(c.subject, UML.Comment): c.subject.annotatedElement = self.element.subject else: self.element.subject.annotatedElement = c.subject def disconnect(self, handle): c1 = self.get_connected(handle) opposite = self.line.opposite(handle) c2 = self.get_connected(opposite) if c1 and c2: if ( isinstance(c1.subject, UML.Comment) and c2.subject in c1.subject.annotatedElement ): del c1.subject.annotatedElement[c2.subject] elif c2.subject and c1.subject in c2.subject.annotatedElement: del c2.subject.annotatedElement[c1.subject] super(CommentLineLineConnect, self).disconnect(handle) component.provideAdapter( CommentLineLineConnect, adapts=(items.DiagramLine, items.CommentLineItem) ) class InverseCommentLineLineConnect(CommentLineLineConnect): """ In case a line is disconnected that contains a comment-line, the comment line unlinking should happen in a correct way. """ def __init__(self, line, element): super().__init__(element, line) component.provideAdapter( InverseCommentLineLineConnect, adapts=(items.CommentLineItem, items.DiagramLine) ) class UnaryRelationshipConnect(AbstractConnect): """ Base class for relationship connections, such as associations, dependencies and implementations. Unary relationships are allowed to connect both ends to the same element This class introduces a new method: relationship() which is used to find an existing relationship in the model that does not yet exist on the canvas. """ element_factory = inject("element_factory") def relationship(self, required_type, head, tail): """ Find an existing relationship in the model that meets the required type and is connected to the same model element the head and tail of the line are connected to. type - the type of relationship we're looking for head - tuple (association name on line, association name on element) tail - tuple (association name on line, association name on element) """ line = self.line head_subject = self.get_connected(line.head).subject tail_subject = self.get_connected(line.tail).subject # First check if the right subject is already connected: if ( line.subject and getattr(line.subject, head.name) is head_subject and getattr(line.subject, tail.name) is tail_subject ): return line.subject # Try to find a relationship, that is already created, but not # yet displayed in the diagram. for gen in getattr(tail_subject, tail.opposite): if not isinstance(gen, required_type): continue gen_head = getattr(gen, head.name) try: if not head_subject in gen_head: continue except TypeError: if not gen_head is head_subject: continue # Check for this entry on line.canvas for item in gen.presentation: # Allow line to be returned. Avoids strange # behaviour during loading if item.canvas is line.canvas and item is not line: break else: return gen return None def relationship_or_new(self, type, head, tail): """ Like relation(), but create a new instance if none was found. """ relation = self.relationship(type, head, tail) if not relation: line = self.line relation = self.element_factory.create(type) setattr(relation, head.name, self.get_connected(line.head).subject) setattr(relation, tail.name, self.get_connected(line.tail).subject) return relation def reconnect_relationship(self, handle, head, tail): """ Reconnect relationship for given handle. :Parameters: handle Handle at which reconnection happens. head Relationship head attribute name. tail Relationship tail attribute name. """ line = self.line c1 = self.get_connected(line.head) c2 = self.get_connected(line.tail) if line.head is handle: setattr(line.subject, head.name, c1.subject) elif line.tail is handle: setattr(line.subject, tail.name, c2.subject) else: raise ValueError("Incorrect handle passed to adapter") def connect_connected_items(self, connections=None): """ Cause items connected to ``line`` to reconnect, allowing them to establish or destroy relationships at model level. """ line = self.line canvas = self.canvas solver = canvas.solver # First make sure coordinates match solver.solve() for cinfo in connections or canvas.get_connections(connected=line): if line is cinfo.connected: continue adapter = component.queryMultiAdapter((line, cinfo.connected), IConnect) assert adapter, "No element to connect {} and {}".format( line, cinfo.connected ) adapter.connect(cinfo.handle, cinfo.port) def disconnect_connected_items(self): """ Cause items connected to @line to be disconnected. This is necessary if the subject of the @line is to be removed. Returns a list of (item, handle) pairs that were connected (this list can be used to connect items again with connect_connected_items()). """ line = self.line canvas = self.canvas solver = canvas.solver # First make sure coordinates match solver.solve() connections = list(canvas.get_connections(connected=line)) for cinfo in connections: adapter = component.queryMultiAdapter( (cinfo.item, cinfo.connected), IConnect ) assert adapter adapter.disconnect(cinfo.handle) return connections def connect_subject(self, handle): """ Establish the relationship at model level. """ raise NotImplementedError("Implement connect_subject() in a subclass") def disconnect_subject(self, handle): """ Disconnect the diagram item from its model element. If there are no more presentations(diagram items) connected to the model element, unlink() it too. """ line = self.line old = line.subject del line.subject if old and len(old.presentation) == 0: old.unlink() def connect(self, handle, port): """ Connect the items to each other. The model level relationship is created by create_subject() """ if super(UnaryRelationshipConnect, self).connect(handle, port): opposite = self.line.opposite(handle) oct = self.get_connected(opposite) if oct: self.connect_subject(handle) line = self.line if line.subject: self.connect_connected_items() return True def disconnect(self, handle): """ Disconnect model element. """ line = self.line opposite = line.opposite(handle) oct = self.get_connected(opposite) hct = self.get_connected(handle) if hct and oct: # Both sides of line are connected => disconnect connections = self.disconnect_connected_items() self.disconnect_subject(handle) super(UnaryRelationshipConnect, self).disconnect(handle) class RelationshipConnect(UnaryRelationshipConnect): """ """ def allow(self, handle, port): """ In addition to the normal check, both relationship ends may not be connected to the same element. Same goes for subjects. """ opposite = self.line.opposite(handle) line = self.line element = self.element connected_to = self.get_connected(opposite) # Element can not be a parent of itself. if connected_to is element: return None # Same goes for subjects: if ( connected_to and (not (connected_to.subject or element.subject)) and connected_to.subject is element.subject ): return None return super(RelationshipConnect, self).allow(handle, port) PK!B +NNgaphor/adapters/editors.py""" Adapters """ import logging from zope import component from simplegeneric import generic from zope.interface import implementer from gaphor import UML from gaphor.core import inject from gaphor.diagram import items from gaphor.diagram.interfaces import IEditor from gaphor.misc.rattr import rgetattr, rsetattr log = logging.getLogger(__name__) @generic def editable(el): """ Return editable part of UML element. It returns element itself by default. """ return el @editable.when_type(UML.Slot) def editable_slot(el): """ Return editable part of a slot. """ return el.value @implementer(IEditor) @component.adapter(items.CommentItem) class CommentItemEditor(object): """Text edit support for Comment item.""" def __init__(self, item): self._item = item def is_editable(self, x, y): return True def get_text(self): return self._item.subject.body def get_bounds(self): return None def update_text(self, text): self._item.subject.body = text def key_pressed(self, pos, key): pass component.provideAdapter(CommentItemEditor) @implementer(IEditor) @component.adapter(items.NamedItem) class NamedItemEditor(object): """Text edit support for Named items.""" def __init__(self, item): self._item = item def is_editable(self, x, y): return True def get_text(self): s = self._item.subject return s.name if s else "" def get_bounds(self): return None def update_text(self, text): if self._item.subject: self._item.subject.name = text self._item.request_update() def key_pressed(self, pos, key): pass component.provideAdapter(NamedItemEditor) @implementer(IEditor) @component.adapter(items.DiagramItem) class DiagramItemTextEditor(object): """Text edit support for diagram items containing text elements.""" def __init__(self, item): self._item = item self._text_element = None def is_editable(self, x, y): if not self._item.subject: return False for txt in self._item.texts(): if (x, y) in txt.bounds: self._text_element = txt break return self._text_element is not None def get_text(self): if self._text_element: return rgetattr(self._item.subject, self._text_element.attr) def get_bounds(self): return None def update_text(self, text): log.debug("Updating text to %s" % text) if self._text_element: self._text_element.text = text rsetattr(self._item.subject, self._text_element.attr, text) def key_pressed(self, pos, key): pass component.provideAdapter(DiagramItemTextEditor) @implementer(IEditor) @component.adapter(items.CompartmentItem) class CompartmentItemEditor(object): """Text editor support for compartment items.""" def __init__(self, item): self._item = item self._edit = None def is_editable(self, x, y): """ Find out what's located at point (x, y), is it in the name part or is it text in some compartment """ self._edit = self._item.item_at(x, y) return bool(self._edit and self._edit.subject) def get_text(self): return UML.format(editable(self._edit.subject)) def get_bounds(self): return None def update_text(self, text): UML.parse(editable(self._edit.subject), text) def key_pressed(self, pos, key): pass component.provideAdapter(CompartmentItemEditor) @implementer(IEditor) @component.adapter(items.AssociationItem) class AssociationItemEditor(object): def __init__(self, item): self._item = item self._edit = None def is_editable(self, x, y): """Find out what's located at point (x, y), is it in the name part or is it text in some compartment """ item = self._item if not item.subject: return False if item.head_end.point((x, y)) <= 0: self._edit = item.head_end elif item.tail_end.point((x, y)) <= 0: self._edit = item.tail_end else: self._edit = item return True def get_text(self): if self._edit is self._item: return self._edit.subject.name return UML.format( self._edit.subject, visibility=True, is_derived=True, type=True, multiplicity=True, default=True, ) def get_bounds(self): return None def update_text(self, text): UML.parse(self._edit.subject, text) def key_pressed(self, pos, key): pass component.provideAdapter(AssociationItemEditor) @implementer(IEditor) @component.adapter(items.ForkNodeItem) class ForkNodeItemEditor(object): """Text edit support for fork node join specification.""" element_factory = inject("element_factory") def __init__(self, item): self._item = item def is_editable(self, x, y): return True def get_text(self): """ Get join specification text. """ if self._item.subject.joinSpec: return self._item.subject.joinSpec else: return "" def get_bounds(self): return None def update_text(self, text): """ Set join specification text. """ spec = self._item.subject.joinSpec if not spec: spec = text def key_pressed(self, pos, key): pass component.provideAdapter(ForkNodeItemEditor) PK!:6gaphor/adapters/grouping.py""" Grouping functionality allows to nest one item within another item (parent item). This is useful in several use cases - artifact deployed within a node - a class within a package or a component - composite structures (i.e. component within a node) The grouping adapters has to implement three methods, see `AbstractGroup` class. It is important to note, that grouping adapters can be queried before instance of an item to be grouped is created. This happens when item is about to be created. Therefore `AbstractGroup.can_contain` has to be aware that `AbstractGroup.item` can be null. """ import logging from zope.interface import implementer from zope import component from gaphor import UML from gaphor.core import inject from gaphor.diagram import items from gaphor.diagram.interfaces import IGroup log = logging.getLogger(__name__) @implementer(IGroup) class AbstractGroup(object): """ Base class for grouping UML objects. :Attributes: parent Parent item, which groups other items. item Item to be grouped. """ element_factory = inject("element_factory") def __init__(self, parent, item): self.parent = parent self.item = item def can_contain(self): """ Check if parent can contain an item. True by default. """ return True def group(self): """ Group an item within parent. """ raise NotImplemented("This is abstract method") def ungroup(self): """ Remove item from parent. """ raise NotImplemented("This is abstract method") class InteractionLifelineGroup(AbstractGroup): """ Add lifeline to interaction. """ def group(self): self.parent.subject.lifeline = self.item.subject self.parent.canvas.reparent(self.item, self.parent) def ungroup(self): del self.parent.subject.lifeline[self.item.subject] component.provideAdapter( factory=InteractionLifelineGroup, adapts=(items.InteractionItem, items.LifelineItem) ) class NodeGroup(AbstractGroup): """ Add node to another node. """ def group(self): self.parent.subject.nestedNode = self.item.subject def ungroup(self): del self.parent.subject.nestedNode[self.item.subject] component.provideAdapter(factory=NodeGroup, adapts=(items.NodeItem, items.NodeItem)) class NodeComponentGroup(AbstractGroup): """ Add components to node using internal structures. """ def group(self): node = self.parent.subject component = self.item.subject # node attribute a1 = self.element_factory.create(UML.Property) a1.aggregation = "composite" # component attribute a2 = self.element_factory.create(UML.Property) e1 = self.element_factory.create(UML.ConnectorEnd) e2 = self.element_factory.create(UML.ConnectorEnd) # create connection between node and component e1.role = a1 e2.role = a2 connector = self.element_factory.create(UML.Connector) connector.end = e1 connector.end = e2 # compose component within node node.ownedAttribute = a1 node.ownedConnector = connector component.ownedAttribute = a2 def ungroup(self): node = self.parent.subject component = self.item.subject for connector in node.ownedConnector: e1 = connector.end[0] e2 = connector.end[1] if e1.role in node.ownedAttribute and e2.role in component.ownedAttribute: e1.role.unlink() e2.role.unlink() e1.unlink() e2.unlink() connector.unlink() log.debug("Removed %s from node %s" % (component, node)) component.provideAdapter( factory=NodeComponentGroup, adapts=(items.NodeItem, items.ComponentItem) ) class NodeArtifactGroup(AbstractGroup): """ Deploy artifact on node. """ def group(self): node = self.parent.subject artifact = self.item.subject # deploy artifact on node deployment = self.element_factory.create(UML.Deployment) node.deployment = deployment deployment.deployedArtifact = artifact def ungroup(self): node = self.parent.subject artifact = self.item.subject for deployment in node.deployment: if deployment.deployedArtifact[0] is artifact: deployment.unlink() log.debug("Removed %s from node %s" % (artifact, node)) component.provideAdapter( factory=NodeArtifactGroup, adapts=(items.NodeItem, items.ArtifactItem) ) class SubsystemUseCaseGroup(AbstractGroup): """ Make subsystem a subject of an use case. """ def group(self): component = self.parent.subject usecase = self.item.subject usecase.subject = component def ungroup(self): component = self.parent.subject usecase = self.item.subject usecase.subject.remove(component) component.provideAdapter( factory=SubsystemUseCaseGroup, adapts=(items.SubsystemItem, items.UseCaseItem) ) class ActivityPartitionsGroup(AbstractGroup): """ Group activity partitions. """ def can_contain(self): return not self.parent.subject or ( self.parent.subject and len(self.parent.subject.node) == 0 ) def group(self): p = self.parent.subject sp = self.element_factory.create(UML.ActivityPartition) self.item.subject = sp sp.name = "Swimlane" if p: p.subpartition = sp for k in self.item.canvas.get_children(self.item): sp.subpartition = k.subject def ungroup(self): p = self.parent.subject sp = self.item.subject if p: p.subpartition.remove(sp) self.item.subject = None for s in sp.subpartition: sp.subpartition.remove(s) assert len(sp.node) == 0 # ungroup activity nodes canvas = self.item.canvas nodes = [ n for n in canvas.get_children(self.item) if isinstance( n, ( items.ActivityNodeItem, items.ActionItem, items.ObjectNodeItem, items.ForkNodeItem, ), ) ] for n in nodes: canvas.reparent(n, None) sp.unlink() component.provideAdapter( factory=ActivityPartitionsGroup, adapts=(items.PartitionItem, items.PartitionItem) ) class ActivityNodePartitionGroup(AbstractGroup): """ Group activity nodes within activity partition. """ def can_contain(self): return self.parent.subject and len(self.parent.subject.subpartition) == 0 def group(self): partition = self.parent.subject node = self.item.subject partition.node = node def ungroup(self): partition = self.parent.subject node = self.item.subject partition.node.remove(node) component.provideAdapter( factory=ActivityNodePartitionGroup, adapts=(items.PartitionItem, items.ActivityNodeItem), ) component.provideAdapter( factory=ActivityNodePartitionGroup, adapts=(items.PartitionItem, items.ActionItem) ) component.provideAdapter( factory=ActivityNodePartitionGroup, adapts=(items.PartitionItem, items.ObjectNodeItem), ) component.provideAdapter( factory=ActivityNodePartitionGroup, adapts=(items.PartitionItem, items.ForkNodeItem) ) PK!(gaphor/adapters/interactions/__init__.pyPK!=ass.gaphor/adapters/interactions/messageconnect.py"""Message item connection adapters.""" from gaphor.adapters.connectors import AbstractConnect from zope import interface, component from gaphor import UML from gaphor.diagram import items @component.adapter(items.LifelineItem, items.MessageItem) class MessageLifelineConnect(AbstractConnect): """Connect lifeline with a message. A message can connect to both the lifeline's head (the rectangle) or the lifetime line. In case it's added to the head, the message is considered to be part of a communication diagram. If the message is added to a lifetime line, it's considered a sequence diagram. """ def connect_lifelines(self, line, send, received): """ Always create a new Message with two EventOccurrence instances. """ def get_subject(): if not line.subject: message = self.element_factory.create(UML.Message) message.name = "call()" line.subject = message return line.subject if send: message = get_subject() if not message.sendEvent: event = self.element_factory.create(UML.MessageOccurrenceSpecification) event.sendMessage = message event.covered = send.subject if received: message = get_subject() if not message.receiveEvent: event = self.element_factory.create(UML.MessageOccurrenceSpecification) event.receiveMessage = message event.covered = received.subject def disconnect_lifelines(self, line): """ Disconnect lifeline and set appropriate kind of message item. If there are no lifelines connected on both ends, then remove the message from the data model. """ send = self.get_connected(line.head) received = self.get_connected(line.tail) if send: event = line.subject.receiveEvent if event: event.unlink() if received: event = line.subject.sendEvent if event: event.unlink() # one is disconnected and one is about to be disconnected, # so destroy the message if not send or not received: # Both ends are disconnected: message = line.subject del line.subject if not message.presentation: message.unlink() for message in list(line._messages): line.remove_message(message, False) message.unlink() for message in list(line._inverted_messages): line.remove_message(message, True) message.unlink() def allow(self, handle, port): """ Glue to lifeline's head or lifetime. If lifeline's lifetime is visible then disallow connection to lifeline's head. """ element = self.element lifetime = element.lifetime line = self.line opposite = line.opposite(handle) ol = self.get_connected(opposite) if ol: opposite_is_visible = ol.lifetime.visible # connect lifetimes if both are visible or both invisible return not (lifetime.visible ^ opposite_is_visible) return not (lifetime.visible ^ (port is element.lifetime.port)) def connect(self, handle, port): super(MessageLifelineConnect, self).connect(handle, port) line = self.line send = self.get_connected(line.head) received = self.get_connected(line.tail) self.connect_lifelines(line, send, received) lifetime = self.element.lifetime # if connected to head, then make lifetime invisible if port is lifetime.port: lifetime.min_length = lifetime.MIN_LENGTH_VISIBLE else: lifetime.visible = False lifetime.connectable = False def disconnect(self, handle): super(MessageLifelineConnect, self).disconnect(handle) line = self.line send = self.get_connected(line.head) received = self.get_connected(line.tail) lifeline = self.element lifetime = lifeline.lifetime # if a message is delete message, then disconnection causes # lifeline to be no longer destroyed (note that there can be # only one delete message connected to lifeline) if received and line.subject.messageSort == "deleteMessage": received.is_destroyed = False received.request_update() self.disconnect_lifelines(line) if len(list(self.canvas.get_connections(connected=lifeline))) == 1: # after disconnection count of connected items will be # zero, so allow connections to lifeline's lifetime lifetime.connectable = True lifetime.min_length = lifetime.MIN_LENGTH component.provideAdapter(MessageLifelineConnect) PK!.gaphor/adapters/interactions/tests/__init__.pyPK!38%%2gaphor/adapters/interactions/tests/test_message.py""" Message connection adapter tests. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class BasicMessageConnectionsTestCase(TestCase): def test_head_glue(self): """Test message head glue """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) # get head port port = ll.ports()[0] glued = self.allow(msg, msg.head, ll, port) self.assertTrue(glued) def test_invisible_lifetime_glue(self): """Test message to invisible lifetime glue """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) glued = self.allow(msg, msg.head, ll, ll.lifetime.port) assert not ll.lifetime.visible self.assertFalse(glued) def test_visible_lifetime_glue(self): """Test message to visible lifetime glue """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) ll.lifetime.visible = True glued = self.allow(msg, msg.head, ll, ll.lifetime.port) self.assertTrue(glued) def test_lost_message_connection(self): """Test lost message connection """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) self.connect(msg, msg.head, ll) # If one side is connected a "lost" message is created self.assertTrue(msg.subject is not None) self.assertEqual(msg.subject.messageKind, "lost") messages = self.kindof(UML.Message) occurrences = self.kindof(UML.MessageOccurrenceSpecification) self.assertEqual(1, len(messages)) self.assertEqual(1, len(occurrences)) self.assertTrue(messages[0] is msg.subject) self.assertTrue(occurrences[0] is msg.subject.sendEvent) def test_found_message_connection(self): """Test found message connection """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) self.connect(msg, msg.tail, ll) # If one side is connected a "found" message is created self.assertTrue(msg.subject is not None) self.assertEqual(msg.subject.messageKind, "found") messages = self.kindof(UML.Message) occurrences = self.kindof(UML.MessageOccurrenceSpecification) self.assertEqual(1, len(messages)) self.assertEqual(1, len(occurrences)) self.assertTrue(messages[0] is msg.subject) self.assertTrue(occurrences[0] is msg.subject.receiveEvent) def test_complete_message_connection(self): """Test complete message connection """ ll1 = self.create(items.LifelineItem) ll2 = self.create(items.LifelineItem) msg = self.create(items.MessageItem) self.connect(msg, msg.head, ll1) self.connect(msg, msg.tail, ll2) # two sides are connected - "complete" message is created self.assertTrue(msg.subject is not None) self.assertEqual(msg.subject.messageKind, "complete") messages = self.kindof(UML.Message) occurences = self.kindof(UML.MessageOccurrenceSpecification) self.assertEqual(1, len(messages)) self.assertEqual(2, len(occurences)) self.assertTrue(messages[0] is msg.subject) self.assertTrue(msg.subject.sendEvent in occurences, "%s" % occurences) self.assertTrue(msg.subject.receiveEvent in occurences, "%s" % occurences) def test_lifetime_connection(self): """Test messages' lifetimes connection """ msg = self.create(items.MessageItem) ll1 = self.create(items.LifelineItem) ll2 = self.create(items.LifelineItem) # make lifelines to be in sequence diagram mode ll1.lifetime.visible = True ll2.lifetime.visible = True assert ll1.lifetime.visible and ll2.lifetime.visible # connect lifetimes with messages message to lifeline's head self.connect(msg, msg.head, ll1, ll1.lifetime.port) self.connect(msg, msg.tail, ll2, ll2.lifetime.port) self.assertTrue(msg.subject is not None) self.assertEqual(msg.subject.messageKind, "complete") def test_disconnection(self): """Test message disconnection """ ll1 = self.create(items.LifelineItem) ll2 = self.create(items.LifelineItem) msg = self.create(items.MessageItem) self.connect(msg, msg.head, ll1) self.connect(msg, msg.tail, ll2) # one side disconnection self.disconnect(msg, msg.head) self.assertTrue(msg.subject is not None, "%s" % msg.subject) # 2nd side disconnection self.disconnect(msg, msg.tail) self.assertTrue(msg.subject is None, "%s" % msg.subject) def test_lifetime_connectivity_on_head(self): """Test lifeline's lifetime connectivity change on head connection """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) # connect message to lifeline's head, lifeline's lifetime # visibility and connectivity should change self.connect(msg, msg.head, ll) self.assertFalse(ll.lifetime.visible) self.assertFalse(ll.lifetime.connectable) self.assertEqual(ll.lifetime.MIN_LENGTH, ll.lifetime.min_length) # ... and disconnection self.disconnect(msg, msg.head) self.assertTrue(ll.lifetime.connectable) self.assertEqual(ll.lifetime.MIN_LENGTH, ll.lifetime.min_length) def test_lifetime_connectivity_on_lifetime(self): """Test lifeline's lifetime connectivity change on lifetime connection """ ll = self.create(items.LifelineItem) msg = self.create(items.MessageItem) ll.lifetime.visible = True # connect message to lifeline's lifetime, lifeline's lifetime # visibility and connectivity should be unchanged self.connect(msg, msg.head, ll, ll.lifetime.port) self.assertTrue(ll.lifetime.connectable) self.assertEqual(ll.lifetime.MIN_LENGTH_VISIBLE, ll.lifetime.min_length) # ... and disconnection self.disconnect(msg, msg.head) self.assertTrue(ll.lifetime.connectable) self.assertTrue(ll.lifetime.visible) self.assertEqual(ll.lifetime.MIN_LENGTH, ll.lifetime.min_length) class DiagramModeMessageConnectionTestCase(TestCase): def test_message_glue_cd(self): """Test gluing message on communication diagram.""" lifeline1 = self.create(items.LifelineItem) lifeline2 = self.create(items.LifelineItem) message = self.create(items.MessageItem) # make second lifeline to be in sequence diagram mode lifeline2.lifetime.visible = True # connect head of message to lifeline's head self.connect(message, message.head, lifeline1) glued = self.allow(message, message.tail, lifeline2, lifeline2.lifetime.port) # no connection possible as 2nd lifeline is in sequence diagram # mode self.assertFalse(glued) def test_message_glue_sd(self): """Test gluing message on sequence diagram.""" msg = self.create(items.MessageItem) ll1 = self.create(items.LifelineItem) ll2 = self.create(items.LifelineItem) # 1st lifeline - communication diagram # 2nd lifeline - sequence diagram ll2.lifetime.visible = True # connect lifetime of message to lifeline's lifetime self.connect(msg, msg.head, ll1, ll1.lifetime.port) glued = self.allow(msg, msg.tail, ll2) # no connection possible as 2nd lifeline is in communication # diagram mode self.assertFalse(glued) def test_messages_disconnect_cd(self): """Test disconnecting messages on communication diagram """ ll1 = self.create(items.LifelineItem) ll2 = self.create(items.LifelineItem) msg = self.create(items.MessageItem) self.connect(msg, msg.head, ll1) self.connect(msg, msg.tail, ll2) factory = self.element_factory subject = msg.subject assert subject.sendEvent and subject.receiveEvent # add some more messages m1 = UML.model.create_message(factory, subject) m2 = UML.model.create_message(factory, subject) msg.add_message(m1, False) msg.add_message(m2, False) # add some inverted messages m3 = UML.model.create_message(factory, subject, True) m4 = UML.model.create_message(factory, subject, True) msg.add_message(m3, True) msg.add_message(m4, True) messages = list(self.kindof(UML.Message)) occurrences = set(self.kindof(UML.MessageOccurrenceSpecification)) # verify integrity of messages self.assertEqual(5, len(messages)) self.assertEqual(10, len(occurrences)) for m in messages: self.assertTrue(m.sendEvent in occurrences) self.assertTrue(m.receiveEvent in occurrences) # lost/received messages self.disconnect(msg, msg.head) self.assertEqual(5, len(messages)) # verify integrity of messages self.assertEqual(10, len(occurrences)) for m in messages: self.assertTrue(m.sendEvent is None or m.sendEvent in occurrences) self.assertTrue(m.receiveEvent is None or m.receiveEvent in occurrences) # no message after full disconnection self.disconnect(msg, msg.tail) self.assertEqual(0, len(self.kindof(UML.Message))) PK!$gaphor/adapters/profiles/__init__.pyPK!r= = ,gaphor/adapters/profiles/extensionconnect.pyfrom zope import component from gaphor import UML from gaphor.adapters.connectors import RelationshipConnect from gaphor.diagram import items @component.adapter(items.ClassifierItem, items.ExtensionItem) class ExtensionConnect(RelationshipConnect): """Connect class and stereotype items using an extension item.""" def allow(self, handle, port): line = self.line subject = self.element.subject if handle is line.head: # Element at the head should be a class # (implies stereotype as well) allow = isinstance(subject, UML.Class) elif handle is line.tail: # Element at the tail should be a stereotype allow = isinstance(subject, UML.Stereotype) return allow and super(ExtensionConnect, self).allow(handle, port) def connect_subject(self, handle): element = self.element line = self.line c1 = self.get_connected(line.head) c2 = self.get_connected(line.tail) if c1 and c2: head_type = c1.subject tail_type = c2.subject # First check if we do not already contain the right subject: if line.subject: end1 = line.subject.memberEnd[0] end2 = line.subject.memberEnd[1] if (end1.type is head_type and end2.type is tail_type) or ( end2.type is head_type and end1.type is tail_type ): return # TODO: make element at head end update! c1.request_update() # Find all associations and determine if the properties on # the association ends have a type that points to the class. for assoc in self.element_factory.select(): if isinstance(assoc, UML.Extension): end1 = assoc.memberEnd[0] end2 = assoc.memberEnd[1] if (end1.type is head_type and end2.type is tail_type) or ( end2.type is head_type and end1.type is tail_type ): # check if this entry is not yet in the diagram # Return if the association is not (yet) on the canvas for item in assoc.presentation: if item.canvas is element.canvas: break else: line.subject = assoc return else: # Create a new Extension relationship relation = UML.model.extend_with_stereotype( self.element_factory, head_type, tail_type ) line.subject = relation def disconnect_subject(self, handle): """ Disconnect model element. Disconnect property (memberEnd) too, in case of end of life for Extension. """ opposite = self.line.opposite(handle) hct = self.get_connected(handle) oct = self.get_connected(opposite) if hct and oct: old = self.line.subject del self.line.subject if old and len(old.presentation) == 0: for e in old.memberEnd: e.unlink() old.unlink() component.provideAdapter(ExtensionConnect) PK! +gaphor/adapters/profiles/metaclasseditor.py""" Metaclass item editors. """ from zope import component from gi.repository import Gtk from zope.interface import implementer from gaphor import UML from gaphor.adapters.propertypages import create_hbox_label, EventWatcher from gaphor.core import _, transactional from gaphor.diagram import items from gaphor.ui.interfaces import IPropertyPage def _issubclass(c, b): try: return issubclass(c, b) except TypeError: return False @implementer(IPropertyPage) class MetaclassNameEditor(object): """ Metaclass name editor. Provides editable combo box entry with predefined list of names of UML classes. """ order = 10 NAME_LABEL = _("Name") CLASSES = list( sorted( n for n in dir(UML) if _issubclass(getattr(UML, n), UML.Element) and n != "Stereotype" ) ) def __init__(self, item): self.item = item self.size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) self.watcher = EventWatcher(item.subject) def construct(self): page = Gtk.VBox() subject = self.item.subject if not subject: return page hbox = create_hbox_label(self, page, self.NAME_LABEL) model = Gtk.ListStore(str) for c in self.CLASSES: model.append([c]) cb = Gtk.ComboBox.new_with_model_and_entry(model) completion = Gtk.EntryCompletion() completion.set_model(model) completion.set_minimum_key_length(1) completion.set_text_column(0) cb.get_child().set_completion(completion) entry = cb.get_child() entry.set_text(subject and subject.name or "") hbox.pack_start(cb, True, True, 0) page.default = entry # monitor subject.name attribute changed_id = entry.connect("changed", self._on_name_change) def handler(event): if event.element is subject and event.new_value is not None: entry.handler_block(changed_id) entry.set_text(event.new_value) entry.handler_unblock(changed_id) self.watcher.watch("name", handler).register_handlers() entry.connect("destroy", self.watcher.unregister_handlers) page.show_all() return page @transactional def _on_name_change(self, entry): self.item.subject.name = entry.get_text() component.provideAdapter( MetaclassNameEditor, adapts=[items.MetaclassItem], name="Properties" ) PK!ڜ__*gaphor/adapters/profiles/stereotypepage.py""" Stereotype property page. """ from zope import component from gi.repository import GObject, Gtk from zope.interface import implementer from gaphor import UML from gaphor.core import _, inject, transactional from gaphor.diagram.diagramitem import StereotypeSupport from gaphor.diagram import items from gaphor.ui.interfaces import IPropertyPage def create_stereotype_tree_view(model, toggle_stereotype, set_slot_value): """ Create a tree view for an editable tree model. :Parameters: model Model, for which tree view is created. """ tree_view = Gtk.TreeView.new_with_model(model) # Stereotype/Attributes col = Gtk.TreeViewColumn.new() col.set_title("%s / %s" % (_("Stereotype"), _("Attribute"))) col.set_expand(True) renderer = Gtk.CellRendererToggle() renderer.set_property("active", True) renderer.set_property("activatable", True) renderer.connect("toggled", toggle_stereotype, model, 2) col.pack_start(renderer, False) col.add_attribute(renderer, "active", 2) def show_checkbox(column, cell, model, iter, data): # value = model.get_value(iter, 4) # cell.set_property('active', value is not None) value = model.get_value(iter, 3) cell.set_property("visible", isinstance(value, UML.Stereotype)) col.set_cell_data_func(renderer, show_checkbox) renderer = Gtk.CellRendererText.new() renderer.set_property("editable", False) renderer.set_property("is-expanded", True) col.pack_start(renderer, False) col.add_attribute(renderer, "text", 0) tree_view.append_column(col) # TODO: use col.set_cell_data_func(renderer, func, None) to toggle visibility # Value renderer = Gtk.CellRendererText() renderer.set_property("is-expanded", True) renderer.connect("edited", set_slot_value, model, 1) col = Gtk.TreeViewColumn(_("Value"), renderer, text=1) col.set_expand(True) def set_editable(column, cell, model, iter, data): value = model.get_value(iter, 4) cell.set_property("editable", bool(value)) col.set_cell_data_func(renderer, set_editable) tree_view.append_column(col) # tree_view.connect('key_press_event', remove_on_keypress) # tree_view.connect('key_press_event', swap_on_keypress) return tree_view @implementer(IPropertyPage) class StereotypePage(object): order = 40 element_factory = inject("element_factory") def __init__(self, item): self.item = item self.size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) def construct(self): page = Gtk.VBox() subject = self.item.subject if subject is None: return None stereotypes = UML.model.get_stereotypes(self.element_factory, subject) if not stereotypes: return None # show stereotypes attributes toggle if isinstance(self.item, StereotypeSupport): hbox = Gtk.HBox() label = Gtk.Label(label="") hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Show stereotypes attributes")) button.set_active(self.item.show_stereotypes_attrs) button.connect("toggled", self._on_show_stereotypes_attrs_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) # stereotype attributes # self.model = StereotypeAttributes(self.item.subject) self.model = Gtk.TreeStore.new([str, str, bool, object, object, object]) tree_view = create_stereotype_tree_view( self.model, self._toggle_stereotype, self._set_value ) page.pack_start(tree_view, True, True, 0) page.show_all() self.refresh() return page @transactional def _on_show_stereotypes_attrs_change(self, button): self.item.show_stereotypes_attrs = button.get_active() self.item.request_update() def refresh(self): self.model.clear() subject = self.item.subject stereotypes = UML.model.get_stereotypes(self.element_factory, subject) instances = subject.appliedStereotype # shortcut map stereotype -> slot (InstanceSpecification) slots = {} for obj in instances: for slot in obj.slot: slots[slot.definingFeature] = slot for st in stereotypes: for obj in instances: if st in obj.classifier: break else: obj = None parent = self.model.append(None, (st.name, "", bool(obj), st, None, None)) if obj: for attr in st.ownedAttribute: if not attr.association: slot = slots.get(attr) value = slot.value if slot else "" data = (attr.name, value, True, attr, obj, slot) # print 'data', data self.model.append(parent, data) else: for attr in st.ownedAttribute: if not attr.association: data = (attr.name, "", False, attr, None, None) # print 'no data', data self.model.append(parent, data) @transactional def _set_value(self, renderer, path, value, model, col=0): iter = model.get_iter(path) self.set_slot_value(iter, value) @transactional def _toggle_stereotype(self, renderer, path, model, col): iter = model.get_iter(path) self.select_stereotype(iter) def select_stereotype(self, iter): """ Select the stereotype. """ path = self.model.get_path(iter) row = self.model[path] name, old_value, is_applied, stereotype, _, _ = row value = not is_applied subject = self.item.subject if value: UML.model.apply_stereotype(self.element_factory, subject, stereotype) else: UML.model.remove_stereotype(subject, stereotype) row[2] = value # TODO: change refresh in a refresh of the data model, rather than a clear-refresh self.refresh() def set_slot_value(self, iter, value): """ Set value of stereotype property applied to an UML element. Slot is created if instance Create valuChange value of instance spe """ path = self.model.get_path(iter) row = self.model[path] name, old_value, is_applied, attr, obj, slot = row if isinstance(attr, UML.Stereotype): return # don't edit stereotype rows if slot is None and not value: return # nothing to do and don't create slot without value if slot is None: slot = UML.model.add_slot(self.element_factory, obj, attr) assert slot if value: slot.value = value else: # no value, then remove slot del obj.slot[slot] slot = None value = "" row[1] = value row[5] = slot component.provideAdapter(StereotypePage, adapts=[UML.Element], name="Stereotypes") # vim:sw=4:et:ai PK!*gaphor/adapters/profiles/tests/__init__.pyPK!0gaphor/adapters/profiles/tests/test_extension.py""" Extension item connection adapter tests. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class ExtensionConnectorTestCase(TestCase): """ Extension item connection adapter tests. """ def test_class_glue(self): """Test extension item gluing to a class.""" ext = self.create(items.ExtensionItem) cls = self.create(items.ClassItem, UML.Class) # cannot connect extension item tail to a class glued = self.allow(ext, ext.tail, cls) self.assertFalse(glued) def test_stereotype_glue(self): """Test extension item gluing to a stereotype.""" ext = self.create(items.ExtensionItem) st = self.create(items.ClassItem, UML.Stereotype) # test precondition assert isinstance(st.subject, UML.Stereotype) # can connect extension item head to a Stereotype UML metaclass, # because it derives from Class UML metaclass glued = self.allow(ext, ext.head, st) self.assertTrue(glued) def test_glue(self): """Test extension item glue """ ext = self.create(items.ExtensionItem) st = self.create(items.ClassItem, UML.Stereotype) cls = self.create(items.ClassItem, UML.Class) glued = self.allow(ext, ext.tail, st) self.assertTrue(glued) self.connect(ext, ext.tail, st) glued = self.allow(ext, ext.head, cls) self.assertTrue(glued) def test_connection(self): """Test extension item connection """ ext = self.create(items.ExtensionItem) st = self.create(items.ClassItem, UML.Stereotype) cls = self.create(items.ClassItem, UML.Class) self.connect(ext, ext.tail, st) self.connect(ext, ext.head, cls) PK!ʆ6gaphor/adapters/profiles/tests/test_metaclasseditor.pyfrom gaphor.tests import TestCase from gaphor.adapters.profiles.metaclasseditor import MetaclassNameEditor from gaphor.diagram import items from gaphor import UML from gi.repository import Gtk class MetaclassEditorTest(TestCase): def test_name_selection(self): ci = self.create(items.MetaclassItem, UML.Class) ci.subject.name = "Class" editor = MetaclassNameEditor(ci) page = editor.construct() self.assertTrue(page) combo = page.get_children()[0].get_children()[1] self.assertSame(Gtk.ComboBox, type(combo)) self.assertEqual("Class", combo.get_child().get_text()) ci.subject.name = "Blah" self.assertEqual("Blah", combo.get_child().get_text()) PK!C_G5gaphor/adapters/profiles/tests/test_stereotypepage.pyfrom gaphor.tests import TestCase from gaphor.adapters.profiles.stereotypepage import StereotypePage from gaphor.diagram import items from gaphor import UML from gi.repository import Gtk class MetaclassEditorTest(TestCase): def test_stereotype_page_with_no_stereotype(self): ci = self.create(items.ClassItem, UML.Class) ci.subject.name = "Class" editor = StereotypePage(ci) page = editor.construct() assert page is None def test_stereotype_page_with_stereotype(self): # Create an stereotype applicable to Class types: metaclass = self.element_factory.create(UML.Class) metaclass.name = "Class" stereotype = self.element_factory.create(UML.Stereotype) stereotype.name = "NewStereotype" UML.model.create_extension(self.element_factory, metaclass, stereotype) attr = self.element_factory.create(UML.Property) attr.name = "Property" # stereotype.ownedAttribute = attr ci = self.create(items.ClassItem, UML.Class) ci.subject.name = "Foo" editor = StereotypePage(ci) page = editor.construct() editor.refresh() assert len(editor.model) == 1 assert page is not None # vim:sw=4:et:ai PK!xH gaphor/adapters/propertypages.py""" Adapters for the Property Editor. To register property pages implemented in this module, it is imported in gaphor.adapter package. # TODO: make all labels align top-left # Add hidden columns for list stores where i can put the actual object # being edited. TODO: - stereotypes - association / association ends. - Follow HIG guidelines: * Leave a 12-pixel border between the edge of the window and the nearest controls. * Leave a 12-pixel horizontal gap between a control and its label. (The gap may be bigger for other controls in the same group, due to differences in the lengths of the labels.) * Labels must be concise and make sense when taken out of context. Otherwise, users relying on screenreaders or similar assistive technologies will not always be able to immediately understand the relationship between a control and those surrounding it. * Assign access keys to all editable controls. Ensure that using the access key focuses its associated control. """ import math import logging from zope import component import gaphas.item from gaphas.decorators import AsyncIO from gi.repository import GObject from gi.repository import Gdk from gi.repository import Gtk from zope.interface import implementer from gaphor import UML from gaphor.core import _, inject, transactional from gaphor.diagram import items from gaphor.services.elementdispatcher import EventWatcher from gaphor.ui.interfaces import IPropertyPage log = logging.getLogger(__name__) class EditableTreeModel(Gtk.ListStore): """Editable GTK tree model based on ListStore model. Every row is represented by a list of editable values. Last column contains an object, which is being edited (this column is not displayed). When editable value in first column is set to empty string then object is deleted. Last row is empty and contains no object to edit. It allows to enter new values. When model is edited, then item is requested to be updated on canvas. Attributes: - _item: diagram item owning tree model """ element_factory = inject("element_factory") def __init__(self, item, cols=None): """Create new model. Parameters: - _item: diagram item owning tree model - cols: model columns, defaults to [str, object] """ if cols is None: cols = (str, object) super(EditableTreeModel, self).__init__(*cols) self._item = item for data in self._get_rows(): self.append(data) self._add_empty() def refresh(self, obj): for row in self: if row[-1] is obj: self._set_object_value(row, len(row) - 1, obj) self.row_changed(row.path, row.iter) return def _get_rows(self): """Return rows to be edited. Last column has to contain object being edited. """ raise NotImplemented def _create_object(self): """ Create new object. """ raise NotImplemented def _set_object_value(self, row, col, value): """ Update row's column with a value. """ raise NotImplemented def _swap_objects(self, o1, o2): """ Swap two objects. If objects are swapped, then return ``True``. """ raise NotImplemented def _get_object(self, iter): """ Get object from ``iter``. """ path = self.get_path(iter) return self[path][-1] def swap(self, a, b): """ Swap two list rows. Parameters: - a: path to first row - b: path to second row """ if not a or not b: return o1 = self[a][-1] o2 = self[b][-1] if o1 and o2 and self._swap_objects(o1, o2): # self._item.request_update(matrix=False) super(EditableTreeModel, self).swap(a, b) def _add_empty(self): """ Add empty row to the end of the model. """ self.append([None] * self.get_n_columns()) @transactional def set_value(self, iter, col, value): row = self[iter][:] if col == 0 and not value and row[-1]: # kill row and delete object if text of first column is empty self.remove(iter) elif col == 0 and value and not row[-1]: # create new object obj = self._create_object() row[-1] = obj self._set_object_value(row, col, value) self._add_empty() elif row[-1]: self._set_object_value(row, col, value) self.set(iter, list(range(len(row))), row) self.set(iter, list(range(len(row))), row) def remove(self, iter): """ Remove object from GTK model and destroy it. """ obj = self._get_object(iter) if obj: obj.unlink() # self._item.request_update(matrix=False) return super(EditableTreeModel, self).remove(iter) else: return iter class ClassAttributes(EditableTreeModel): """GTK tree model to edit class attributes.""" def _get_rows(self): for attr in self._item.subject.ownedAttribute: if not attr.association: yield [UML.format(attr), attr.isStatic, attr] def _create_object(self): attr = self.element_factory.create(UML.Property) self._item.subject.ownedAttribute = attr return attr @transactional def _set_object_value(self, row, col, value): attr = row[-1] if col == 0: UML.parse(attr, value) row[0] = UML.format(attr) elif col == 1: attr.isStatic = not attr.isStatic row[1] = attr.isStatic elif col == 2: # Value in attribute object changed: row[0] = UML.format(attr) row[1] = attr.isStatic def _swap_objects(self, o1, o2): return self._item.subject.ownedAttribute.swap(o1, o2) class ClassOperations(EditableTreeModel): """GTK tree model to edit class operations.""" def _get_rows(self): for operation in self._item.subject.ownedOperation: yield [ UML.format(operation), operation.isAbstract, operation.isStatic, operation, ] def _create_object(self): operation = self.element_factory.create(UML.Operation) self._item.subject.ownedOperation = operation return operation @transactional def _set_object_value(self, row, col, value): operation = row[-1] if col == 0: UML.parse(operation, value) row[0] = UML.format(operation) elif col == 1: operation.isAbstract = not operation.isAbstract row[1] = operation.isAbstract elif col == 2: operation.isStatic = not operation.isStatic row[2] = operation.isStatic elif col == 3: row[0] = UML.format(operation) row[1] = operation.isAbstract row[2] = operation.isStatic def _swap_objects(self, o1, o2): return self._item.subject.ownedOperation.swap(o1, o2) class CommunicationMessageModel(EditableTreeModel): """GTK tree model for list of messages on communication diagram.""" def __init__(self, item, cols=None, inverted=False): self.inverted = inverted super(CommunicationMessageModel, self).__init__(item, cols) def _get_rows(self): if self.inverted: for message in self._item._inverted_messages: yield [message.name, message] else: for message in self._item._messages: yield [message.name, message] def remove(self, iter): """Remove message from message item and destroy it.""" message = self._get_object(iter) item = self._item super(CommunicationMessageModel, self).remove(iter) item.remove_message(message, self.inverted) def _create_object(self): item = self._item subject = item.subject message = UML.model.create_message(self.element_factory, subject, self.inverted) item.add_message(message, self.inverted) return message def _set_object_value(self, row, col, value): message = row[-1] message.name = value row[0] = value self._item.set_message_text(message, value, self.inverted) def _swap_objects(self, o1, o2): return self._item.swap_messages(o1, o2, self.inverted) @transactional def remove_on_keypress(tree, event): """Remove selected items from GTK model on ``backspace`` keypress.""" k = Gdk.keyval_name(event.keyval).lower() if k == "backspace" or k == "kp_delete": model, iter = tree.get_selection().get_selected() if iter: model.remove(iter) @transactional def swap_on_keypress(tree, event): """Swap selected and previous (or next) items.""" k = Gdk.keyval_name(event.keyval).lower() if k == "equal" or k == "kp_add": model, iter = tree.get_selection().get_selected() model.swap(iter, model.iter_next(iter)) return True elif k == "minus": model, iter = tree.get_selection().get_selected() model.swap(iter, model.iter_previous(iter)) return True @transactional def on_text_cell_edited(renderer, path, value, model, col=0): """Update editable tree model based on fresh user input.""" iter = model.get_iter(path) model.set_value(iter, col, value) @transactional def on_bool_cell_edited(renderer, path, model, col): """Update editable tree model based on fresh user input.""" iter = model.get_iter(path) model.set_value(iter, col, renderer.get_active()) class UMLComboModel(Gtk.ListStore): """UML combo box model. Model allows to easily create a combo box with values and their labels, for example label1 -> value1 label2 -> value2 label3 -> value3 Labels are displayed by combo box and programmer has easy access to values associated with given label. Attributes: - _data: model data - _indices: dictionary of values' indices """ def __init__(self, data): super(UMLComboModel, self).__init__(str) self._indices = {} self._data = data # add labels to underlying model and store index information for i, (label, value) in enumerate(data): self.append([label]) self._indices[value] = i def get_index(self, value): """ Return index of a ``value``. """ return self._indices[value] def get_value(self, index): """ Get value for given ``index``. """ return self._data[index][1] def create_uml_combo(data, callback): """ Create a combo box using ``UMLComboModel`` model. Combo box is returned. """ model = UMLComboModel(data) combo = Gtk.ComboBox(model=model) cell = Gtk.CellRendererText() combo.pack_start(cell, True) combo.add_attribute(cell, "text", 0) combo.connect("changed", callback) return combo def create_hbox_label(adapter, page, label): """ Create a HBox with a label for given property page adapter and page itself. """ hbox = Gtk.HBox(spacing=12) label = Gtk.Label(label=label) # label.set_alignment(0.0, 0.5) adapter.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) page.pack_start(hbox, False, True, 0) return hbox def create_tree_view(model, names, tip="", ro_cols=None): """ Create a tree view for an editable tree model. :Parameters: model Model, for which tree view is created. names Names of columns. tip User interface tool tip for tree view. ro_cols Collection of indices pointing read only columns. """ if ro_cols is None: ro_cols = set() tree_view = Gtk.TreeView(model=model) n = model.get_n_columns() - 1 for name, i in zip(names, list(range(n))): col_type = model.get_column_type(i) if col_type == GObject.TYPE_STRING: renderer = Gtk.CellRendererText() renderer.set_property("editable", i not in ro_cols) renderer.set_property("is-expanded", True) renderer.connect("edited", on_text_cell_edited, model, i) col = Gtk.TreeViewColumn(name, renderer, text=i) col.set_expand(True) tree_view.append_column(col) elif col_type == GObject.TYPE_BOOLEAN: renderer = Gtk.CellRendererToggle() renderer.set_property("activatable", i not in ro_cols) renderer.connect("toggled", on_bool_cell_edited, model, i) col = Gtk.TreeViewColumn(name, renderer, active=i) col.set_expand(False) tree_view.append_column(col) tree_view.connect("key_press_event", remove_on_keypress) tree_view.connect("key_press_event", swap_on_keypress) tip = ( tip + """ Press ENTER to edit item, BS/DEL to remove item. Use -/= to move items up or down.\ """ ) tree_view.set_tooltip_text(tip) return tree_view @implementer(IPropertyPage) @component.adapter(UML.Comment) class CommentItemPropertyPage(object): """Property page for Comments.""" order = 0 def __init__(self, subject): self.subject = subject self.watcher = EventWatcher(subject) def construct(self): subject = self.subject page = Gtk.VBox() if not subject: return page label = Gtk.Label(label=_("Comment")) label.set_justify(Gtk.Justification.LEFT) page.pack_start(label, False, True, 0) buffer = Gtk.TextBuffer() if subject.body: buffer.set_text(subject.body) text_view = Gtk.TextView() text_view.set_buffer(buffer) text_view.show() text_view.set_size_request(-1, 100) page.pack_start(text_view, True, True, 0) page.default = text_view changed_id = buffer.connect("changed", self._on_body_change) def handler(event): if not text_view.props.has_focus: buffer.handler_block(changed_id) buffer.set_text(event.new_value) buffer.handler_unblock(changed_id) self.watcher.watch("body", handler).register_handlers() text_view.connect("destroy", self.watcher.unregister_handlers) return page @transactional def _on_body_change(self, buffer): self.subject.body = buffer.get_text( buffer.get_start_iter(), buffer.get_end_iter(), False ) component.provideAdapter(CommentItemPropertyPage, name="Properties") @implementer(IPropertyPage) @component.adapter(UML.NamedElement) class NamedElementPropertyPage(object): """An adapter which works for any named item view. It also sets up a table view which can be extended. """ order = 10 NAME_LABEL = _("Name") def __init__(self, subject): assert subject is None or isinstance(subject, UML.NamedElement), "%s" % type( subject ) self.subject = subject self.watcher = EventWatcher(subject) self.size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL) def construct(self): page = Gtk.VBox() subject = self.subject if not subject: return page hbox = create_hbox_label(self, page, self.NAME_LABEL) entry = Gtk.Entry() entry.set_text(subject and subject.name or "") hbox.pack_start(entry, True, True, 0) page.default = entry # monitor subject.name attribute changed_id = entry.connect("changed", self._on_name_change) def handler(event): if event.element is subject and event.new_value is not None: entry.handler_block(changed_id) entry.set_text(event.new_value) entry.handler_unblock(changed_id) self.watcher.watch("name", handler).register_handlers() entry.connect("destroy", self.watcher.unregister_handlers) return page @transactional def _on_name_change(self, entry): self.subject.name = entry.get_text() component.provideAdapter(NamedElementPropertyPage, name="Properties") class NamedItemPropertyPage(NamedElementPropertyPage): """ Base class for named item based adapters. """ def __init__(self, item): self.item = item super(NamedItemPropertyPage, self).__init__(item.subject) @component.adapter(UML.Class) class ClassPropertyPage(NamedElementPropertyPage): """Adapter which shows a property page for a class view.""" def __init__(self, subject): super(ClassPropertyPage, self).__init__(subject) def construct(self): page = super(ClassPropertyPage, self).construct() if not self.subject: return page # Abstract toggle hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) self.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Abstract")) button.set_active(self.subject.isAbstract) button.connect("toggled", self._on_abstract_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) return page @transactional def _on_abstract_change(self, button): self.subject.isAbstract = button.get_active() component.provideAdapter(ClassPropertyPage, name="Properties") @component.adapter(items.InterfaceItem) class InterfacePropertyPage(NamedItemPropertyPage): """Adapter which shows a property page for an interface view.""" def construct(self): page = super(InterfacePropertyPage, self).construct() item = self.item # Fold toggle hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) self.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(_("Folded")) button.set_active(item.folded) button.connect("toggled", self._on_fold_change) connected_items = [c.item for c in item.canvas.get_connections(connected=item)] allowed = (items.DependencyItem, items.ImplementationItem) can_fold = ( len(connected_items) == 0 or len(connected_items) == 1 and isinstance(connected_items[0], allowed) ) button.set_sensitive(can_fold) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) return page @transactional def _on_fold_change(self, button): item = self.item connected_items = [c.item for c in item.canvas.get_connections(connected=item)] assert len(connected_items) <= 1 line = None if len(connected_items) == 1: line = connected_items[0] fold = button.get_active() if fold: item.folded = item.FOLDED_PROVIDED else: item.folded = item.FOLDED_NONE if line: if fold and isinstance(line, items.DependencyItem): item.folded = item.FOLDED_REQUIRED line._solid = fold constraint = line.canvas.get_connection(line.head).constraint constraint.ratio_x = 0.5 constraint.ratio_y = 0.5 line.request_update() component.provideAdapter(InterfacePropertyPage, name="Properties") @implementer(IPropertyPage) @component.adapter(items.ClassItem) class AttributesPage(object): """An editor for attributes associated with classes and interfaces.""" order = 20 def __init__(self, item): super(AttributesPage, self).__init__() self.item = item self.watcher = EventWatcher(item.subject) def construct(self): page = Gtk.VBox() if not self.item.subject: return page # Show attributes toggle hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Show attributes")) button.set_active(self.item.show_attributes) button.connect("toggled", self._on_show_attributes_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) def create_model(): return ClassAttributes(self.item, (str, bool, object)) self.model = create_model() tip = """\ Add and edit class attributes according to UML syntax. Attribute syntax examples - attr - + attr: int - # /attr: int """ tree_view = create_tree_view(self.model, (_("Attributes"), _("S")), tip) page.pack_start(tree_view, True, True, 0) @AsyncIO(single=True) def handler(event): # Single it's asynchronous, make sure all properties are still there if not tree_view.props.has_focus and self.item and self.item.subject: self.model = create_model() tree_view.set_model(self.model) self.watcher.watch("ownedAttribute.name", handler).watch( "ownedAttribute.isDerived", handler ).watch("ownedAttribute.visibility", handler).watch( "ownedAttribute.isStatic", handler ).watch( "ownedAttribute.lowerValue", handler ).watch( "ownedAttribute.upperValue", handler ).watch( "ownedAttribute.defaultValue", handler ).watch( "ownedAttribute.typeValue", handler ).register_handlers() tree_view.connect("destroy", self.watcher.unregister_handlers) return page @transactional def _on_show_attributes_change(self, button): self.item.show_attributes = button.get_active() self.item.request_update() component.provideAdapter(AttributesPage, name="Attributes") @implementer(IPropertyPage) @component.adapter(items.ClassItem) class OperationsPage(object): """An editor for operations associated with classes and interfaces.""" order = 30 def __init__(self, item): super(OperationsPage, self).__init__() self.item = item self.watcher = EventWatcher(item.subject) def construct(self): page = Gtk.VBox() if not self.item.subject: return page # Show operations toggle hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Show operations")) button.set_active(self.item.show_operations) button.connect("toggled", self._on_show_operations_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) def create_model(): return ClassOperations(self.item, (str, bool, bool, object)) self.model = create_model() tip = """\ Add and edit class operations according to UML syntax. Operation syntax examples - call() - + call(a: int, b: str) - # call(a: int: b: str): bool """ tree_view = create_tree_view(self.model, (_("Operation"), _("A"), _("S")), tip) page.pack_start(tree_view, True, True, 0) @AsyncIO(single=True) def handler(event): if not tree_view.props.has_focus and self.item and self.item.subject: self.model = create_model() tree_view.set_model(self.model) self.watcher.watch("ownedOperation.name", handler).watch( "ownedOperation.isAbstract", handler ).watch("ownedOperation.visibility", handler).watch( "ownedOperation.returnResult.lowerValue", handler ).watch( "ownedOperation.returnResult.upperValue", handler ).watch( "ownedOperation.returnResult.typeValue", handler ).watch( "ownedOperation.formalParameter.lowerValue", handler ).watch( "ownedOperation.formalParameter.upperValue", handler ).watch( "ownedOperation.formalParameter.typeValue", handler ).watch( "ownedOperation.formalParameter.defaultValue", handler ).register_handlers() tree_view.connect("destroy", self.watcher.unregister_handlers) return page @transactional def _on_show_operations_change(self, button): self.item.show_operations = button.get_active() self.item.request_update() component.provideAdapter(OperationsPage, name="Operations") @implementer(IPropertyPage) @component.adapter(items.DependencyItem) class DependencyPropertyPage(object): """Dependency item editor.""" order = 0 element_factory = inject("element_factory") DEPENDENCY_TYPES = ( (_("Dependency"), UML.Dependency), (_("Usage"), UML.Usage), (_("Realization"), UML.Realization), (_("Implementation"), UML.Implementation), ) def __init__(self, item): super(DependencyPropertyPage, self).__init__() self.item = item self.size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL) self.watcher = EventWatcher(self.item) def construct(self): page = Gtk.VBox() hbox = create_hbox_label(self, page, _("Dependency type")) self.combo = create_uml_combo( self.DEPENDENCY_TYPES, self._on_dependency_type_change ) hbox.pack_start(self.combo, False, True, 0) hbox = create_hbox_label(self, page, "") button = Gtk.CheckButton(_("Automatic")) button.set_active(self.item.auto_dependency) button.connect("toggled", self._on_auto_dependency_change) hbox.pack_start(button, True, True, 0) self.watcher.watch("subject", self._on_subject_change).register_handlers() button.connect("destroy", self.watcher.unregister_handlers) self.update() return page def _on_subject_change(self, event): self.update() def update(self): """ Update dependency type combo box. Disallow dependency type when dependency is established. """ combo = self.combo item = self.item index = combo.get_model().get_index(item.dependency_type) combo.props.sensitive = not item.auto_dependency combo.set_active(index) @transactional def _on_dependency_type_change(self, combo): combo = self.combo cls = combo.get_model().get_value(combo.get_active()) self.item.dependency_type = cls if self.item.subject: self.element_factory.swap_element(self.item.subject, cls) self.item.request_update() @transactional def _on_auto_dependency_change(self, button): self.item.auto_dependency = button.get_active() self.update() component.provideAdapter(DependencyPropertyPage, name="Properties") @component.adapter(items.AssociationItem) class AssociationPropertyPage(NamedItemPropertyPage): """ """ def construct_end(self, title, end): if not end.subject: return None # TODO: use Gtk.Frame here frame = Gtk.Frame.new("%s (: %s)" % (title, end.subject.type.name)) vbox = Gtk.VBox() vbox.set_border_width(6) vbox.set_spacing(6) frame.add(vbox) self.create_pages(end, vbox) return frame def construct(self): page = super(AssociationPropertyPage, self).construct() if not self.subject: return page hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) self.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Show direction")) button.set_active(self.item.show_direction) button.connect("toggled", self._on_show_direction_change) hbox.pack_start(button, True, True, 0) button = Gtk.Button(label=_("Invert Direction")) button.connect("clicked", self._on_invert_direction_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) box = self.construct_end(_("Head"), self.item.head_end) if box: page.pack_start(box, False, True, 0) box = self.construct_end(_("Tail"), self.item.tail_end) if box: page.pack_start(box, False, True, 0) self.update() return page def update(self): pass @transactional def _on_show_direction_change(self, button): self.item.show_direction = button.get_active() @transactional def _on_invert_direction_change(self, button): self.item.invert_direction() def get_adapters(self, item): """ Return an ordered list of (order, name, adapter). """ adaptermap = {} try: if item.subject: for name, adapter in component.getAdapters( [item.subject], IPropertyPage ): adaptermap[name] = (adapter.order, name, adapter) except AttributeError: pass for name, adapter in component.getAdapters([item], IPropertyPage): adaptermap[name] = (adapter.order, name, adapter) adapters = sorted(adaptermap.values()) return adapters def create_pages(self, item, vbox): """ Load all tabs that can operate on the given item. The first item will not contain a title. """ adapters = self.get_adapters(item) first = True for _, name, adapter in adapters: try: page = adapter.construct() if page is None: continue if first: vbox.pack_start(page, False, True, 0) first = False else: expander = Gtk.Expander() expander.set_use_markup(True) expander.set_label("%s" % name) expander.add(page) expander.show_all() vbox.pack_start(expander, False, True, 0) except Exception as e: log.error( "Could not construct property page for " + name, exc_info=True ) component.provideAdapter(AssociationPropertyPage, name="Properties") @implementer(IPropertyPage) @component.adapter(UML.Property) class AssociationEndPropertyPage(object): """Property page for association end properties.""" order = 0 NAVIGABILITY = [None, False, True] def __init__(self, subject): self.subject = subject self.watcher = EventWatcher(subject) def construct(self): vbox = Gtk.VBox() entry = Gtk.Entry() # entry.set_text(UML.format(self.subject, visibility=True, is_derived=Truemultiplicity=True) or '') # monitor subject attribute (all, cause it contains many children) changed_id = entry.connect("changed", self._on_end_name_change) def handler(event): if not entry.props.has_focus: entry.handler_block(changed_id) entry.set_text( UML.format( self.subject, visibility=True, is_derived=True, multiplicity=True, ) or "" ) # entry.set_text(UML.format(self.subject, multiplicity=True) or '') entry.handler_unblock(changed_id) handler(None) self.watcher.watch("name", handler).watch("aggregation", handler).watch( "visibility", handler ).watch("lowerValue", handler).watch("upperValue", handler).register_handlers() entry.connect("destroy", self.watcher.unregister_handlers) vbox.pack_start(entry, True, True, 0) entry.set_tooltip_text( """\ Enter attribute name and multiplicity, for example - name + name [1] - name [1..2] ~ 1..2 - [1..2]\ """ ) combo = Gtk.ComboBoxText() for t in ("Unknown navigation", "Not navigable", "Navigable"): combo.append_text(t) nav = self.subject.navigability combo.set_active(self.NAVIGABILITY.index(nav)) combo.connect("changed", self._on_navigability_change) vbox.pack_start(combo, False, True, 0) combo = Gtk.ComboBoxText() for t in ("No aggregation", "Shared", "Composite"): combo.append_text(t) combo.set_active( ["none", "shared", "composite"].index(self.subject.aggregation) ) combo.connect("changed", self._on_aggregation_change) vbox.pack_start(combo, False, True, 0) return vbox @transactional def _on_end_name_change(self, entry): UML.parse(self.subject, entry.get_text()) @transactional def _on_navigability_change(self, combo): nav = self.NAVIGABILITY[combo.get_active()] UML.model.set_navigability(self.subject.association, self.subject, nav) @transactional def _on_aggregation_change(self, combo): self.subject.aggregation = ("none", "shared", "composite")[combo.get_active()] component.provideAdapter(AssociationEndPropertyPage, name="Properties") @implementer(IPropertyPage) @component.adapter(gaphas.item.Line) class LineStylePage(object): """Basic line style properties: color, orthogonal, etc.""" order = 400 def __init__(self, item): super(LineStylePage, self).__init__() self.item = item self.size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL) def construct(self): page = Gtk.VBox() hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) self.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Orthogonal")) button.set_active(self.item.orthogonal) button.connect("toggled", self._on_orthogonal_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) if len(self.item.handles()) < 3: # Only one segment button.props.sensitive = False hbox = Gtk.HBox() label = Gtk.Label(label="") label.set_justify(Gtk.Justification.LEFT) self.size_group.add_widget(label) hbox.pack_start(label, False, True, 0) button = Gtk.CheckButton(label=_("Horizontal")) button.set_active(self.item.horizontal) button.connect("toggled", self._on_horizontal_change) hbox.pack_start(button, True, True, 0) page.pack_start(hbox, False, True, 0) return page @transactional def _on_orthogonal_change(self, button): self.item.orthogonal = button.get_active() @transactional def _on_horizontal_change(self, button): self.item.horizontal = button.get_active() component.provideAdapter(LineStylePage, name="Style") @component.adapter(items.ObjectNodeItem) class ObjectNodePropertyPage(NamedItemPropertyPage): """ """ ORDERING_VALUES = ["unordered", "ordered", "LIFO", "FIFO"] def construct(self): page = super(ObjectNodePropertyPage, self).construct() subject = self.subject if not subject: return page hbox = create_hbox_label(self, page, _("Upper bound")) entry = Gtk.Entry() entry.set_text(subject.upperBound or "") entry.connect("changed", self._on_upper_bound_change) hbox.pack_start(entry, True, True, 0) hbox = create_hbox_label(self, page, "") combo = Gtk.ComboBoxText() for v in self.ORDERING_VALUES: combo.append_text(v) combo.set_active(self.ORDERING_VALUES.index(subject.ordering)) combo.connect("changed", self._on_ordering_change) hbox.pack_start(combo, False, True, 0) hbox = create_hbox_label(self, page, "") button = Gtk.CheckButton(_("Ordering")) button.set_active(self.item.show_ordering) button.connect("toggled", self._on_ordering_show_change) hbox.pack_start(button, False, True, 0) return page def update(self): pass @transactional def _on_upper_bound_change(self, entry): value = entry.get_text().strip() self.item.set_upper_bound(value) @transactional def _on_ordering_change(self, combo): value = self.ORDERING_VALUES[combo.get_active()] self.subject.ordering = value @transactional def _on_ordering_show_change(self, button): self.item.show_ordering = button.get_active() self.item.set_ordering(self.subject.ordering) component.provideAdapter(ObjectNodePropertyPage, name="Properties") @component.adapter(items.ForkNodeItem) class JoinNodePropertyPage(NamedItemPropertyPage): """ """ def construct(self): page = super(JoinNodePropertyPage, self).construct() subject = self.subject if not subject: return page hbox = Gtk.HBox() page.pack_start(hbox, False, True, 0) if isinstance(subject, UML.JoinNode): hbox = create_hbox_label(self, page, _("Join specification")) entry = Gtk.Entry() entry.set_text(subject.joinSpec or "") entry.connect("changed", self._on_join_spec_change) hbox.pack_start(entry, True, True, 0) button = Gtk.CheckButton(_("Horizontal")) button.set_active(self.item.matrix[2] != 0) button.connect("toggled", self._on_horizontal_change) page.pack_start(button, False, True, 0) return page def update(self): pass @transactional def _on_join_spec_change(self, entry): value = entry.get_text().strip() self.subject.joinSpec = value def _on_horizontal_change(self, button): if button.get_active(): self.item.matrix.rotate(math.pi / 2) else: self.item.matrix.rotate(-math.pi / 2) self.item.request_update() component.provideAdapter(JoinNodePropertyPage, name="Properties") class FlowPropertyPageAbstract(NamedElementPropertyPage): """Flow item element editor.""" def construct(self): page = super(FlowPropertyPageAbstract, self).construct() subject = self.subject if not subject: return page hbox = create_hbox_label(self, page, _("Guard")) entry = Gtk.Entry() entry.set_text(subject.guard or "") changed_id = entry.connect("changed", self._on_guard_change) hbox.pack_start(entry, True, True, 0) def handler(event): entry.handler_block(changed_id) v = event.new_value entry.set_text(v if v else "") entry.handler_unblock(changed_id) self.watcher.watch("guard", handler).register_handlers() entry.connect("destroy", self.watcher.unregister_handlers) return page @transactional def _on_guard_change(self, entry): value = entry.get_text().strip() self.subject.guard = value # TODO: unify ObjectFlowPropertyPage and ControlFlowPropertyPage # after introducing common class for element editors @component.adapter(UML.ControlFlow) class ControlFlowPropertyPage(FlowPropertyPageAbstract): pass @component.adapter(UML.ObjectFlow) class ObjectFlowPropertyPage(FlowPropertyPageAbstract): pass component.provideAdapter(ControlFlowPropertyPage, name="Properties") component.provideAdapter(ObjectFlowPropertyPage, name="Properties") @component.adapter(items.ComponentItem) class ComponentPropertyPage(NamedItemPropertyPage): """ """ def construct(self): page = super(ComponentPropertyPage, self).construct() subject = self.subject if not subject: return page hbox = Gtk.HBox() page.pack_start(hbox, False, True, 0) button = Gtk.CheckButton(_("Indirectly instantiated")) button.set_active(subject.isIndirectlyInstantiated) button.connect("toggled", self._on_ii_change) hbox.pack_start(button, False, True, 0) return page def update(self): pass @transactional def _on_ii_change(self, button): """ Called when user clicks "Indirectly instantiated" check button. """ subject = self.subject if subject: subject.isIndirectlyInstantiated = button.get_active() component.provideAdapter(ComponentPropertyPage, name="Properties") @component.adapter(items.MessageItem) class MessagePropertyPage(NamedItemPropertyPage): """Property page for editing message items. When message is on communication diagram, then additional messages can be added. On sequence diagram sort of message can be changed. """ element_factory = inject("element_factory") NAME_LABEL = _("Message") MESSAGE_SORT = ( ("Call", "synchCall"), ("Asynchronous", "asynchCall"), ("Signal", "asynchSignal"), ("Create", "createMessage"), ("Delete", "deleteMessage"), ("Reply", "reply"), ) def construct(self): page = super(MessagePropertyPage, self).construct() item = self.item subject = item.subject if not subject: return page if item.is_communication(): self._messages = CommunicationMessageModel(item) tree_view = create_tree_view(self._messages, (_("Message"),)) tree_view.set_headers_visible(False) frame = Gtk.Frame.new(label=_("Additional Messages")) frame.add(tree_view) page.pack_start(frame, True, True, 0) self._inverted_messages = CommunicationMessageModel(item, inverted=True) tree_view = create_tree_view(self._inverted_messages, (_("Message"),)) tree_view.set_headers_visible(False) frame = Gtk.Frame.new(label=_("Inverted Messages")) frame.add(tree_view) page.pack_end(frame, True, True, 0) else: hbox = create_hbox_label(self, page, _("Message sort")) sort_data = self.MESSAGE_SORT lifeline = None cinfo = item.canvas.get_connection(item.tail) if cinfo: lifeline = cinfo.connected # disallow connecting two delete messages to a lifeline if ( lifeline and lifeline.is_destroyed and subject.messageSort != "deleteMessage" ): sort_data = list(sort_data) assert sort_data[4][1] == "deleteMessage" del sort_data[4] combo = self.combo = create_uml_combo( sort_data, self._on_message_sort_change ) hbox.pack_start(combo, False, True, 0) index = combo.get_model().get_index(subject.messageSort) combo.set_active(index) return page @transactional def _on_message_sort_change(self, combo): """Update message item's message sort information.""" combo = self.combo ms = combo.get_model().get_value(combo.get_active()) item = self.item subject = item.subject lifeline = None cinfo = item.canvas.get_connection(item.tail) if cinfo: lifeline = cinfo.connected # # allow only one delete message to connect to lifeline's lifetime # destroyed status can be changed only by delete message itself # if lifeline: if subject.messageSort == "deleteMessage" or not lifeline.is_destroyed: is_destroyed = ms == "deleteMessage" lifeline.is_destroyed = is_destroyed # TODO: is required here? lifeline.request_update() subject.messageSort = ms # TODO: is required here? item.request_update() component.provideAdapter(MessagePropertyPage, name="Properties") PK!Zl gaphor/adapters/relationships.py""" This module is not yet ready to be used for adapters. Here the logic should be placed that allows the application to draw already existing relationships on the diagram when a model element is added (e.g. by drag and drop). """ from zope import component ###class AssociationRelationship(Relationship): ### """Relationship for associations. ### """ ### def relationship(self, line, head_subject = None, tail_subject = None): ### # First check if we do not already contain the right subject: ### if line.subject: ### end1 = line.subject.memberEnd[0] ### end2 = line.subject.memberEnd[1] ### if (end1.type is head_type and end2.type is tail_type) \ ### or (end2.type is head_type and end1.type is tail_type): ### return ### ### # Find all associations and determine if the properties on the ### # association ends have a type that points to the class. ### Association = UML.Association ### for assoc in resource(UML.ElementFactory).itervalues(): ### if isinstance(assoc, Association): ### #print 'assoc.memberEnd', assoc.memberEnd ### end1 = assoc.memberEnd[0] ### end2 = assoc.memberEnd[1] ### if (end1.type is head_type and end2.type is tail_type) \ ### or (end2.type is head_type and end1.type is tail_type): ### # check if this entry is not yet in the diagram ### # Return if the association is not (yet) on the canvas ### for item in assoc.presentation: ### if item.canvas is line.canvas: ### break ### else: ### return assoc ### return None PK!y'bb"gaphor/adapters/states/__init__.pyfrom gaphor.adapters.states import propertypages from gaphor.adapters.states import vertexconnect PK!C!k 'gaphor/adapters/states/propertypages.py"""State items property pages. To register property pages implemented in this module, it is imported in gaphor.adapter package. """ from gi.repository import Gtk from gaphor.core import _, inject, transactional from gaphor import UML from gaphor.diagram import items from zope import interface, component from gaphor.adapters.propertypages import NamedItemPropertyPage, create_hbox_label @component.adapter(items.TransitionItem) class TransitionPropertyPage(NamedItemPropertyPage): """Transition property page allows to edit guard specification.""" element_factory = inject("element_factory") def construct(self): page = super(TransitionPropertyPage, self).construct() subject = self.subject if not subject: return page hbox = create_hbox_label(self, page, _("Guard")) entry = Gtk.Entry() v = subject.guard.specification entry.set_text(v if v else "") entry.connect("changed", self._on_guard_change) changed_id = entry.connect("changed", self._on_guard_change) hbox.pack_start(entry, True, True, 0) def handler(event): entry.handler_block(changed_id) v = event.new_value entry.set_text(v if v else "") entry.handler_unblock(changed_id) self.watcher.watch( "guard.specification", handler ).register_handlers() entry.connect("destroy", self.watcher.unregister_handlers) return page def update(self): pass @transactional def _on_guard_change(self, entry): value = entry.get_text().strip() self.subject.guard.specification = value component.provideAdapter(TransitionPropertyPage, name="Properties") @component.adapter(items.StateItem) class StatePropertyPage(NamedItemPropertyPage): """State property page.""" element_factory = inject("element_factory") def construct(self): page = super(StatePropertyPage, self).construct() subject = self.subject if not subject: return page hbox = create_hbox_label(self, page, _("Entry")) entry = Gtk.Entry() if self.item._entry.subject: entry.set_text(self.item._entry.subject.name) entry.connect("changed", self._on_text_change, self.item.set_entry) hbox.pack_start(entry, True, True, 0) hbox = create_hbox_label(self, page, _("Exit")) entry = Gtk.Entry() if self.item._exit.subject: entry.set_text(self.item._exit.subject.name) entry.connect("changed", self._on_text_change, self.item.set_exit) hbox.pack_start(entry, True, True, 0) hbox = create_hbox_label(self, page, _("Do Activity")) entry = Gtk.Entry() if self.item._do_activity.subject: entry.set_text(self.item._do_activity.subject.name) entry.connect("changed", self._on_text_change, self.item.set_do_activity) hbox.pack_start(entry, True, True, 0) page.show_all() return page def update(self): pass @transactional def _on_text_change(self, entry, method): value = entry.get_text().strip() method(value) component.provideAdapter(StatePropertyPage, name="Properties") PK!(gaphor/adapters/states/tests/__init__.pyPK!(ߎm887gaphor/adapters/states/tests/test_transition_connect.py""" Test transition item and state vertices connections. """ from gaphor.tests import TestCase from zope import component from gaphor import UML from gaphor.diagram import items from gaphor.diagram.interfaces import IConnect class TransitionConnectorTestCase(TestCase): services = TestCase.services def test_vertex_connect(self): """Test transition to state vertex connection """ v1 = self.create(items.StateItem, UML.State) v2 = self.create(items.StateItem, UML.State) t = self.create(items.TransitionItem) assert t.subject is None # connect vertices with transition self.connect(t, t.head, v1) self.connect(t, t.tail, v2) self.assertTrue(t.subject is not None) self.assertEqual(1, len(self.kindof(UML.Transition))) self.assertEqual(t.subject, v1.subject.outgoing[0]) self.assertEqual(t.subject, v2.subject.incoming[0]) self.assertEqual(t.subject.source, v1.subject) self.assertEqual(t.subject.target, v2.subject) def test_vertex_reconnect(self): """Test transition to state vertex reconnection """ v1 = self.create(items.StateItem, UML.State) v2 = self.create(items.StateItem, UML.State) v3 = self.create(items.StateItem, UML.State) t = self.create(items.TransitionItem) assert t.subject is None # connect: v1 -> v2 self.connect(t, t.head, v1) self.connect(t, t.tail, v2) s = t.subject s.name = "tname" s.guard.specification = "tguard" # reconnect: v1 -> v3 self.connect(t, t.tail, v3) self.assertSame(s, t.subject) self.assertEqual(1, len(self.kindof(UML.Transition))) self.assertEqual(t.subject, v1.subject.outgoing[0]) self.assertEqual(t.subject, v3.subject.incoming[0]) self.assertEqual(t.subject.source, v1.subject) self.assertEqual(t.subject.target, v3.subject) self.assertEqual(0, len(v2.subject.incoming)) self.assertEqual(0, len(v2.subject.outgoing)) def test_vertex_disconnect(self): """Test transition and state vertices disconnection """ t = self.create(items.TransitionItem) v1 = self.create(items.StateItem, UML.State) v2 = self.create(items.StateItem, UML.State) self.connect(t, t.head, v1) self.connect(t, t.tail, v2) assert t.subject is not None self.assertEqual(1, len(self.kindof(UML.Transition))) # test preconditions assert t.subject == v1.subject.outgoing[0] assert t.subject == v2.subject.incoming[0] self.disconnect(t, t.tail) self.assertTrue(t.subject is None) self.disconnect(t, t.head) self.assertTrue(t.subject is None) def test_initial_pseudostate_connect(self): """Test transition and initial pseudostate connection """ v1 = self.create(items.InitialPseudostateItem, UML.Pseudostate) v2 = self.create(items.StateItem, UML.State) t = self.create(items.TransitionItem) assert t.subject is None # connect head of transition to an initial pseudostate self.connect(t, t.head, v1) self.assertTrue(t.subject is None) # connect tail of transition to a state self.connect(t, t.tail, v2) self.assertTrue(t.subject is not None) self.assertEqual(1, len(self.kindof(UML.Transition))) # test preconditions assert t.subject == v1.subject.outgoing[0] assert t.subject == v2.subject.incoming[0] # we should not be able to connect two transitions to initial # pseudostate t2 = self.create(items.TransitionItem) # connection to `t2` should not be possible as v1 is already connected # to `t` glued = self.allow(t2, t2.head, v1) self.assertFalse(glued) self.assertTrue(self.get_connected(t2.head) is None) def test_initial_pseudostate_disconnect(self): """Test transition and initial pseudostate disconnection """ v1 = self.create(items.InitialPseudostateItem, UML.Pseudostate) v2 = self.create(items.StateItem, UML.State) t = self.create(items.TransitionItem) assert t.subject is None # connect head of transition to an initial pseudostate self.connect(t, t.head, v1) self.assertTrue(self.get_connected(t.head)) # perform the test self.disconnect(t, t.head) self.assertFalse(self.get_connected(t.head)) def test_initial_pseudostate_tail_glue(self): """Test transition tail and initial pseudostate gluing.""" v1 = self.create(items.InitialPseudostateItem, UML.Pseudostate) t = self.create(items.TransitionItem) assert t.subject is None # no tail connection should be possible glued = self.allow(t, t.tail, v1) self.assertFalse(glued) def test_final_state_connect(self): """Test transition to final state connection """ v1 = self.create(items.StateItem, UML.State) v2 = self.create(items.FinalStateItem, UML.FinalState) t = self.create(items.TransitionItem) # connect head of transition to a state self.connect(t, t.head, v1) # check if transition can connect to final state glued = self.allow(t, t.tail, v2) self.assertTrue(glued) # and connect tail of transition to final state self.connect(t, t.tail, v2) self.assertTrue(t.subject is not None) self.assertEqual(1, len(self.kindof(UML.Transition))) self.assertEqual(t.subject, v1.subject.outgoing[0]) self.assertEqual(t.subject, v2.subject.incoming[0]) self.assertEqual(t.subject.source, v1.subject) self.assertEqual(t.subject.target, v2.subject) def test_final_state_head_glue(self): """Test transition head to final state connection """ v = self.create(items.FinalStateItem, UML.FinalState) t = self.create(items.TransitionItem) glued = self.allow(t, t.head, v) self.assertFalse(glued) PK!?V 'gaphor/adapters/states/vertexconnect.py""" Connection between two state machine vertices (state, pseudostate) using transition. To register connectors implemented in this module, it is imported in gaphor.adapter package. """ from zope import interface, component from gaphor import UML from gaphor.diagram import items from gaphor.adapters.connectors import RelationshipConnect class VertexConnect(RelationshipConnect): """ Abstract relationship between two state vertices. """ def reconnect(self, handle, port): self.reconnect_relationship( handle, UML.Transition.source, UML.Transition.target ) def connect_subject(self, handle): relation = self.relationship_or_new( UML.Transition, UML.Transition.source, UML.Transition.target ) self.line.subject = relation if relation.guard is None: relation.guard = self.element_factory.create(UML.Constraint) @component.adapter(items.VertexItem, items.TransitionItem) class TransitionConnect(VertexConnect): """Connect two state vertices using transition item.""" def allow(self, handle, port): """ Glue transition handle and vertex item. Guard from connecting transition's head with final state. """ line = self.line subject = self.element.subject is_final = isinstance(subject, UML.FinalState) if ( isinstance(subject, UML.State) and not is_final or handle is line.tail and is_final ): return super(TransitionConnect, self).allow(handle, port) else: return None component.provideAdapter(TransitionConnect) @component.adapter(items.InitialPseudostateItem, items.TransitionItem) class InitialPseudostateTransitionConnect(VertexConnect): """Connect initial pseudostate using transition item. It modifies InitialPseudostateItem._connected attribute to disallow connection of more than one transition. """ def allow(self, handle, port): """ Glue to initial pseudostate with transition's head and when there are no transitions connected. """ line = self.line element = self.element subject = element.subject # Check if no other items are connected connections = self.canvas.get_connections(connected=element) connected_items = [ c for c in connections if isinstance(c.item, items.TransitionItem) and c.item is not line ] if handle is line.head and not any(connected_items): return super(InitialPseudostateTransitionConnect, self).allow(handle, port) else: return None component.provideAdapter(InitialPseudostateTransitionConnect) @component.adapter(items.HistoryPseudostateItem, items.TransitionItem) class HistoryPseudostateTransitionConnect(VertexConnect): """Connect history pseudostate using transition item. It modifies InitialPseudostateItem._connected attribute to disallow connection of more than one transition. """ def allow(self, handle, port): """ """ return super(HistoryPseudostateTransitionConnect, self).allow(handle, port) component.provideAdapter(HistoryPseudostateTransitionConnect) PK!!gaphor/adapters/tests/__init__.pyPK!V\'vv%gaphor/adapters/tests/test_comment.py""" Comment and comment line items connection adapters tests. """ from gaphor import UML from gaphor.diagram import items from gaphor.tests import TestCase class CommentLineTestCase(TestCase): services = TestCase.services + ["sanitizer"] # NOTE: Still have to test what happens if one Item at the CommentLineItem # end is removed, while the item still has references and is not # removed itself. def test_commentline_annotated_element(self): """Test comment line item annotated element creation """ comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) self.connect(line, line.head, comment) # connected, but no annotated element yet self.assertTrue(self.get_connected(line.head) is not None) self.assertFalse(comment.subject.annotatedElement) def test_commentline_same_comment_glue(self): """Test comment line item gluing to already connected comment item.""" comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) self.connect(line, line.head, comment) glued = self.allow(line, line.tail, comment) self.assertFalse(glued) def test_commentline_element_connect(self): """Test comment line connecting to comment and actor items. """ comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) ac = self.create(items.ActorItem, UML.Actor) self.connect(line, line.head, comment) self.connect(line, line.tail, ac) self.assertTrue(self.get_connected(line.tail) is ac) self.assertEqual(1, len(comment.subject.annotatedElement)) self.assertTrue(ac.subject in comment.subject.annotatedElement) def test_commentline_element_connect(self): """Test comment line connecting to comment and actor items. """ comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) ac = self.create(items.ActorItem, UML.Actor) self.connect(line, line.head, comment) self.connect(line, line.tail, ac) self.assertTrue(self.get_connected(line.tail) is ac) self.assertEqual(1, len(comment.subject.annotatedElement)) self.assertTrue(ac.subject in comment.subject.annotatedElement) def test_commentline_element_reconnect(self): """Test comment line connecting to comment and actor items. """ comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) ac = self.create(items.ActorItem, UML.Actor) self.connect(line, line.head, comment) self.connect(line, line.tail, ac) self.assertTrue(self.get_connected(line.tail) is ac) self.assertEqual(1, len(comment.subject.annotatedElement)) self.assertTrue(ac.subject in comment.subject.annotatedElement) ac2 = self.create(items.ActorItem, UML.Actor) # ac.canvas.disconnect_item(line, line.tail) self.disconnect(line, line.tail) self.connect(line, line.tail, ac2) self.assertTrue(self.get_connected(line.tail) is ac2) self.assertEqual(1, len(comment.subject.annotatedElement)) self.assertTrue(ac2.subject in comment.subject.annotatedElement) def test_commentline_element_disconnect(self): """Test comment line connecting to comment and disconnecting actor item. """ comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) ac = self.create(items.ActorItem, UML.Actor) self.connect(line, line.head, comment) self.connect(line, line.tail, ac) self.assertTrue(self.get_connected(line.tail) is ac) self.disconnect(line, line.tail) self.assertFalse(self.get_connected(line.tail) is ac) def test_commentline_unlink(self): """Test comment line unlinking. """ clazz = self.create(items.ClassItem, UML.Class) comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) self.connect(line, line.head, comment) self.connect(line, line.tail, clazz) self.assertTrue(clazz.subject in comment.subject.annotatedElement) self.assertTrue(comment.subject in clazz.subject.ownedComment) self.assertTrue(line.canvas) # FixMe: This should invoke the disconnect handler of the line's # handles. line.unlink() self.assertFalse(line.canvas) self.assertFalse(clazz.subject in comment.subject.annotatedElement) self.assertFalse(comment.subject in clazz.subject.ownedComment) self.assertTrue( len(comment.subject.annotatedElement) == 0, comment.subject.annotatedElement ) self.assertTrue( len(clazz.subject.ownedComment) == 0, clazz.subject.ownedComment ) def test_commentline_element_unlink(self): """Test comment line unlinking using a class item. """ clazz = self.create(items.ClassItem, UML.Class) comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) self.connect(line, line.head, comment) self.connect(line, line.tail, clazz) self.assertTrue(clazz.subject in comment.subject.annotatedElement) self.assertTrue(comment.subject in clazz.subject.ownedComment) self.assertTrue(line.canvas) clazz_subject = clazz.subject # FixMe: This should invoke the disconnect handler of the line's # handles. clazz.unlink() self.assertFalse(clazz.canvas) self.assertTrue(line.canvas) self.assertFalse(comment.subject.annotatedElement) self.assertTrue(len(clazz_subject.ownedComment) == 0) def test_commentline_relationship_unlink(self): """Test comment line to a relationship item connection and unlink. Demonstrates defect #103. """ clazz1 = self.create(items.ClassItem, UML.Class) clazz2 = self.create(items.ClassItem, UML.Class) gen = self.create(items.GeneralizationItem) self.connect(gen, gen.head, clazz1) self.connect(gen, gen.tail, clazz2) assert gen.subject # now, connect comment to a generalization (relationship) comment = self.create(items.CommentItem, UML.Comment) line = self.create(items.CommentLineItem) self.connect(line, line.head, comment) self.connect(line, line.tail, gen) assert gen.subject in comment.subject.annotatedElement assert comment.subject in gen.subject.ownedComment gen.unlink() assert not comment.subject.annotatedElement assert gen.subject is None def test_commentline_linked_to_same_element_twice(self): """ It is not allowed to create two commentlines between the same elements. """ clazz = self.create(items.ClassItem, UML.Class) # now, connect comment to a generalization (relationship) comment = self.create(items.CommentItem, UML.Comment) line1 = self.create(items.CommentLineItem) self.connect(line1, line1.head, comment) self.connect(line1, line1.tail, clazz) self.assertTrue(clazz.subject in comment.subject.annotatedElement) self.assertTrue(comment.subject in clazz.subject.ownedComment) # Now add another line line2 = self.create(items.CommentLineItem) self.connect(line2, line2.head, comment) self.assertFalse(self.allow(line2, line2.tail, clazz)) # vim: sw=4:et:ai PK!ō00'gaphor/adapters/tests/test_connector.py""" Test Item connections. """ from gaphor.tests import TestCase from zope import component from gaphor import UML from gaphor.diagram import items from gaphor.diagram.interfaces import IConnect class ConnectorTestCase(TestCase): pass # fixme: relationship test moved from multi dependency test PK!$gaphor/adapters/tests/test_editor.pyfrom gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items from gaphor.diagram.interfaces import IEditor from gaphor.adapters.propertypages import AttributesPage, OperationsPage from gi.repository import Gtk class EditorTestCase(TestCase): def setUp(self): super(EditorTestCase, self).setUp() def test_association_editor(self): assoc = self.create(items.AssociationItem) adapter = IEditor(assoc) assert not adapter.is_editable(10, 10) assert adapter._edit is None # Intermezzo: connect the association between two classes class1 = self.create(items.ClassItem, UML.Class) class2 = self.create(items.ClassItem, UML.Class) assoc.handles()[0].pos = 10, 10 assoc.handles()[-1].pos = 100, 100 self.connect(assoc, assoc.head, class1) self.connect(assoc, assoc.tail, class2) assert assoc.subject # Now the association has a subject member, so editing should really # work. pos = 55, 55 self.assertTrue(adapter.is_editable(*pos)) self.assertTrue(adapter._edit is assoc) pos = assoc.head_end._name_bounds[:2] self.assertTrue(adapter.is_editable(*pos)) self.assertTrue(adapter._edit is assoc.head_end) pos = assoc.tail_end._name_bounds[:2] self.assertTrue(adapter.is_editable(*pos)) self.assertTrue(adapter._edit is assoc.tail_end) def test_objectnode_editor(self): node = self.create(items.ObjectNodeItem, UML.ObjectNode) self.diagram.canvas.update_now() adapter = IEditor(node) self.assertTrue(adapter.is_editable(10, 10)) # assert not adapter.edit_tag # assert adapter.is_editable(*node.tag_bounds[:2]) # assert adapter.edit_tag def test_classifier_editor(self): """ Test classifier editor """ klass = self.create(items.ClassItem, UML.Class) klass.subject.name = "Class1" self.diagram.canvas.update() attr = self.element_factory.create(UML.Property) attr.name = "blah" klass.subject.ownedAttribute = attr oper = self.element_factory.create(UML.Operation) oper.name = "method" klass.subject.ownedOperation = oper self.diagram.canvas.update() edit = IEditor(klass) self.assertEqual("CompartmentItemEditor", edit.__class__.__name__) self.assertEqual(True, edit.is_editable(10, 10)) # Test the inner working of the editor self.assertEqual(klass, edit._edit) self.assertEqual("Class1", edit.get_text()) # The attribute: y = klass._header_size[1] + klass.style.compartment_padding[0] + 3 self.assertEqual(True, edit.is_editable(4, y)) self.assertEqual(attr, edit._edit.subject) self.assertEqual("+ blah", edit.get_text()) y += klass.compartments[0].height # The operation self.assertEqual(True, edit.is_editable(3, y)) self.assertEqual(oper, edit._edit.subject) self.assertEqual("+ method()", edit.get_text()) def test_class_attribute_editor(self): klass = self.create(items.ClassItem, UML.Class) klass.subject.name = "Class1" editor = AttributesPage(klass) page = editor.construct() tree_view = page.get_children()[1] self.assertSame(Gtk.TreeView, type(tree_view)) attr = self.element_factory.create(UML.Property) attr.name = "blah" klass.subject.ownedAttribute = attr self.assertSame(attr, tree_view.get_model()[0][-1]) self.assertEqual("+ blah", tree_view.get_model()[0][0]) attr.name = "foo" self.assertEqual("+ foo", tree_view.get_model()[0][0]) attr.typeValue = "int" self.assertEqual("+ foo: int", tree_view.get_model()[0][0]) attr.isDerived = True self.assertEqual("+ /foo: int", tree_view.get_model()[0][0]) page.destroy() def test_class_operation_editor(self): klass = self.create(items.ClassItem, UML.Class) klass.subject.name = "Class1" editor = OperationsPage(klass) page = editor.construct() tree_view = page.get_children()[1] self.assertSame(Gtk.TreeView, type(tree_view)) oper = self.element_factory.create(UML.Operation) oper.name = "o" klass.subject.ownedOperation = oper self.assertSame(oper, tree_view.get_model()[0][-1]) self.assertEqual("+ o()", tree_view.get_model()[0][0]) p = self.element_factory.create(UML.Parameter) p.name = "blah" oper.formalParameter = p self.assertEqual("+ o(in blah)", tree_view.get_model()[0][0]) page.destroy() PK!(_*_*&gaphor/adapters/tests/test_grouping.py""" Tests for grouping functionality in Gaphor. """ from gaphor import UML from gaphor.ui.namespace import NamespaceModel from gaphor.diagram import items from gaphor.tests import TestCase class NodesGroupTestCase(TestCase): """ Nodes grouping tests. """ def test_grouping(self): """Test node within another node composition """ n1 = self.create(items.NodeItem, UML.Node) n2 = self.create(items.NodeItem, UML.Node) self.group(n1, n2) self.assertTrue(n2.subject in n1.subject.nestedNode) self.assertFalse(n1.subject in n2.subject.nestedNode) def test_ungrouping(self): """Test decomposition of component from node """ n1 = self.create(items.NodeItem, UML.Node) n2 = self.create(items.NodeItem, UML.Node) self.group(n1, n2) self.ungroup(n1, n2) self.assertFalse(n2.subject in n1.subject.nestedNode) self.assertFalse(n1.subject in n2.subject.nestedNode) class NodeComponentGroupTestCase(TestCase): def test_grouping(self): """Test component within node composition """ n = self.create(items.NodeItem, UML.Node) c = self.create(items.ComponentItem, UML.Component) self.group(n, c) self.assertEqual(1, len(n.subject.ownedAttribute)) self.assertEqual(1, len(n.subject.ownedConnector)) self.assertEqual(1, len(c.subject.ownedAttribute)) self.assertEqual(2, len(self.kindof(UML.ConnectorEnd))) a1 = n.subject.ownedAttribute[0] a2 = c.subject.ownedAttribute[0] self.assertTrue(a1.isComposite) self.assertTrue(a1 in n.subject.part) connector = n.subject.ownedConnector[0] self.assertTrue(connector.end[0].role is a1) self.assertTrue(connector.end[1].role is a2) def test_ungrouping(self): """Test decomposition of component from node """ n = self.create(items.NodeItem, UML.Node) c = self.create(items.ComponentItem, UML.Component) query = self.group(n, c) query = self.ungroup(n, c) self.assertEqual(0, len(n.subject.ownedAttribute)) self.assertEqual(0, len(c.subject.ownedAttribute)) self.assertEqual(0, len(self.kindof(UML.Property))) self.assertEqual(0, len(self.kindof(UML.Connector))) self.assertEqual(0, len(self.kindof(UML.ConnectorEnd))) class NodeArtifactGroupTestCase(TestCase): def test_grouping(self): """Test artifact within node deployment """ n = self.create(items.NodeItem, UML.Node) a = self.create(items.ArtifactItem, UML.Artifact) self.group(n, a) self.assertEqual(1, len(n.subject.deployment)) self.assertTrue(n.subject.deployment[0].deployedArtifact[0] is a.subject) def test_ungrouping(self): """Test removal of artifact from node """ n = self.create(items.NodeItem, UML.Node) a = self.create(items.ArtifactItem, UML.Artifact) query = self.group(n, a) query = self.ungroup(n, a) self.assertEqual(0, len(n.subject.deployment)) self.assertEqual(0, len(self.kindof(UML.Deployment))) class SubsystemUseCaseGroupTestCase(TestCase): def test_grouping(self): """Test adding an use case to a subsystem """ s = self.create(items.SubsystemItem, UML.Component) uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) self.group(s, uc1) self.assertEqual(1, len(uc1.subject.subject)) self.group(s, uc2) self.assertEqual(1, len(uc2.subject.subject)) # Classifier.useCase is not navigable to UseCase # self.assertEqual(2, len(s.subject.useCase)) def test_grouping_with_namespace(self): """Test adding an use case to a subsystem (with namespace) """ namespace = NamespaceModel(self.element_factory) s = self.create(items.SubsystemItem, UML.Component) uc = self.create(items.UseCaseItem, UML.UseCase) # manipulate namespace c = self.element_factory.create(UML.Class) attribute = self.element_factory.create(UML.Property) c.ownedAttribute = attribute self.group(s, uc) self.assertEqual(1, len(uc.subject.subject)) self.assertTrue(s.subject.namespace is not uc.subject) def test_ungrouping(self): """Test removal of use case from subsystem """ s = self.create(items.SubsystemItem, UML.Component) uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) self.group(s, uc1) self.group(s, uc2) self.ungroup(s, uc1) self.assertEqual(0, len(uc1.subject.subject)) # Classifier.useCase is not navigable to UseCase # self.assertEqual(1, len(s.subject.useCase)) self.ungroup(s, uc2) self.assertEqual(0, len(uc2.subject.subject)) # Classifier.useCase is not navigable to UseCase # self.assertEqual(0, len(s.subject.useCase)) class PartitionGroupTestCase(TestCase): def test_no_subpartition_when_nodes_in(self): """Test adding subpartition when nodes added """ p = self.create(items.PartitionItem) a1 = self.create(items.ActionItem, UML.Action) p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) self.group(p, p1) self.group(p1, a1) self.assertFalse(self.can_group(p1, p2)) def test_no_nodes_when_subpartition_in(self): """Test adding nodes when subpartition added """ p = self.create(items.PartitionItem) a1 = self.create(items.ActionItem, UML.Action) p1 = self.create(items.PartitionItem) self.group(p, p1) self.assertFalse(self.can_group(p, a1)) def test_action_grouping(self): """Test adding action to partition """ p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) self.assertFalse(self.can_group(p1, a1)) # cannot add to dimension self.group(p1, p2) self.group(p2, a1) self.assertTrue(self.can_group(p2, a1)) self.assertEqual(1, len(p2.subject.node)) self.group(p2, a2) self.assertEqual(2, len(p2.subject.node)) def test_subpartition_grouping(self): """Test adding subpartition to partition """ p = self.create(items.PartitionItem) p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) self.group(p, p1) self.assertTrue(p.subject is None) self.assertTrue(p1.subject is not None) self.group(p, p2) self.assertTrue(p.subject is None) self.assertTrue(p2.subject is not None) def test_ungrouping(self): """Test action and subpartition removal """ p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) self.group(p1, p2) # group to p2, it is disallowed to p1 self.group(p2, a1) self.group(p2, a2) self.ungroup(p2, a1) self.assertEqual(1, len(p2.subject.node)) self.ungroup(p2, a2) self.assertEqual(0, len(p2.subject.node)) self.ungroup(p1, p2) self.assertTrue(p1.subject is None, p1.subject) self.assertTrue(p2.subject is None, p2.subject) self.assertEqual(0, len(self.kindof(UML.ActivityPartition))) def test_ungrouping_with_actions(self): """Test subpartition with actions removal """ p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) a1 = self.create(items.ActionItem, UML.Action) a2 = self.create(items.ActionItem, UML.Action) self.group(p1, p2) # group to p2, it is disallowed to p1 self.group(p2, a1) self.group(p2, a2) partition = p2.subject assert len(partition.node) == 2, partition.node assert 2 == len(p2.canvas.get_children(p2)), p2.canvas.get_children(p2) self.ungroup(p1, p2) self.assertEqual(0, len(partition.node)) self.assertEqual(0, len(p2.canvas.get_children(p2))) self.assertEqual(0, len(partition.node)) def test_nested_subpartition_ungrouping(self): """Test removal of subpartition with swimlanes """ p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) p3 = self.create(items.PartitionItem) p4 = self.create(items.PartitionItem) self.group(p1, p2) self.group(p2, p3) self.group(p2, p4) self.assertTrue(p2.subject is not None, p2.subject) self.assertTrue(p3.subject is not None, p3.subject) self.assertTrue(p4.subject is not None, p4.subject) self.ungroup(p1, p2) self.assertTrue(p1.subject is None, p1.subject) self.assertTrue(p2.subject is None, p2.subject) self.assertTrue(p3.subject is not None, p3.subject) self.assertTrue(p4.subject is not None, p4.subject) self.assertEqual(2, len(self.kindof(UML.ActivityPartition))) def test_nested_subpartition_regrouping(self): """Test regrouping of subpartition with swimlanes """ p1 = self.create(items.PartitionItem) p2 = self.create(items.PartitionItem) p3 = self.create(items.PartitionItem) p4 = self.create(items.PartitionItem) self.group(p1, p2) self.group(p2, p3) self.group(p2, p4) self.assertTrue(p2.subject is not None, p2.subject) self.assertTrue(p3.subject is not None, p3.subject) self.assertTrue(p4.subject is not None, p4.subject) self.ungroup(p1, p2) self.assertTrue(p1.subject is None, p1.subject) self.assertTrue(p2.subject is None, p2.subject) self.assertTrue(p3.subject is not None, p3.subject) self.assertTrue(p4.subject is not None, p4.subject) self.assertEqual(2, len(self.kindof(UML.ActivityPartition))) self.group(p1, p2) self.assertEqual(3, len(self.kindof(UML.ActivityPartition))) self.assertTrue(p2.subject is not None, p2.subject) self.assertTrue(p3.subject is not None, p3.subject) self.assertTrue(p4.subject is not None, p4.subject) self.assertTrue(p3.subject in p2.subject.subpartition, p2.subject.subpartition) self.assertTrue(p4.subject in p2.subject.subpartition, p2.subject.subpartition) PK! Orr+gaphor/adapters/tests/test_propertypages.pyfrom gi.repository import Gtk from gaphor import UML from gaphor.diagram import items from gaphor.tests import TestCase from gaphor.adapters.propertypages import ClassAttributes class ClassAttributesTestCase(TestCase): def test_attribute_editing(self): class_item = self.create(items.ClassItem, UML.Class) model = ClassAttributes(class_item, (str, bool, object)) model.append([None, False, None]) path = Gtk.TreePath.new_first() iter = model.get_iter(path) model.set_value(iter, col=0, value="attr") assert model[iter][-1] is class_item.subject.ownedAttribute[0] PK!$gaphor/adapters/usecases/__init__.pyPK!*gaphor/adapters/usecases/tests/__init__.pyPK!-gaphor/adapters/usecases/tests/test_extend.py""" Test extend item connections. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class ExtendItemTestCase(TestCase): def test_use_case_glue(self): """Test "extend" gluing to use cases.""" uc1 = self.create(items.UseCaseItem, UML.UseCase) extend = self.create(items.ExtendItem) glued = self.allow(extend, extend.head, uc1) self.assertTrue(glued) def test_use_case_connect(self): """Test connecting "extend" to use cases """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) extend = self.create(items.ExtendItem) self.connect(extend, extend.head, uc1) self.assertTrue(self.get_connected(extend.head), uc1) self.connect(extend, extend.tail, uc2) self.assertTrue(self.get_connected(extend.tail), uc2) def test_use_case_connect(self): """Test reconnecting use cases with "extend" """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) uc3 = self.create(items.UseCaseItem, UML.UseCase) extend = self.create(items.ExtendItem) # connect: uc1 -> uc2 self.connect(extend, extend.head, uc1) self.connect(extend, extend.tail, uc2) e = extend.subject # reconnect: uc1 -> uc2 self.connect(extend, extend.tail, uc3) self.assertSame(e, extend.subject) self.assertSame(extend.subject.extendedCase, uc1.subject) self.assertSame(extend.subject.extension, uc3.subject) def test_use_case_disconnect(self): """Test disconnecting "extend" from use cases """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) extend = self.create(items.ExtendItem) self.connect(extend, extend.head, uc1) self.connect(extend, extend.tail, uc2) self.disconnect(extend, extend.head) self.assertTrue(self.get_connected(extend.head) is None) self.assertTrue(extend.subject is None) self.disconnect(extend, extend.tail) self.assertTrue(self.get_connected(extend.tail) is None) PK!S]0  .gaphor/adapters/usecases/tests/test_include.py""" Test include item connections. """ from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class IncludeItemTestCase(TestCase): def test_use_case_glue(self): """Test "include" gluing to use cases.""" uc1 = self.create(items.UseCaseItem, UML.UseCase) include = self.create(items.IncludeItem) glued = self.allow(include, include.head, uc1) self.assertTrue(glued) def test_use_case_connect(self): """Test connecting "include" to use cases """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) include = self.create(items.IncludeItem) self.connect(include, include.head, uc1) self.assertTrue(self.get_connected(include.head), uc1) self.connect(include, include.tail, uc2) self.assertTrue(self.get_connected(include.tail), uc2) def test_use_case_connect(self): """Test reconnecting use cases with "include" """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) uc3 = self.create(items.UseCaseItem, UML.UseCase) include = self.create(items.IncludeItem) # connect: uc1 -> uc2 self.connect(include, include.head, uc1) self.connect(include, include.tail, uc2) e = include.subject # reconnect: uc1 -> uc2 self.connect(include, include.tail, uc3) self.assertSame(e, include.subject) self.assertSame(include.subject.addition, uc1.subject) self.assertSame(include.subject.includingCase, uc3.subject) def test_use_case_disconnect(self): """Test disconnecting "include" from use cases """ uc1 = self.create(items.UseCaseItem, UML.UseCase) uc2 = self.create(items.UseCaseItem, UML.UseCase) include = self.create(items.IncludeItem) self.connect(include, include.head, uc1) self.connect(include, include.tail, uc2) self.disconnect(include, include.head) self.assertTrue(self.get_connected(include.head) is None) self.assertTrue(include.subject is None) self.disconnect(include, include.tail) self.assertTrue(self.get_connected(include.tail) is None) PK!ؾ,,*gaphor/adapters/usecases/usecaseconnect.py""" Use cases related connection adapters. """ from zope import component from gaphor import UML from gaphor.diagram import items from gaphor.adapters.connectors import RelationshipConnect @component.adapter(items.UseCaseItem, items.IncludeItem) class IncludeConnect(RelationshipConnect): """Connect use cases with an include item relationship.""" def allow(self, handle, port): line = self.line element = self.element if not (element.subject and isinstance(element.subject, UML.UseCase)): return None return super(IncludeConnect, self).allow(handle, port) def reconnect(self, handle, port): self.reconnect_relationship( handle, UML.Include.addition, UML.Include.includingCase ) def connect_subject(self, handle): relation = self.relationship_or_new( UML.Include, UML.Include.addition, UML.Include.includingCase ) self.line.subject = relation component.provideAdapter(IncludeConnect) @component.adapter(items.UseCaseItem, items.ExtendItem) class ExtendConnect(RelationshipConnect): """Connect use cases with an extend item relationship.""" def allow(self, handle, port): line = self.line element = self.element if not (element.subject and isinstance(element.subject, UML.UseCase)): return None return super(ExtendConnect, self).allow(handle, port) def reconnect(self, handle, port): self.reconnect_relationship( handle, UML.Extend.extendedCase, UML.Extend.extension ) def connect_subject(self, handle): relation = self.relationship_or_new( UML.Extend, UML.Extend.extendedCase, UML.Extend.extension ) self.line.subject = relation component.provideAdapter(ExtendConnect) PK!,,gaphor/application.py""" The Application object. One application should be available. All important services are present in the application object: - plugin manager - undo manager - main window - UML element factory - action sets """ import logging import pkg_resources from zope import component from gaphor.event import ServiceInitializedEvent, ServiceShutdownEvent from gaphor.interfaces import IService logger = logging.getLogger(__name__) class NotInitializedError(Exception): pass class _Application(object): """ The Gaphor application is started from the Application instance. It behaves like a singleton in many ways. The Application is responsible for loading services and plugins. Services are registered as "utilities" in the application registry. Methods are provided that wrap zope.component's handle, adapter and subscription registrations. In addition to registration methods also unregister methods are provided. This way services can be properly unregistered on shutdown for example. """ # interface.implements(IApplication) _ESSENTIAL_SERVICES = ["component_registry"] def __init__(self): self._uninitialized_services = {} self._event_filter = None self.component_registry = None def init(self, services=None): """ Initialize the application. """ self.load_services(services) self.init_all_services() essential_services = property( lambda s: s._ESSENTIAL_SERVICES, doc=""" Provide an ordered list of services that need to be loaded first. """, ) def load_services(self, services=None): """ Load services from resources. Services are registered as utilities in zope.component. Service should provide an interface gaphor.interfaces.IService. """ # Ensure essential services are always loaded. if services: for name in self.essential_services: if name not in services: services.append(name) for ep in pkg_resources.iter_entry_points("gaphor.services"): cls = ep.load() if not IService.implementedBy(cls): raise NameError("Entry point %s doesn" "t provide IService" % ep.name) if not services or ep.name in services: logger.debug('found service entry point "%s"' % ep.name) srv = cls() self._uninitialized_services[ep.name] = srv def init_all_services(self): for name in self.essential_services: self.init_service(name) while self._uninitialized_services: self.init_service(next(iter(list(self._uninitialized_services.keys())))) def init_service(self, name): """ Initialize a not yet initialized service. Raises ComponentLookupError if the service has not been found """ try: srv = self._uninitialized_services.pop(name) except KeyError: raise component.ComponentLookupError(IService, name) else: logger.info("initializing service service.%s" % name) srv.init(self) # Bootstrap symptoms if name in self.essential_services: setattr(self, name, srv) self.component_registry.register_utility(srv, IService, name) self.component_registry.handle(ServiceInitializedEvent(name, srv)) return srv distribution = property( lambda s: pkg_resources.get_distribution("gaphor"), doc="Get the PkgResources distribution for Gaphor", ) def get_service(self, name): if not self.component_registry: raise NotInitializedError("First call Application.init() to load services") try: return self.component_registry.get_service(name) except component.ComponentLookupError: return self.init_service(name) def run(self): from gi.repository import Gtk Gtk.main() def shutdown(self): for name, srv in self.component_registry.get_utilities(IService): if name not in self.essential_services: self.shutdown_service(name) for name in reversed(self.essential_services): self.shutdown_service(name) setattr(self, name, None) def shutdown_service(self, name): srv = self.component_registry.get_service(name) self.component_registry.handle(ServiceShutdownEvent(name, srv)) self.component_registry.unregister_utility(srv, IService, name) srv.shutdown() # Make sure there is only one! Application = _Application() class inject(object): """ Simple descriptor for dependency injection. This is technically a wrapper around Application.get_service(). Usage:: >>> class A(object): ... element_factory = inject('element_factory') """ def __init__(self, name): self._name = name # self._s = None def __get__(self, obj, class_=None): """ Resolve a dependency, but only if we're called from an object instance. """ if not obj: return self return Application.get_service(self._name) # if self._s is None: # self._s = _Application.get_service(self._name) # return self._s PK!6)"ccgaphor/core.py""" The Core module provides an entry point for Gaphor's core constructs. An average module should only need to import this module. """ from gaphor.application import inject, Application from gaphor.transaction import Transaction, transactional from gaphor.action import action, toggle_action, radio_action, build_action_group from gaphor.i18n import _ PK!c==gaphor/diagram/READMEmodule: gaphor.diagram ====================== This module contains the items that can be placed in gaphor.UML.Diagram's. Each diagram item represents one or more gaphor.UML.Element's (subclasses thereof). Diagram items are subclassed from Presentation. This is the base class for "presentation elements": items used to visualize an UML element. Also gaphor.UML.Element is inherited. This class contains logic for event notification among other utility functions (e.g. the __unlink__ signal). Presentation elements ("items") have a "subject" attribute, that refers to the (main-) model element class that is represented (e.g. a class representation has as subject a gaphor.UML.Class instance, other elements, such as gaphor.UML.Property and gaphor.UML.Operation are implicitly referenced. Signal handling --------------- As of this version, signal notification is done through zope.component. As a result all modification events originated from gaphor.UML classes are received by all items. (In the old days every item should register for specific events on specific model elements, which resulted in quite a complex administration of event handlers, since all those handlers should be unregistered when an item was removed). Connecting items ---------------- Another change, due to the introduction of the Zope3 framework, is how items are connected to one another. This is done through adapters (multi-adapters to be more specific). An adapter is used to add additional behavior to an object. In this case the IConnect interface should be implemented for (element, line) tuples. For each possible connection an adapter should be written (CommentItem with any DiagramItem, AssociationItem with ClassifierItem, etc.). The big (huge) advantage is that complex connection stuff is removed from the diagram items (which resulted in cyclic dependencies in the past) and is put on a higher level: the adapter. See interfaces.py and adapter.py for implementations. Text editing ------------ Like item connecting, text editing is also implemented through adapters. The IEditor interface is used for this. PK!eP gaphor/diagram/__init__.py""" The diagram package contains items (to be drawn on the diagram), tools (used for interacting with the diagram) and interfaces (used for adapting the diagram). """ import uuid from gaphor.diagram.style import Style # Map UML elements to their (default) representation. _uml_to_item_map = {} def create(type): return create_as(type, str(uuid.uuid1())) def create_as(type, id): return type(id) def get_diagram_item(element): global _uml_to_item_map return _uml_to_item_map.get(element) def set_diagram_item(element, item): global _uml_to_item_map _uml_to_item_map[element] = item def uml(uml_class, stereotype=None): """ Assign UML metamodel class and a stereotype to diagram item. :Parameters: uml_class UML metamodel class. stereotype Stereotype name (i.e. 'subsystem'). """ def f(item_class): t = uml_class if stereotype is not None: t = (uml_class, stereotype) item_class.__stereotype__ = stereotype set_diagram_item(t, item_class) return item_class return f class DiagramItemMeta(type): """ Initialize a new diagram item. 1. Register UML.Elements by means of the __uml__ attribute (see map_uml_class method). 2. Set items style information. @ivar style: style information """ def __init__(self, name, bases, data): type.__init__(self, name, bases, data) self.map_uml_class(data) self.set_style(data) def map_uml_class(self, data): """ Map UML class to diagram item. @param cls: new instance of item class @param data: metaclass data with UML class information """ if "__uml__" in data: obj = data["__uml__"] if isinstance(obj, (tuple, set, list)): for c in obj: set_diagram_item(c, self) else: set_diagram_item(obj, self) def set_style(self, data): """ Set item style information by merging provided information with style information from base classes. @param cls: new instance of diagram item class @param bases: base classes of an item @param data: metaclass data with style information """ style = Style() for c in self.__bases__: if hasattr(c, "style"): for (name, value) in list(c.style.items()): style.add(name, value) if "__style__" in data: for (name, value) in data["__style__"].items(): style.add(name, value) self.style = style # vim:sw=4:et PK!"gaphor/diagram/actions/__init__.pyPK! gaphor/diagram/actions/action.py""" Action diagram item. """ from math import pi from gaphor import UML from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.style import ALIGN_CENTER, ALIGN_MIDDLE class ActionItem(NamedItem): __uml__ = UML.Action __style__ = {"min-size": (50, 30), "name-align": (ALIGN_CENTER, ALIGN_MIDDLE)} def draw(self, context): """ Draw action symbol. """ super(ActionItem, self).draw(context) c = context.cairo d = 15 c.move_to(0, d) c.arc(d, d, d, pi, 1.5 * pi) c.line_to(self.width - d, 0) c.arc(self.width - d, d, d, 1.5 * pi, 0) c.line_to(self.width, self.height - d) c.arc(self.width - d, self.height - d, d, 0, 0.5 * pi) c.line_to(d, self.height) c.arc(d, self.height - d, d, 0.5 * pi, pi) c.close_path() c.stroke() class SendSignalActionItem(NamedItem): __uml__ = UML.SendSignalAction __style__ = {"min-size": (50, 30), "name-align": (ALIGN_CENTER, ALIGN_MIDDLE)} def draw(self, context): """ Draw action symbol. """ super(SendSignalActionItem, self).draw(context) c = context.cairo d = 15 w = self.width h = self.height c.move_to(0, 0) c.line_to(w - d, 0) c.line_to(w, h / 2) c.line_to(w - d, h) c.line_to(0, h) c.close_path() c.stroke() class AcceptEventActionItem(NamedItem): __uml__ = UML.SendSignalAction __style__ = {"min-size": (50, 30), "name-align": (ALIGN_CENTER, ALIGN_MIDDLE)} def draw(self, context): """ Draw action symbol. """ super(AcceptEventActionItem, self).draw(context) c = context.cairo d = 15 w = self.width h = self.height c.move_to(0, 0) c.line_to(w, 0) c.line_to(w, h) c.line_to(0, h) c.line_to(d, h / 2) c.close_path() c.stroke() PK!ԷLBLBgaphor/diagram/actions/flow.py""" Control flow and object flow implementation. Contains also implementation to split flows using activity edge connectors. """ from math import atan, pi, sin, cos from gaphor import UML from gaphor.diagram.diagramline import NamedLine from gaphor.diagram.style import ALIGN_LEFT, ALIGN_RIGHT, ALIGN_TOP node_classes = { UML.ForkNode: UML.JoinNode, UML.DecisionNode: UML.MergeNode, UML.JoinNode: UML.ForkNode, UML.MergeNode: UML.DecisionNode, } class FlowItem(NamedLine): """ Representation of control flow and object flow. Flow item has name and guard. It can be splitted into two flows with activity edge connectors. """ __uml__ = UML.ControlFlow __style__ = {"name-align": (ALIGN_RIGHT, ALIGN_TOP), "name-padding": (5, 15, 5, 5)} def __init__(self, id=None): NamedLine.__init__(self, id) self._guard = self.add_text("guard.value", editable=True) self.watch("subject.guard", self.on_control_flow_guard) self.watch("subject.guard", self.on_control_flow_guard) def postload(self): try: self._guard.text = self.subject.guard.value except AttributeError as e: self._guard.text = "" super(FlowItem, self).postload() def on_control_flow_guard(self, event): subject = self.subject try: self._guard.text = subject.guard if subject else "" except AttributeError as e: self._guard.text = "" self.request_update() def draw_tail(self, context): cr = context.cairo cr.line_to(0, 0) cr.stroke() cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) # class ACItem(TextElement): class ACItem(object): """ Activity edge connector. It is a circle with name inside. """ RADIUS = 10 def __init__(self, id): pass # TextElement.__init__(self, id) # self._circle = diacanvas.shape.Ellipse() # self._circle.set_line_width(2.0) # self._circle.set_fill_color(diacanvas.color(255, 255, 255)) # self._circle.set_fill(diacanvas.shape.FILL_SOLID) # self.show_border = False # set new value notification function to change activity edge # connector name globally # vnf = self.on_subject_notify__value # def f(subject, pspec): # vnf(subject, pspec) # if self.parent._opposite: # self.parent._opposite._connector.subject.value = subject.value # self.on_subject_notify__value = f def move_center(self, x, y): """ Move center of item to point (x, y). Other parts of item are aligned to this point. """ a = self.props.affine x -= self.RADIUS y -= self.RADIUS self.props.affine = (a[0], a[1], a[2], a[3], x, y) def on_update(self, affine): """ Center name of activity edge connector and put a circle around it. """ r = self.RADIUS * 2 x = self.RADIUS y = self.RADIUS self._circle.ellipse(center=(x, y), width=r, height=r) # get label size and move it so it is centered with circle w, h = self.get_size() x, y = x - w / 2, y - h / 2 self._name.set_pos((x, y)) self._name_bounds = (x, y, x + w, y + h) TextElement.on_update(self, affine) self.set_bounds((-1, -1, r + 1, r + 1)) # class CFlowItem(FlowItem): # """ # Abstract class for flows with activity edge connector. Flow with # activity edge connector references other one, which has activity edge # connector with same name (it is called opposite one). # # Such flows have active and inactive ends. Active end is connected to # any node and inactive end is connected only to activity edge connector. # """ # # def __init__(self, id = None): # FlowItem.__init__(self, id) # # self._connector = ACItem('value') # # factory = resource(UML.ElementFactory) # # self._opposite = None # # # when flow item with connector is deleted, then kill opposite, too # self.unlink_handler_id = self.connect('__unlink__', self.kill_opposite) # # # def kill_opposite(self, source, name): # # do not allow to be killed by opposite # self._opposite.disconnect(self._opposite.unlink_handler_id) # self._opposite.unlink() # # # def save(self, save_func): # """ # Save connector name and opposite flow with activity edge connector. # """ # FlowItem.save(self, save_func) # save_func('opposite', self._opposite, True) # save_func('connector-name', self._connector.subject.value) # # # def load(self, name, value): # """ # Load connector name and opposite flow with activity edge connector. # """ # if name == 'connector-name': # self._connector.subject.value = value # elif name == 'opposite': # self._opposite = value # else: # FlowItem.load(self, name, value) # # # def on_update(self, affine): # """ # Draw flow line and activity edge connector. # """ # # get parent line points to determine angle # # used to rotate position of activity edge connector # p1, p2 = self.get_line() # # # calculate position of connector center # r = self._connector.RADIUS # #x = p1[0] < p2[0] and r or -r # x = p1[0] < p2[0] and -r or r # y = 0 # x, y = rotate(p1, p2, x, y, p1[0], p1[1]) # # self._connector.move_center(x, y) # # FlowItem.on_update(self, affine) # # # def confirm_connect_handle(self, handle): # """See NamedLine.confirm_connect_handle(). # """ # c1 = self.get_active_handle().connected_to # source # c2 = self._opposite.get_active_handle().connected_to # target # # # set correct relationship between connected items; # # it should be (source, target) not (target, source); # # otherwise we are looking for non-existing or wrong relationship # if isinstance(self, CFlowItemB): # c1, c2 = c2, c1 # # self.connect_items(c1, c2) # self._opposite.set_subject(self.subject) # # # def allow_connect_handle(self, handle, connecting_to): # if handle == self.get_inactive_handle(): # return False # return FlowItem.allow_connect_handle(self, handle, connecting_to) # # # def confirm_disconnect_handle (self, handle, was_connected_to): # """See NamedLine.confirm_disconnect_handle(). # """ # c1 = self.get_active_handle().connected_to # source # c2 = self._opposite.get_active_handle().connected_to # target # self.disconnect_items(c1, c2, was_connected_to) # self._opposite.set_subject(None) # # # # class CFlowItemA(CFlowItem): # """ # * Is used for split flows, as is CFlowItemB * # # Flow with activity edge connector, which starts from node and points to # activity edge connector. # """ # def __init__(self, id): # CFlowItem.__init__(self, id) # self.create_guard() # # # def on_update(self, affine): # self.update_guard(affine) # CFlowItem.on_update(self, affine) # # # def get_line(self): # p1 = self.handles[-1].get_pos_i() # p2 = self.handles[-2].get_pos_i() # return p1, p2 # # # def get_active_handle(self): # """ # Return source handle as active one. # """ # return self.handles[0] # # # def get_inactive_handle(self): # """ # Return target handle as inactive one. # """ # return self.handles[-1] # # # # class CFlowItemB(CFlowItem): # """ # Flow with activity edge connector, which starts from activity edge # connector and points to a node. # """ # def __init__(self, id): # CFlowItem.__init__(self, id) # self.create_name() # # # def on_update(self, affine): # self.update_name(affine) # CFlowItem.on_update(self, affine) # # # def get_line(self): # p1 = self.handles[0].get_pos_i() # p2 = self.handles[1].get_pos_i() # return p1, p2 # # # def get_active_handle(self): # """ # Return target handle as active one. # """ # return self.handles[-1] # # # def get_inactive_handle(self): # """ # Return source handle as inactive one. # """ # return self.handles[0] # # # def move_collection(src, target, name): # """ # Copy collection from one object to another. # # src - source object # target - target object # name - name of attribute, which is collection to copy # """ # # first make of copy of collection, because assigning # # element to target collection moves this element # for flow in list(getattr(src, name)): # getattr(target, name).append(flow) # # def is_fd(node): # """ # Check if node is fork or decision node. # """ # return isinstance(node, (UML.ForkNode, UML.DecisionNode)) # # # def change_node_class(node): # """ # If UML constraints for fork, join, decision and merge nodes are not # met, then create new node depending on input node class, i.e. create # fork node from join node or merge node from decision node. # # If constraints are met, then return node itself. # """ # if is_fd(node) and len(node.incoming) > 1 \ # or not is_fd(node) and len(node.incoming) < 2: # # factory = resource(UML.ElementFactory) # cls = node_classes[node.__class__] # log.debug('creating %s' % cls) # nn = factory.create(cls) # move_collection(node, nn, 'incoming') # move_collection(node, nn, 'outgoing') # else: # nn = node # # assert nn is not None # # # we have to accept zero of outgoing edges in case of fork/descision # # nodes # assert is_fd(nn) and len(nn.incoming) <= 1 \ # or not is_fd(nn) and len(nn.incoming) >= 1, '%s' % nn # assert is_fd(nn) and len(nn.outgoing) >= 0 \ # or not is_fd(nn) and len(nn.outgoing) <= 1, '%s' % nn # return nn # # # def combine_nodes(node): # """ # Create fork/join (decision/merge) nodes combination as described in UML # specification. # """ # log.debug('combining nodes') # # cls = node_classes[node.__class__] # log.debug('creating %s' % cls) # factory = resource(UML.ElementFactory) # target = factory.create(cls) # # source = node # if is_fd(node): # source = target # move_collection(node, target, 'incoming') # # # create new fork node # cls = node_classes[target.__class__] # log.debug('creating %s' % cls) # target = factory.create(cls) # move_collection(node, target, 'outgoing') # else: # # fork node is created, referenced by target # move_collection(node, target, 'outgoing') # # assert not is_fd(source) # assert is_fd(target) # # # create flow # c1 = count_object_flows(source, 'incoming') # c2 = count_object_flows(target, 'outgoing') # # if c1 > 0 or c2 > 0: # flow = factory.create(UML.ControlFlow) # else: # flow = factory.create(UML.ObjectFlow) # flow.source = source # flow.target = target # # assert len(source.incoming) > 1 # assert len(source.outgoing) == 1 # # assert len(target.incoming) == 1 # assert len(target.outgoing) > 1 # # return source # # # def decombine_nodes(source): # """ # Create node depending on source argument which denotes combination of # fork/join (decision/merge) nodes as described in UML specification. # # Combination of nodes is destroyed. # """ # log.debug('decombining nodes') # flow = source.outgoing[0] # target = flow.target # # if len(source.incoming) < 2: # # create fork or decision # cls = target.__class__ # else: # # create join or merge # cls = source.__class__ # # factory = resource(UML.ElementFactory) # node = factory.create(cls) # # move_collection(source, node, 'incoming') # move_collection(target, node, 'outgoing') # # assert source != node # # # delete target and combining flow # # source should be deleted by caller # target.unlink() # flow.unlink() # # # return new node # return node # def determine_node_on_connect(el): # """ # Determine classes of nodes depending on amount of incoming # and outgoing edges. This method is called when flow is attached # to node. # # If there is more than one incoming edge and more than one # outgoing edge, then create two nodes and combine them with # flow as described in UML specification. # """ # subject = el.subject # if not isinstance(subject, tuple(node_classes.keys())): # return # # new_subject = subject # # if len(subject.incoming) > 1 and len(subject.outgoing) > 1: # new_subject = combine_nodes(subject) # el.props.combined = True # # else: # new_subject = change_node_class(subject) # # change_node_subject(el, new_subject) # # if el.props.combined: # check_combining_flow(el) # def determine_node_on_disconnect(el): # """ # Determine classes of nodes depending on amount of incoming # and outgoing edges. This method is called when flow is dettached # from node. # # If there are combined nodes and there is no need for them, then replace # combination with appropriate node (i.e. replace with fork node when # there are less than two incoming edges). This way data model is kept as # simple as possible. # """ # subject = el.subject # if not isinstance(subject, tuple(node_classes.keys())): # return # # new_subject = subject # # if el.props.combined: # cs = subject.outgoing[0].target # # decombine node when there is no more than one incoming # # and no more than one outgoing flow # if len(subject.incoming) < 2 or len(cs.outgoing) < 2: # new_subject = decombine_nodes(subject) # el.props.combined = False # else: # check_combining_flow(el) # # else: # new_subject = change_node_class(subject) # # change_node_subject(el, new_subject) # def change_node_subject(el, new_subject): # """ # Change element's subject if new subject is different than element's # subject. If subject is changed, then old subject is destroyed. # """ # subject = el.subject # if new_subject != subject: # log.debug('changing subject of ui node %s' % el) # el.set_subject(new_subject) # # log.debug('deleting node %s' % subject) # subject.unlink() # def create_flow(cls, flow): # """ # Create new flow of class cls. Flow data from flow argument are copied # to new created flow. Old flow is destroyed. # """ # factory = resource(UML.ElementFactory) # f = factory.create(cls) # f.source = flow.source # f.target = flow.target # flow.unlink() # return f # def count_object_flows(node, attr): # """ # Count incoming or outgoing object flows. # """ # return len(getattr(node, attr) # .select(lambda flow: isinstance(flow, UML.ObjectFlow))) # # # def check_combining_flow(el): # """ # Set object flow as combining flow when incoming or outgoing flow count # is greater than zero. Otherwise change combining flow to control flow. # """ # subject = el.subject # flow = subject.outgoing[0] # combining flow # combined = flow.target # combined node # # c1 = count_object_flows(subject, 'incoming') # c2 = count_object_flows(combined, 'outgoing') # # log.debug('combined incoming and outgoing object flow count: (%d, %d)' % (c1, c2)) # # if (c1 > 0 or c2 > 0) and isinstance(flow, UML.ControlFlow): # log.debug('changing combing flow to object flow') # create_flow(UML.ObjectFlow, flow) # elif c1 == 0 and c2 == 0 and isinstance(flow, UML.ObjectFlow): # log.debug('changing combing flow to control flow') # create_flow(UML.ControlFlow, flow) # # def create_connector_end(connector, role): # """ # Create Connector End, set role and attach created end to # connector. # """ # end = resource(UML.ElementFactory).create(UML.ConnectorEnd) # end.role = role # connector.end = end # assert end in role.end # return end # # def rotate(p1, p2, a, b, x, y): # """ # Rotate point (a, b) by angle, which is determined by line (p1, p2). # # Rotated point is moved by vector (x, y). # """ # try: # angle = atan((p1[1] - p2[1]) / (p1[0] - p2[0])) # except ZeroDivisionError: # da = p1[1] < p2[1] and 1.5 or -1.5 # angle = pi * da # # sin_angle = sin(angle) # cos_angle = cos(angle) # return (cos_angle * a - sin_angle * b + x, # sin_angle * a + cos_angle * b + y) PK!#gaphor/diagram/actions/partition.py""" Activity Partition item. TODO: partition can be resized only horizontally or vertically, therefore - define constraints for horizontal and vertical handles - reallocate handles in such way, so they clearly indicate horizontal or vertical size change """ from gaphor import UML from gaphor.diagram.nameditem import NamedItem class PartitionItem(NamedItem): __uml__ = UML.ActivityPartition __stereotype__ = {"external": lambda self: self.subject and self.subject.isExternal} __style__ = {"min-size": (100, 300), "line-width": 2.4} DELTA = 30 def __init__(self, id=None): super(PartitionItem, self).__init__(id) self._toplevel = False self._bottom = False self._subpart = False self._hdmax = 0 # maximum subpartition header height def pre_update(self, context): super(PartitionItem, self).pre_update(context) # get subpartitions children = list( k for k in self.canvas.get_children(self) if isinstance(k, PartitionItem) ) self._toplevel = self.canvas.get_parent(self) is None self._subpart = len(children) > 0 self._bottom = not self._toplevel and not self._subpart if self._toplevel: self._header_size = self._header_size[0], self.DELTA handles = self.handles() # toplevel partition controls the height # partitions at the very bottom control the width # middle partitions control nothing for h in handles: h.movable = False h.visible = False if self._bottom: h = handles[1] h.visible = h.movable = True if self._toplevel: h1, h2 = handles[2:4] h1.visible = h1.movable = True h2.visible = h2.movable = True if self._subpart: wsum = sum(sl.width for sl in children) self._hdmax = max(sl._header_size[1] for sl in children) # extend width of swimline due the children but keep the height # untouched self.width = wsum dp = 0 for sl in self.canvas.get_children(self): x, y = sl.matrix[4], sl.matrix[5] x = dp - x y = -y + self._header_size[1] + self._hdmax - sl._header_size[1] sl.matrix.translate(x, y) sl.height = sl.min_height = max(0, self.height - self._header_size[1]) dp += sl.width def draw(self, context): """ By default horizontal partition is drawn. It is open on right side (or bottom side when horizontal). """ cr = context.cairo cr.set_line_width(self.style.line_width) if self.subject and not self.subject.isDimension and self._toplevel: cr.move_to(0, 0) cr.line_to(self.width, 0) h = self._header_size[1] # draw outside lines if this item is toplevel partition if self._toplevel: cr.move_to(0, self.height) cr.line_to(0, h) cr.line_to(self.width, h) cr.line_to(self.width, self.height) super(PartitionItem, self).draw(context) if self._subpart: # header line for all subparitions hd = h + self._hdmax cr.move_to(0, hd) cr.line_to(self.width, hd) if self._subpart: # draw inside lines for all children but last one dp = 0 for sl in self.canvas.get_children(self)[:-1]: dp += sl.width cr.move_to(dp, h) cr.line_to(dp, self.height) cr.stroke() if context.hovered or context.dropzone: cr.save() cr.set_dash((1.0, 5.0), 0) cr.set_line_width(1.0) cr.rectangle(0, 0, self.width, self.height) self.highlight(context) cr.stroke() cr.restore() # vim:sw=4:et PK!$((+gaphor/diagram/actions/tests/test_action.py""" Test actions. """ from gaphor import UML from gaphor.diagram.actions.action import ActionItem from gaphor.tests.testcase import TestCase class ActionTestCase(TestCase): def test_action(self): """Test creation of actions. """ self.create(ActionItem, UML.Action) PK!h^>)gaphor/diagram/actions/tests/test_flow.pyimport gaphor.UML as UML from gaphor.diagram import items from gaphor.tests.testcase import TestCase class FlowTestCase(TestCase): def test_flow(self): self.create(items.FlowItem, UML.ControlFlow) def test_name(self): """ Test updating of flow name text. """ flow = self.create(items.FlowItem, UML.ControlFlow) flow.subject.name = "Blah" self.assertEqual("Blah", flow._name.text) flow.subject = None self.assertEqual("", flow._name.text) def test_guard(self): """ Test updating of flow guard text. """ flow = self.create(items.FlowItem, UML.ControlFlow) self.assertEqual("", flow._guard.text) flow.subject.guard = "GuardMe" self.assertEqual("GuardMe", flow._guard.text) flow.subject = None self.assertEqual("", flow._guard.text) def test_persistence(self): """ TODO: Test connector item saving/loading """ pass PK!d2˸%%gaphor/diagram/activitynodes.py""" Activity control nodes. """ import math from gaphas.util import path_ellipse from gaphas.state import observed, reversible_property from gaphas.item import Handle, Item, LinePort from gaphas.constraint import EqualsConstraint, LessThanConstraint from gaphas.geometry import distance_line_point from gaphor import UML from gaphor.core import inject from gaphor.diagram.diagramitem import DiagramItem from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.style import ( ALIGN_LEFT, ALIGN_CENTER, ALIGN_TOP, ALIGN_RIGHT, ALIGN_BOTTOM, ) from gaphor.diagram.style import get_text_point DEFAULT_JOIN_SPEC = "and" class ActivityNodeItem(NamedItem): """Basic class for simple activity nodes. Simple activity node is not resizable. """ __style__ = {"name-outside": True, "name-padding": (2, 2, 2, 2)} def __init__(self, id=None): NamedItem.__init__(self, id) # Do not allow resizing of the node for h in self._handles: h.movable = False class InitialNodeItem(ActivityNodeItem): """ Representation of initial node. Initial node has name which is put near top-left side of node. """ __uml__ = UML.InitialNode __style__ = {"min-size": (20, 20), "name-align": (ALIGN_LEFT, ALIGN_TOP)} RADIUS = 10 def draw(self, context): cr = context.cairo r = self.RADIUS d = r * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.fill() super(InitialNodeItem, self).draw(context) class ActivityFinalNodeItem(ActivityNodeItem): """Representation of activity final node. Activity final node has name which is put near right-bottom side of node. """ __uml__ = UML.ActivityFinalNode __style__ = {"min-size": (30, 30), "name-align": (ALIGN_RIGHT, ALIGN_BOTTOM)} RADIUS_1 = 10 RADIUS_2 = 15 def draw(self, context): cr = context.cairo r = self.RADIUS_2 + 1 d = self.RADIUS_1 * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.fill() d = r * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.set_line_width(2) cr.stroke() super(ActivityFinalNodeItem, self).draw(context) class FlowFinalNodeItem(ActivityNodeItem): """ Representation of flow final node. Flow final node has name which is put near right-bottom side of node. """ __uml__ = UML.FlowFinalNode __style__ = {"min-size": (20, 20), "name-align": (ALIGN_RIGHT, ALIGN_BOTTOM)} RADIUS = 10 def draw(self, context): cr = context.cairo r = self.RADIUS d = r * 2 path_ellipse(cr, r, r, d, d) cr.stroke() dr = (1 - math.sin(math.pi / 4)) * r cr.move_to(dr, dr) cr.line_to(d - dr, d - dr) cr.move_to(dr, d - dr) cr.line_to(d - dr, dr) cr.stroke() super(FlowFinalNodeItem, self).draw(context) class DecisionNodeItem(ActivityNodeItem): """ Representation of decision or merge node. """ __uml__ = UML.DecisionNode __style__ = {"min-size": (20, 30), "name-align": (ALIGN_LEFT, ALIGN_TOP)} RADIUS = 15 def __init__(self, id=None): ActivityNodeItem.__init__(self, id) self._combined = None # self.set_prop_persistent('combined') def save(self, save_func): if self._combined: save_func("combined", self._combined, reference=True) super(DecisionNodeItem, self).save(save_func) def load(self, name, value): if name == "combined": self._combined = value else: super(DecisionNodeItem, self).load(name, value) @observed def _set_combined(self, value): # self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def draw(self, context): """ Draw diamond shape, which represents decision and merge nodes. """ cr = context.cairo r = self.RADIUS r2 = r * 2 / 3 cr.move_to(r2, 0) cr.line_to(r2 * 2, r) cr.line_to(r2, r * 2) cr.line_to(0, r) cr.close_path() cr.stroke() super(DecisionNodeItem, self).draw(context) class ForkNodeItem(Item, DiagramItem): """ Representation of fork and join node. """ element_factory = inject("element_factory") __uml__ = UML.ForkNode __style__ = { "min-size": (6, 45), "name-align": (ALIGN_CENTER, ALIGN_BOTTOM), "name-padding": (2, 2, 2, 2), "name-outside": True, "name-align-str": None, } STYLE_TOP = {"text-align": (ALIGN_CENTER, ALIGN_TOP), "text-outside": True} def __init__(self, id=None): Item.__init__(self) DiagramItem.__init__(self, id) h1, h2 = Handle(), Handle() self._handles.append(h1) self._handles.append(h2) self._ports.append(LinePort(h1.pos, h2.pos)) self._combined = None self._join_spec = self.add_text( "joinSpec", pattern="{ joinSpec = %s }", style=self.STYLE_TOP, visible=self.is_join_spec_visible, ) self._name = self.add_text( "name", style={ "text-align": self.style.name_align, "text-padding": self.style.name_padding, "text-outside": self.style.name_outside, "text-align-str": self.style.name_align_str, "text-align-group": "stereotype", }, editable=True, ) self.watch("subject.name", self.on_named_element_name).watch( "subject.joinSpec", self.on_join_node_join_spec ) def save(self, save_func): save_func("matrix", tuple(self.matrix)) save_func("height", float(self._handles[1].pos.y)) if self._combined: save_func("combined", self._combined, reference=True) DiagramItem.save(self, save_func) def load(self, name, value): if name == "matrix": self.matrix = eval(value) elif name == "height": self._handles[1].pos.y = eval(value) elif name == "combined": self._combined = value else: # DiagramItem.load(self, name, value) super(ForkNodeItem, self).load(name, value) def postload(self): subject = self.subject if subject and isinstance(subject, UML.JoinNode) and subject.joinSpec: self._join_spec.text = self.subject.joinSpec self.on_named_element_name(None) super(ForkNodeItem, self).postload() @observed def _set_combined(self, value): # self.preserve_property('combined') self._combined = value combined = reversible_property(lambda s: s._combined, _set_combined) def setup_canvas(self): super(ForkNodeItem, self).setup_canvas() self.register_handlers() h1, h2 = self._handles cadd = self.canvas.solver.add_constraint c1 = EqualsConstraint(a=h1.pos.x, b=h2.pos.x) c2 = LessThanConstraint(smaller=h1.pos.y, bigger=h2.pos.y, delta=30) self.__constraints = (cadd(c1), cadd(c2)) list(map(self.canvas.solver.add_constraint, self.__constraints)) def teardown_canvas(self): super(ForkNodeItem, self).teardown_canvas() list(map(self.canvas.solver.remove_constraint, self.__constraints)) self.unregister_handlers() def is_join_spec_visible(self): """ Check if join specification should be displayed. """ return ( isinstance(self.subject, UML.JoinNode) and self.subject.joinSpec is not None and self.subject.joinSpec != DEFAULT_JOIN_SPEC ) def text_align(self, extents, align, padding, outside): h1, h2 = self._handles w, _ = self.style.min_size h = h2.pos.y - h1.pos.y x, y = get_text_point(extents, w, h, align, padding, outside) return x, y def pre_update(self, context): self.update_stereotype() Item.pre_update(self, context) DiagramItem.pre_update(self, context) def post_update(self, context): Item.post_update(self, context) DiagramItem.post_update(self, context) def draw(self, context): """ Draw vertical line - symbol of fork and join nodes. Join specification is also drawn above the item. """ Item.draw(self, context) DiagramItem.draw(self, context) cr = context.cairo cr.set_line_width(6) h1, h2 = self._handles cr.move_to(h1.pos.x, h1.pos.y) cr.line_to(h2.pos.x, h2.pos.y) cr.stroke() def point(self, pos): h1, h2 = self._handles d, p = distance_line_point(h1.pos, h2.pos, pos) # Substract line_width / 2 return d - 3 def on_named_element_name(self, event): print("on_named_element_name", self.subject) subject = self.subject if subject: self._name.text = subject.name self.request_update() def on_join_node_join_spec(self, event): subject = self.subject if subject: self._join_spec.text = subject.joinSpec or DEFAULT_JOIN_SPEC self.request_update() def is_join_node(subject): """ Check if ``subject`` is join node. """ return subject and isinstance(subject, UML.JoinNode) # vim:sw=4:et PK! Lgaphor/diagram/actor.py""" Actor item classes. """ from math import pi from gaphor import UML from gaphor.diagram.style import ALIGN_CENTER, ALIGN_BOTTOM from gaphor.diagram.classifier import ClassifierItem class ActorItem(ClassifierItem): """ Actor item is a classifier in icon mode. Maybe it should be possible to switch to comparment mode in the future. """ __uml__ = UML.Actor HEAD = 11 ARM = 19 NECK = 10 BODY = 20 __style__ = { "min-size": (ARM * 2, HEAD + NECK + BODY + ARM), "name-align": (ALIGN_CENTER, ALIGN_BOTTOM), "name-padding": (5, 0, 5, 0), "name-outside": True, } def __init__(self, id=None): ClassifierItem.__init__(self, id) self.drawing_style = self.DRAW_ICON def draw_icon(self, context): """ Draw actor's icon creature. """ super(ActorItem, self).draw(context) cr = context.cairo head, neck, arm, body = self.HEAD, self.NECK, self.ARM, self.BODY fx = self.width / (arm * 2) fy = self.height / (head + neck + body + arm) x = arm * fx y = (head / 2) * fy cy = head * fy cr.move_to(x + head * fy / 2.0, y) cr.arc(x, y, head * fy / 2.0, 0, 2 * pi) cr.move_to(x, y + cy / 2) cr.line_to(arm * fx, (head + neck + body) * fy) cr.move_to(0, (head + neck) * fy) cr.line_to(arm * 2 * fx, (head + neck) * fy) cr.move_to(0, (head + neck + body + arm) * fy) cr.line_to(arm * fx, (head + neck + body) * fy) cr.line_to(arm * 2 * fx, (head + neck + body + arm) * fy) cr.stroke() # vim:sw=4:et PK!WL֪gaphor/diagram/artifact.py""" Artifact item. """ from gaphor import UML from gaphor.diagram.classifier import ClassifierItem class ArtifactItem(ClassifierItem): __uml__ = UML.Artifact __icon__ = True __style__ = {"name-padding": (10, 25, 10, 10)} ICON_HEIGHT = 20 def __init__(self, id=None): ClassifierItem.__init__(self, id) self.height = 50 self.width = 120 # Set drawing style to compartment w/ small icon self.drawing_style = self.DRAW_COMPARTMENT_ICON self._line = [] def pre_update_compartment_icon(self, context): super(ArtifactItem, self).pre_update_compartment_icon(context) w = self.ICON_WIDTH h = self.ICON_HEIGHT ix, iy = self.get_icon_pos() ear = 5 self._line = ( (ix + w - ear, iy + ear), (ix + w, iy + ear), (ix + w - ear, iy), (ix, iy), (ix, iy + h), (ix + w, iy + h), (ix + w, iy + ear), ) def draw_compartment_icon(self, context): cr = context.cairo cr.save() self.draw_compartment(context) cr.restore() # draw icon w = self.ICON_WIDTH h = self.ICON_HEIGHT ix, iy = self.get_icon_pos() ear = 5 cr.set_line_width(1.0) cr.move_to(ix + w - ear, iy) for x, y in self._line: cr.line_to(x, y) cr.stroke() # vim:sw=4:et PK!"gaphor/diagram/classes/__init__.pyPK!a I I%gaphor/diagram/classes/association.py""" Association item - graphical representation of an association. Plan: - transform AssociationEnd in a (dumb) data class - for assocation name and direction tag, use the same trick as is used for line ends. """ # TODO: for Association.postload(): in some cases where the association ends # are connected to the same Class, the head_end property is connected to the # tail end and visa versa. from gaphas.geometry import Rectangle, distance_point_point_fast from gaphas.geometry import distance_rectangle_point from gaphas.state import reversible_property from gaphor import UML from gaphor.diagram.diagramline import NamedLine from gaphor.diagram.textelement import text_extents, text_multiline class AssociationItem(NamedLine): """ AssociationItem represents associations. An AssociationItem has two AssociationEnd items. Each AssociationEnd item represents a Property (with Property.association == my association). """ __uml__ = UML.Association def __init__(self, id=None): NamedLine.__init__(self, id) # AssociationEnds are really inseperable from the AssociationItem. # We give them the same id as the association item. self._head_end = AssociationEnd(owner=self, end="head") self._tail_end = AssociationEnd(owner=self, end="tail") # Direction depends on the ends that hold the ownedEnd attributes. self._show_direction = False self._dir_angle = 0 self._dir_pos = 0, 0 # self.watch('subject.ownedEnd')\ # .watch('subject.memberEnd') # For the association ends: base = "subject.memberEnd." self.watch(base + "name", self.on_association_end_value).watch( base + "aggregation", self.on_association_end_value ).watch(base + "classifier", self.on_association_end_value).watch( base + "visibility", self.on_association_end_value ).watch( base + "lowerValue", self.on_association_end_value ).watch( base + "upperValue", self.on_association_end_value ).watch( base + "owningAssociation", self.on_association_end_value ).watch( base + "type.ownedAttribute", self.on_association_end_value ).watch( base + "type.ownedAttribute", self.on_association_end_value ).watch( base + "appliedStereotype", self.on_association_end_value ).watch( base + "appliedStereotype.slot", self.on_association_end_value ).watch( base + "appliedStereotype.slot.definingFeature.name", self.on_association_end_value, ).watch( base + "appliedStereotype.slot.value", self.on_association_end_value ).watch( "subject.ownedEnd" ).watch( "subject.navigableOwnedEnd" ) def set_show_direction(self, dir): self._show_direction = dir self.request_update() show_direction = reversible_property( lambda s: s._show_direction, set_show_direction ) def setup_canvas(self): super(AssociationItem, self).setup_canvas() def teardown_canvas(self): super(AssociationItem, self).teardown_canvas() def save(self, save_func): NamedLine.save(self, save_func) save_func("show-direction", self._show_direction) if self._head_end.subject: save_func("head-subject", self._head_end.subject) if self._tail_end.subject: save_func("tail-subject", self._tail_end.subject) def load(self, name, value): # end_head and end_tail were used in an older Gaphor version if name in ("head_end", "head_subject", "head-subject"): # type(self._head_end).subject.load(self._head_end, value) # self._head_end.load('subject', value) self._head_end.subject = value elif name in ("tail_end", "tail_subject", "tail-subject"): # type(self._tail_end).subject.load(self._tail_end, value) # self._tail_end.load('subject', value) self._tail_end.subject = value else: NamedLine.load(self, name, value) def postload(self): NamedLine.postload(self) self._head_end.set_text() self._tail_end.set_text() head_end = property(lambda self: self._head_end) tail_end = property(lambda self: self._tail_end) def unlink(self): self._head_end.unlink() self._tail_end.unlink() super(AssociationItem, self).unlink() def invert_direction(self): """ Invert the direction of the association, this is done by swapping the head and tail-ends subjects. """ if not self.subject: return self.subject.memberEnd.swap( self.subject.memberEnd[0], self.subject.memberEnd[1] ) self.request_update() def on_named_element_name(self, event): """ Update names of the association as well as its ends. Override NamedLine.on_named_element_name. """ if event is None: super(AssociationItem, self).on_named_element_name(event) self.on_association_end_value(event) elif event.element is self.subject: super(AssociationItem, self).on_named_element_name(event) else: self.on_association_end_value(event) def on_association_end_value(self, event): """ Handle events and update text on association end. """ # if event: # element = event.element # for end in (self._head_end, self._tail_end): # subject = end.subject # if subject and element in (subject, subject.lowerValue, \ # subject.upperValue, subject.taggedValue): # end.set_text() # self.request_update() ## break; # else: for end in (self._head_end, self._tail_end): end.set_text() self.request_update() def post_update(self, context): """ Update the shapes and sub-items of the association. """ handles = self.handles() # Update line endings: head_subject = self._head_end.subject tail_subject = self._tail_end.subject # Update line ends using the aggregation and isNavigable values: if head_subject and tail_subject: if tail_subject.aggregation == "composite": self.draw_head = self.draw_head_composite elif tail_subject.aggregation == "shared": self.draw_head = self.draw_head_shared elif self._head_end.subject.navigability is True: self.draw_head = self.draw_head_navigable elif self._head_end.subject.navigability is False: self.draw_head = self.draw_head_none else: self.draw_head = self.draw_head_undefined if head_subject.aggregation == "composite": self.draw_tail = self.draw_tail_composite elif head_subject.aggregation == "shared": self.draw_tail = self.draw_tail_shared elif self._tail_end.subject.navigability is True: self.draw_tail = self.draw_tail_navigable elif self._tail_end.subject.navigability is False: self.draw_tail = self.draw_tail_none else: self.draw_tail = self.draw_tail_undefined if self._show_direction: inverted = self.tail_end.subject is self.subject.memberEnd[0] pos, angle = self._get_center_pos(inverted) self._dir_pos = pos self._dir_angle = angle else: self.draw_head = self.draw_head_undefined self.draw_tail = self.draw_tail_undefined # update relationship after self.set calls to avoid circural updates super(AssociationItem, self).post_update(context) # Calculate alignment of the head name and multiplicity self._head_end.post_update(context, handles[0].pos, handles[1].pos) # Calculate alignment of the tail name and multiplicity self._tail_end.post_update(context, handles[-1].pos, handles[-2].pos) def point(self, pos): """ Returns the distance from the Association to the (mouse) cursor. """ return min( super(AssociationItem, self).point(pos), self._head_end.point(pos), self._tail_end.point(pos), ) def draw_head_none(self, context): """ Draw an 'x' on the line end to indicate no navigability at association head. """ cr = context.cairo cr.move_to(6, -4) cr.rel_line_to(8, 8) cr.rel_move_to(0, -8) cr.rel_line_to(-8, 8) cr.stroke() cr.move_to(0, 0) def draw_tail_none(self, context): """ Draw an 'x' on the line end to indicate no navigability at association tail. """ cr = context.cairo cr.line_to(0, 0) cr.move_to(6, -4) cr.rel_line_to(8, 8) cr.rel_move_to(0, -8) cr.rel_line_to(-8, 8) cr.stroke() def _draw_diamond(self, cr): """ Helper function to draw diamond shape for shared and composite aggregations. """ cr.move_to(20, 0) cr.line_to(10, -6) cr.line_to(0, 0) cr.line_to(10, 6) # cr.line_to(20, 0) cr.close_path() def draw_head_composite(self, context): """ Draw a closed diamond on the line end to indicate composite aggregation at association head. """ cr = context.cairo self._draw_diamond(cr) context.cairo.fill_preserve() cr.stroke() cr.move_to(20, 0) def draw_tail_composite(self, context): """ Draw a closed diamond on the line end to indicate composite aggregation at association tail. """ cr = context.cairo cr.line_to(20, 0) cr.stroke() self._draw_diamond(cr) cr.fill_preserve() cr.stroke() def draw_head_shared(self, context): """ Draw an open diamond on the line end to indicate shared aggregation at association head. """ cr = context.cairo self._draw_diamond(cr) cr.move_to(20, 0) def draw_tail_shared(self, context): """ Draw an open diamond on the line end to indicate shared aggregation at association tail. """ cr = context.cairo cr.line_to(20, 0) cr.stroke() self._draw_diamond(cr) cr.stroke() def draw_head_navigable(self, context): """ Draw a normal arrow to indicate association end navigability at association head. """ cr = context.cairo cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) cr.stroke() cr.move_to(0, 0) def draw_tail_navigable(self, context): """ Draw a normal arrow to indicate association end navigability at association tail. """ cr = context.cairo cr.line_to(0, 0) cr.stroke() cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) def draw_head_undefined(self, context): """ Draw nothing to indicate undefined association end at association head. """ context.cairo.move_to(0, 0) def draw_tail_undefined(self, context): """ Draw nothing to indicate undefined association end at association tail. """ context.cairo.line_to(0, 0) def draw(self, context): super(AssociationItem, self).draw(context) cr = context.cairo self._head_end.draw(context) self._tail_end.draw(context) if self._show_direction: cr.save() try: cr.translate(*self._dir_pos) cr.rotate(self._dir_angle) cr.move_to(0, 0) cr.line_to(6, 5) cr.line_to(0, 10) cr.fill() finally: cr.restore() def item_at(self, x, y): if distance_point_point_fast(self._handles[0].pos, (x, y)) < 10: return self._head_end elif distance_point_point_fast(self._handles[-1].pos, (x, y)) < 10: return self._tail_end return self class AssociationEnd(UML.Presentation): """ An association end represents one end of an association. An association has two ends. An association end has two labels: one for the name and one for the multiplicity (and maybe one for tagged values in the future). An AsociationEnd has no ID, hence it will not be stored, but it will be recreated by the owning Association. TODO: - add on_point() and let it return min(distance(_name), distance(_mult)) or the first 20-30 units of the line, for association end popup menu. """ def __init__(self, owner, id=None, end=None): UML.Presentation.__init__(self, id=False) # Transient object self._owner = owner self._end = end # Rendered text for name and multiplicity self._name = None self._mult = None self._name_bounds = Rectangle() self._mult_bounds = Rectangle() self.font = "sans 10" def request_update(self): self._owner.request_update() def set_text(self): """ Set the text on the association end. """ if self.subject: try: n, m = UML.format(self.subject) except ValueError: # need more than 0 values to unpack: property was rendered as # attribute while in a UNDO action for example. pass else: self._name = n self._mult = m self.request_update() def point_name(self, pos): drp = distance_rectangle_point return drp(self._name_bounds, pos) def point_mult(self, pos): drp = distance_rectangle_point return drp(self._mult_bounds, pos) def point(self, pos): return min(self.point_name(pos), self.point_mult(pos)) def get_name(self): return self._name def get_mult(self): return self._mult def post_update(self, context, p1, p2): """ Update label placement for association's name and multiplicity label. p1 is the line end and p2 is the last but one point of the line. """ cr = context.cairo ofs = 5 name_dx = 0.0 name_dy = 0.0 mult_dx = 0.0 mult_dy = 0.0 dx = float(p2[0]) - float(p1[0]) dy = float(p2[1]) - float(p1[1]) name_w, name_h = list( map(max, text_extents(cr, self._name, self.font), (10, 10)) ) mult_w, mult_h = list( map(max, text_extents(cr, self._mult, self.font), (10, 10)) ) if dy == 0: rc = 1000.0 # quite a lot... else: rc = dx / dy abs_rc = abs(rc) h = dx > 0 # right side of the box v = dy > 0 # bottom side if abs_rc > 6: # horizontal line if h: name_dx = ofs name_dy = -ofs - name_h mult_dx = ofs mult_dy = ofs else: name_dx = -ofs - name_w name_dy = -ofs - name_h mult_dx = -ofs - mult_w mult_dy = ofs elif 0 <= abs_rc <= 0.2: # vertical line if v: name_dx = -ofs - name_w name_dy = ofs mult_dx = ofs mult_dy = ofs else: name_dx = -ofs - name_w name_dy = -ofs - name_h mult_dx = ofs mult_dy = -ofs - mult_h else: # Should both items be placed on the same side of the line? r = abs_rc < 1.0 # Find out alignment of text (depends on the direction of the line) align_left = (h and not r) or (r and not h) align_bottom = (v and not r) or (r and not v) if align_left: name_dx = ofs mult_dx = ofs else: name_dx = -ofs - name_w mult_dx = -ofs - mult_w if align_bottom: name_dy = -ofs - name_h mult_dy = -ofs - name_h - mult_h else: name_dy = ofs mult_dy = ofs + mult_h self._name_bounds = Rectangle( p1[0] + name_dx, p1[1] + name_dy, width=name_w, height=name_h ) self._mult_bounds = Rectangle( p1[0] + mult_dx, p1[1] + mult_dy, width=mult_w, height=mult_h ) def point(self, pos): """Given a point (x, y) return the distance to the canvas item. """ drp = distance_rectangle_point d1 = drp(self._name_bounds, pos) d2 = drp(self._mult_bounds, pos) # try: # d3 = geometry.distance_point_point(self._point1, pos) # d4, dummy = distance_line_point(self._point1, self._point2, pos, 1.0, 0) #diacanvas.shape.CAP_ROUND) # if d3 < 15 and d4 < 5: # d3 = 0.0 # except Exception, e: # log.error("Could not determine distance", exc_info=True) d3 = 1000.0 return min(d1, d2, d3) def draw(self, context): """Draw name and multiplicity of the line end. """ if not self.subject: return cr = context.cairo text_multiline( cr, self._name_bounds[0], self._name_bounds[1], self._name, self.font ) text_multiline( cr, self._mult_bounds[0], self._mult_bounds[1], self._mult, self.font ) cr.stroke() if context.hovered or context.focused or context.draw_all: cr.set_line_width(0.5) b = self._name_bounds cr.rectangle(b.x, b.y, b.width, b.height) cr.stroke() b = self._mult_bounds cr.rectangle(b.x, b.y, b.width, b.height) cr.stroke() # vim:sw=4:et PK!a@d d $gaphor/diagram/classes/dependency.py""" Common dependencies like dependency, usage, implementation and realization. Dependency Type =============== Dependency type should be determined automatically by default. User should be able to override the dependency type. When dependency item is connected between two items, then type of the dependency cannot be changed. For example, if two class items are connected, then dependency type cannot be changed to realization as this dependency type can only exist between a component and a classifier. Function dependency_type in model factory should be used to determine type of a dependency in automatic way. """ from gaphor import UML from gaphor.diagram.diagramline import DiagramLine class DependencyItem(DiagramLine): """ Dependency item represents several types of dependencies, i.e. normal dependency or usage. Usually a dependency looks like a dashed line with an arrow head. The dependency can have a stereotype attached to it, stating the kind of dependency we're dealing with. In case of usage dependency connected to folded interface, the line is drawn as solid line without arrow head. """ __uml__ = UML.Dependency # do not use issubclass, because issubclass(UML.Implementation, UML.Realization) # we need to be very strict here __stereotype__ = { "use": lambda self: self._dependency_type == UML.Usage, "realize": lambda self: self._dependency_type == UML.Realization, "implements": lambda self: self._dependency_type == UML.Implementation, } def __init__(self, id=None): DiagramLine.__init__(self, id) self._dependency_type = UML.Dependency self.auto_dependency = True self._solid = False def save(self, save_func): DiagramLine.save(self, save_func) save_func("auto_dependency", self.auto_dependency) def load(self, name, value): if name == "auto_dependency": self.auto_dependency = eval(value) else: DiagramLine.load(self, name, value) def postload(self): if self.subject: dependency_type = self.subject.__class__ DiagramLine.postload(self) self._dependency_type = dependency_type else: DiagramLine.postload(self) def set_dependency_type(self, dependency_type): self._dependency_type = dependency_type dependency_type = property(lambda s: s._dependency_type, set_dependency_type) def draw_head(self, context): cr = context.cairo if not self._solid: cr.set_dash((), 0) cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) cr.stroke() cr.move_to(0, 0) def draw(self, context): if not self._solid: context.cairo.set_dash((7.0, 5.0), 0) super(DependencyItem, self).draw(context) # vim:sw=4:et PK!n66(gaphor/diagram/classes/generalization.py""" Generalization -- """ from gi.repository import GObject from gaphor import UML from gaphor.diagram.diagramline import DiagramLine class GeneralizationItem(DiagramLine): __uml__ = UML.Generalization __relationship__ = "general", None, "specific", "generalization" def __init__(self, id=None): DiagramLine.__init__(self, id) def draw_head(self, context): cr = context.cairo cr.move_to(0, 0) cr.line_to(15, -10) cr.line_to(15, 10) cr.close_path() cr.stroke() cr.move_to(15, 0) PK!/Y(gaphor/diagram/classes/implementation.py""" Implementation of interface. """ from gaphor import UML from gaphor.diagram.diagramline import DiagramLine class ImplementationItem(DiagramLine): __uml__ = UML.Implementation def __init__(self, id=None): DiagramLine.__init__(self, id) self._solid = False def draw_head(self, context): cr = context.cairo cr.move_to(0, 0) if not self._solid: cr.set_dash((), 0) cr.line_to(15, -10) cr.line_to(15, 10) cr.close_path() cr.stroke() cr.move_to(15, 0) def draw(self, context): if not self._solid: context.cairo.set_dash((7.0, 5.0), 0) super(ImplementationItem, self).draw(context) # vim:sw=4 PK!h $$#gaphor/diagram/classes/interface.py""" Interface item implementation. There are several notations supported - class box with interface stereotype - folded interface - ball is drawn to indicate provided interface - socket is drawn to indicate required interface Interface item can act as icon of assembly connector, see `gaphor.diagram.connector` module documentation for details. *Documentation of this module does not take into accout assembly connector icon mode.* Folded Interface Item ===================== Folded interface notation is reserved for very simple situations. When interface is folded - only an implementation can be connected (ball - provided interface) - or only usage dependency can be connected (socket - required interface) Above means that interface cannot be folded when - both, usage dependency and implementation are connected - any other lines are connected Dependencies ------------ Dependencies between folded interfaces are *not supported* +---------------------+---------------------+ | *Supported* | *Unsupported* | +=====================+=====================+ | :: | :: | | | | | |A|--( O--|B| | |A|--(--->O--|B| | | Z Z | Z Z | +---------------------+---------------------+ On above diagram, A requires interface Z and B provides interface Z. Additionally, on the right diagram, Z is connected to itself with dependency. There is no need for additional dependency - UML data model provides information, that Z is common for A and B (A requires Z, B provides Z) - on a diagram, both folded interface items (required and provided) represent the same interface, which is easily identifiable with its name Even more, adding a dependency between folded interfaces provides information, on UML data model level, that an interface depenends on itself but it is not the intention of this (*unsupported*) notation. For more examples of non-supported by Gaphor notation, see http://martinfowler.com/bliki/BallAndSocket.html. Folding and Connecting ---------------------- Current approach to folding and connecting lines to an interface is as follows - allow folding/unfolding of an interface only when there is only one implementation or depenedency usage connected - when interface is folded, allow only one implementation or depenedency usage to be connected Folding and unfolding is performed by `InterfacePropertyPage` class. """ from math import pi from gaphas.connector import LinePort from gaphas.geometry import distance_line_point, distance_point_point from gaphas.item import NW, NE, SE, SW from gaphas.state import observed, reversible_property from gaphor import UML from gaphor.diagram.classes.klass import ClassItem from gaphor.diagram.style import ALIGN_TOP, ALIGN_BOTTOM, ALIGN_CENTER class InterfacePort(LinePort): """ Interface connection port. It is simple line port, which changes glue behaviour depending on interface folded state. If interface is folded, then `InterfacePort.glue` method suggests connection in the middle of the port. The port provides rotation angle information as well. Rotation angle is direction the port is facing (i.e. 0 is north, PI/2 is west, etc.). The rotation angle shall be used to determine rotation of required interface notation (socket's arc is in the same direction as the angle). :IVariables: angle Rotation angle. iface Interface owning port. """ def __init__(self, start, end, iface, angle): super(InterfacePort, self).__init__(start, end) self.angle = angle self.iface = iface self.required = False self.provided = False def glue(self, pos): """ Behaves like simple line port, but for folded interface suggests connection to the middle point of a port. """ if self.iface.folded: px = (self.start.x + self.end.x) / 2 py = (self.start.y + self.end.y) / 2 d = distance_point_point((px, py), pos) return (px, py), d else: p1 = self.start p2 = self.end d, pl = distance_line_point(p1, p2, pos) return pl, d class InterfaceItem(ClassItem): """ Interface item supporting class box, folded notations and assembly connector icon mode. When in folded mode, provided (ball) notation is used by default. """ __uml__ = UML.Interface __stereotype__ = {"interface": lambda self: self.drawing_style != self.DRAW_ICON} __style__ = { "icon-size": (20, 20), "icon-size-provided": (20, 20), "icon-size-required": (28, 28), "name-outside": False, } UNFOLDED_STYLE = {"text-align": (ALIGN_CENTER, ALIGN_TOP), "text-outside": False} FOLDED_STYLE = {"text-align": (ALIGN_CENTER, ALIGN_BOTTOM), "text-outside": True} RADIUS_PROVIDED = 10 RADIUS_REQUIRED = 14 # Non-folded mode. FOLDED_NONE = 0 # Folded mode, provided (ball) notation. FOLDED_PROVIDED = 1 # Folded mode, required (socket) notation. FOLDED_REQUIRED = 2 # Folded mode, notation of assembly connector icon mode (ball&socket). FOLDED_ASSEMBLY = 3 def __init__(self, id=None): ClassItem.__init__(self, id) self._folded = self.FOLDED_NONE self._angle = 0 old_f = self._name.is_visible self._name.is_visible = lambda: old_f() and self._folded != self.FOLDED_ASSEMBLY handles = self._handles h_nw = handles[NW] h_ne = handles[NE] h_sw = handles[SW] h_se = handles[SE] # edge of element define default element ports self._ports = [ InterfacePort(h_nw.pos, h_ne.pos, self, 0), InterfacePort(h_ne.pos, h_se.pos, self, pi / 2), InterfacePort(h_se.pos, h_sw.pos, self, pi), InterfacePort(h_sw.pos, h_nw.pos, self, pi * 1.5), ] self.watch( "subject.ownedAttribute", self.on_class_owned_attribute ).watch( "subject.ownedOperation", self.on_class_owned_operation ).watch( "subject.supplierDependency" ) @observed def set_drawing_style(self, style): """ In addition to setting the drawing style, the handles are make non-movable if the icon (folded) style is used. """ super(InterfaceItem, self).set_drawing_style(style) if self._drawing_style == self.DRAW_ICON: self.folded = self.FOLDED_PROVIDED # set default folded mode else: self.folded = self.FOLDED_NONE # unset default folded mode drawing_style = reversible_property( lambda self: self._drawing_style, set_drawing_style ) def _is_folded(self): """ Check if interface item is folded interface item. """ return self._folded def _set_folded(self, folded): """ Set folded notation. :param folded: Folded state, see FOLDED_* constants. """ self._folded = folded if folded == self.FOLDED_NONE: movable = True draw_mode = self.DRAW_COMPARTMENT name_style = self.UNFOLDED_STYLE else: if self._folded == self.FOLDED_PROVIDED: icon_size = self.style.icon_size_provided else: # required interface or assembly icon mode icon_size = self.style.icon_size_required self.style.icon_size = icon_size self.min_width, self.min_height = icon_size self.width, self.height = icon_size # update only h_se handle - rest of handles should be updated by # constraints h_nw = self._handles[NW] h_se = self._handles[SE] h_se.pos.x = h_nw.pos.x + self.min_width h_se.pos.y = h_nw.pos.y + self.min_height movable = False draw_mode = self.DRAW_ICON name_style = self.FOLDED_STYLE # call super method to avoid recursion (set_drawing_style calls # _set_folded method) super(InterfaceItem, self).set_drawing_style(draw_mode) self._name.style.update(name_style) for h in self._handles: h.movable = movable self.request_update() folded = property( _is_folded, _set_folded, doc="Check or set folded notation, see FOLDED_* constants.", ) def draw_icon(self, context): cr = context.cairo h_nw = self._handles[NW] cx, cy = (h_nw.pos.x + self.width / 2, h_nw.pos.y + self.height / 2) required = ( self._folded == self.FOLDED_REQUIRED or self._folded == self.FOLDED_ASSEMBLY ) provided = ( self._folded == self.FOLDED_PROVIDED or self._folded == self.FOLDED_ASSEMBLY ) if required: cr.save() cr.arc_negative(cx, cy, self.RADIUS_REQUIRED, self._angle, pi + self._angle) cr.restore() if provided: cr.move_to(cx + self.RADIUS_PROVIDED, cy) cr.arc(cx, cy, self.RADIUS_PROVIDED, 0, pi * 2) cr.stroke() super(InterfaceItem, self).draw(context) PK!h5gaphor/diagram/classes/klass.py"""This module defines two visualization items - OperationItem and ClassItem.""" from gaphas.state import observed, reversible_property from gaphor import UML from gaphor.i18n import _ from gaphor.diagram.classifier import ClassifierItem from gaphor.diagram.compartment import FeatureItem class OperationItem(FeatureItem): """This is visualization of a class operation and is a type of FeatureItem.""" def render(self): """Render the OperationItem.""" return ( UML.format( self.subject, visibility=True, type=True, multiplicity=True, default=True, ) or "" ) class ClassItem(ClassifierItem): """This item visualizes a Class instance. A ClassItem contains two compartments (Compartment): one for attributes and one for operations. To add and remove such features the ClassItem implements the CanvasGroupable interface. Items can be added by callling class.add() and class.remove(). This is used to handle CanvasItems, not UML objects!""" __uml__ = UML.Class, UML.Stereotype __stereotype__ = { "stereotype": UML.Stereotype, "metaclass": lambda self: (not isinstance(self.subject, UML.Stereotype)) and hasattr(self.subject, "extension") and self.subject.extension, } __style__ = { "extra-space": "compartment", "abstract-feature-font": "sans italic 10", } def __init__(self, id=None): """Constructor. Initialize the ClassItem. This will also call the ClassifierItem constructor. The drawing style is set here as well. The class item will create two compartments - one for attributes and another for operations.""" ClassifierItem.__init__(self, id) self.drawing_style = self.DRAW_COMPARTMENT self._attributes = self.create_compartment("attributes") self._attributes.font = self.style.feature_font self._operations = self.create_compartment("operations") self._operations.font = self.style.feature_font self._operations.use_extra_space = True self.watch( "subject.ownedOperation", self.on_class_owned_operation ).watch( "subject.ownedAttribute.association", self.on_class_owned_attribute ).watch( "subject.ownedAttribute.name" ).watch( "subject.ownedAttribute.isStatic" ).watch( "subject.ownedAttribute.isDerived" ).watch( "subject.ownedAttribute.visibility" ).watch( "subject.ownedAttribute.lowerValue" ).watch( "subject.ownedAttribute.upperValue" ).watch( "subject.ownedAttribute.defaultValue" ).watch( "subject.ownedAttribute.typeValue" ).watch( "subject.ownedOperation.name" ).watch( "subject.ownedOperation.isAbstract", self.on_operation_is_abstract ).watch( "subject.ownedOperation.isStatic" ).watch( "subject.ownedOperation.visibility" ).watch( "subject.ownedOperation.returnResult.lowerValue" ).watch( "subject.ownedOperation.returnResult.upperValue" ).watch( "subject.ownedOperation.returnResult.typeValue" ).watch( "subject.ownedOperation.formalParameter.lowerValue" ).watch( "subject.ownedOperation.formalParameter.upperValue" ).watch( "subject.ownedOperation.formalParameter.typeValue" ).watch( "subject.ownedOperation.formalParameter.defaultValue" ) def save(self, save_func): """Store the show- properties *before* the width/height properties, otherwise the classes will unintentionally grow due to "visible" attributes or operations.""" self.save_property(save_func, "show-attributes") self.save_property(save_func, "show-operations") ClassifierItem.save(self, save_func) def postload(self): """Called once the ClassItem has been loaded. First the ClassifierItem is "post-loaded", then the attributes and operations are synchronized.""" super(ClassItem, self).postload() self.sync_attributes() self.sync_operations() @observed def _set_show_operations(self, value): """Sets the show operations property. This will either show or hide the operations compartment of the ClassItem. This is part of the show_operations property.""" self._operations.visible = value self._operations.use_extra_space = value self._attributes.use_extra_space = not self._operations.visible show_operations = reversible_property( fget=lambda s: s._operations.visible, fset=_set_show_operations ) @observed def _set_show_attributes(self, value): """Sets the show attributes property. This will either show or hide the attributes compartment of the ClassItem. This is part of the show_attributes property.""" self._attributes.visible = value show_attributes = reversible_property( fget=lambda s: s._attributes.visible, fset=_set_show_attributes ) def _create_attribute(self, attribute): """Create a new attribute item. This will create a new FeatureItem and assigns the specified attribute as the subject.""" new = FeatureItem() new.subject = attribute new.font = self.style.feature_font self._attributes.append(new) def _create_operation(self, operation): """Create a new operation item. This will create a new OperationItem and assigns the specified operation as the subject.""" new = OperationItem() new.subject = operation new.font = self.style.feature_font self._operations.append(new) def sync_attributes(self): """Sync the contents of the attributes compartment with the data in self.subject.""" owned_attributes = [a for a in self.subject.ownedAttribute if not a.association] self.sync_uml_elements( owned_attributes, self._attributes, self._create_attribute ) def sync_operations(self): """Sync the contents of the operations compartment with the data in self.subject.""" self.sync_uml_elements( self.subject.ownedOperation, self._operations, self._create_operation ) def on_class_owned_attribute(self, event): """Event handler for owned attributes. This will synchronize the attributes of this ClassItem.""" if self.subject: self.sync_attributes() def on_class_owned_operation(self, event): """Event handler for owned operations. This will synchronize the operations of this ClassItem.""" if self.subject: self.sync_operations() def on_operation_is_abstract(self, event): """Event handler for abstract operations. This will change the font of the operation.""" o = [o for o in self._operations if o.subject is event.element] if o: o = o[0] o.font = ( (o.subject and o.subject.isAbstract) and self.style.abstract_feature_font or self.style.feature_font ) self.request_update() PK!5Y!gaphor/diagram/classes/package.py""" Package diagram item. """ from gaphor import UML from gaphor.diagram.nameditem import NamedItem class PackageItem(NamedItem): __uml__ = UML.Package, UML.Profile __stereotype__ = {"profile": UML.Profile} __style__ = { "min-size": (NamedItem.style.min_size[0], 70), "name-font": "sans bold 10", "name-padding": (25, 10, 5, 10), "tab-x": 50, "tab-y": 20, } def __init__(self, id=None): super(PackageItem, self).__init__(id) def draw(self, context): super(PackageItem, self).draw(context) cr = context.cairo o = 0.0 h = self.height w = self.width x = self.style.tab_x y = self.style.tab_y cr.move_to(x, y) cr.line_to(x, o) cr.line_to(o, o) cr.line_to(o, h) cr.line_to(w, h) cr.line_to(w, y) cr.line_to(o, y) cr.stroke() # vim:sw=4:et PK!(gaphor/diagram/classes/tests/__init__.pyPK!,@ 0gaphor/diagram/classes/tests/test_association.py""" Unnit tests for AssociationItem. """ from zope import component from gaphor.diagram.interfaces import IConnect from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram.items import ( AssociationItem, ClassItem, InterfaceItem, UseCaseItem, ActorItem, ) class AssociationItemTestCase(TestCase): services = TestCase.services + ["element_dispatcher"] def setUp(self): super(AssociationItemTestCase, self).setUp() self.assoc = self.create(AssociationItem) self.class1 = self.create(ClassItem, UML.Class) self.class2 = self.create(ClassItem, UML.Class) def test_create(self): """Test association creation and its basic properties """ self.connect(self.assoc, self.assoc.head, self.class1) self.connect(self.assoc, self.assoc.tail, self.class2) self.assertTrue(isinstance(self.assoc.subject, UML.Association)) self.assertTrue(self.assoc.head_end.subject is not None) self.assertTrue(self.assoc.tail_end.subject is not None) self.assertFalse(self.assoc.show_direction) self.assoc.show_direction = True self.assertTrue(self.assoc.show_direction) def test_invert_direction(self): """Test association direction inverting """ self.connect(self.assoc, self.assoc.head, self.class1) self.connect(self.assoc, self.assoc.tail, self.class2) head_subject = self.assoc.subject.memberEnd[0] tail_subject = self.assoc.subject.memberEnd[1] self.assoc.invert_direction() self.assertTrue(head_subject is self.assoc.subject.memberEnd[1]) self.assertTrue(tail_subject is self.assoc.subject.memberEnd[0]) def test_association_end_updates(self): """Test association end navigability connected to a class""" from gaphas.canvas import Canvas canvas = Canvas() c1 = self.create(ClassItem, UML.Class) c2 = self.create(ClassItem, UML.Class) a = self.create(AssociationItem) self.connect(a, a.head, c1) c = self.get_connected(a.head) self.assertTrue(c is c1) self.connect(a, a.tail, c2) c = self.get_connected(a.tail) self.assertTrue(c is c2) assert a.subject.memberEnd, a.subject.memberEnd assert a.subject.memberEnd[0] is a.head_end.subject assert a.subject.memberEnd[1] is a.tail_end.subject assert a.subject.memberEnd[0].name is None dispatcher = self.get_service("element_dispatcher") print((a.subject.memberEnd[0], UML.Property.name) in dispatcher._handlers) print("*" * 60) a.subject.memberEnd[0].name = "blah" print("*" * 60) self.diagram.canvas.update() assert a.head_end._name == "+ blah", a.head_end.get_name() def test_association_orthogonal(self): c1 = self.create(ClassItem, UML.Class) c2 = self.create(ClassItem, UML.Class) a = self.create(AssociationItem) self.connect(a, a.head, c1) c = self.get_connected(a.head) self.assertTrue(c is c1) a.matrix.translate(100, 100) self.connect(a, a.tail, c2) c = self.get_connected(a.tail) self.assertTrue(c is c2) try: a.orthogonal = True except ValueError: pass # Expected, hanve only 2 handles, need 3 or more else: assert False, "Can not set line to orthogonal with less than 3 handles" PK!b*gaphor/diagram/classes/tests/test_class.py""" Test classes. """ import logging from gaphor import UML from gaphor.diagram.classes.klass import ClassItem from gaphor.tests.testcase import TestCase log = logging.getLogger(__name__) class ClassTestCase(TestCase): def test_compartments(self): """ Test creation of classes and working of compartments """ element_factory = self.element_factory diagram = element_factory.create(UML.Diagram) klass = diagram.create(ClassItem, subject=element_factory.create(UML.Class)) self.assertEqual(2, len(klass._compartments)) self.assertEqual(0, len(klass._compartments[0])) self.assertEqual(0, len(klass._compartments[1])) self.assertEqual((10, 10), klass._compartments[0].get_size()) diagram.canvas.update() self.assertEqual((10, 10), klass._compartments[0].get_size()) self.assertEqual(50, float(klass.min_height)) # min_height self.assertEqual(100, float(klass.min_width)) attr = element_factory.create(UML.Property) attr.name = 4 * "x" # about 44 pixels klass.subject.ownedAttribute = attr diagram.canvas.update() self.assertEqual(1, len(klass._compartments[0])) self.assertGreater(klass._compartments[0].get_size(), (44.0, 20.0)) oper = element_factory.create(UML.Operation) oper.name = 4 * "x" # about 44 pixels klass.subject.ownedOperation = oper oper = element_factory.create(UML.Operation) oper.name = 6 * "x" # about 66 pixels klass.subject.ownedOperation = oper diagram.canvas.update() self.assertEqual(2, len(klass._compartments[1])) self.assertGreater(klass._compartments[1].get_size(), (63.0, 34.0)) def test_attribute_removal(self): element_factory = self.element_factory diagram = element_factory.create(UML.Diagram) klass = diagram.create(ClassItem, subject=element_factory.create(UML.Class)) diagram.canvas.update() attr = element_factory.create(UML.Property) attr.name = "blah1" klass.subject.ownedAttribute = attr attr2 = element_factory.create(UML.Property) attr2.name = "blah2" klass.subject.ownedAttribute = attr2 attr = element_factory.create(UML.Property) attr.name = "blah3" klass.subject.ownedAttribute = attr diagram.canvas.update() self.assertEqual(3, len(klass._compartments[0])) attr2.unlink() diagram.canvas.update() self.assertEqual(2, len(klass._compartments[0])) def test_item_at(self): """ Test working of item_at method """ element_factory = self.element_factory diagram = element_factory.create(UML.Diagram) klass = diagram.create(ClassItem, subject=element_factory.create(UML.Class)) klass.subject.name = "Class1" diagram.canvas.update() attr = element_factory.create(UML.Property) attr.name = "blah" klass.subject.ownedAttribute = attr oper = element_factory.create(UML.Operation) oper.name = "method" klass.subject.ownedOperation = oper diagram.canvas.update() assert len(klass.compartments[0]) == 1 assert len(klass.compartments[1]) == 1 name_size = klass._header_size assert klass.item_at(10, 10) is klass assert klass.item_at(name_size[0] - 1, name_size[1] - 1) is klass padding = klass.style.compartment_padding vspacing = klass.style.compartment_vspacing x = padding[-1] + 1 y = name_size[1] + padding[0] + 2 assert klass.item_at(x, y) is not None, klass.item_at(x, y) assert klass.item_at(x, y).subject is attr, klass.item_at(x, y).subject y = name_size[1] + klass.compartments[0].height + padding[0] + 2 assert klass.item_at(x, y) is not None, klass.item_at(x, y) assert klass.item_at(x, y).subject is oper, klass.item_at(x, y).subject def test_compartment_resizing(self): element_factory = self.element_factory diagram = element_factory.create(UML.Diagram) klass = diagram.create(ClassItem, subject=element_factory.create(UML.Class)) klass.subject.name = "Class1" diagram.canvas.update() attr = element_factory.create(UML.Property) attr.name = "blah" klass.subject.ownedAttribute = attr oper = element_factory.create(UML.Operation) oper.name = "method" klass.subject.ownedOperation = oper self.assertEqual(100, klass.width) attr.name = "x" * 25 log.debug("name: %s" % attr.name) diagram.canvas.update() width = klass.width self.assertGreater(width, 170.0) PK!,gaphor/diagram/classes/tests/test_feature.pyfrom gaphor.tests.testcase import TestCase from gaphor import UML from gaphor.diagram.classes.klass import ClassItem from gaphor.diagram.compartment import FeatureItem from gaphor.UML.diagram import DiagramCanvas class FeatureTestCase(TestCase): def setUp(self): super(FeatureTestCase, self).setUp() def tearDown(self): super(FeatureTestCase, self).tearDown() def testAttribute(self): """ Test how attribute is updated """ attr = self.element_factory.create(UML.Property) UML.parse(attr, "-name:myType") clazzitem = self.create(ClassItem, UML.Class) clazzitem.subject.ownedAttribute = attr self.assertEqual(1, len(clazzitem._compartments[0])) item = clazzitem._compartments[0][0] self.assertTrue(isinstance(item, FeatureItem)) size = item.get_size() self.assertNotEqual((0, 0), size) attr.defaultValue = "myDefault" self.diagram.canvas.update() self.assertTrue(size < item.get_size()) PK!H  .gaphor/diagram/classes/tests/test_interface.py""" Test classes. """ from zope import component from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram.classes.interface import InterfaceItem class InterfaceTestCase(TestCase): def test_interface_creation(self): """Test interface creation """ iface = self.create(InterfaceItem, UML.Interface) self.assertTrue(isinstance(iface.subject, UML.Interface)) self.assertTrue(iface._name.is_visible()) # check style information self.assertFalse(iface.style.name_outside) def test_changing_to_icon_mode(self): """Test interface changing to icon mode """ iface = self.create(InterfaceItem, UML.Interface) iface.drawing_style = iface.DRAW_ICON self.assertEqual(iface.DRAW_ICON, iface.drawing_style) # default folded mode is provided self.assertTrue(iface.FOLDED_PROVIDED, iface.folded) # check if style information changed self.assertTrue(iface._name.style.text_outside) # handles are not movable anymore for h in iface.handles(): self.assertFalse(h.movable) # name is visible self.assertTrue(iface._name.is_visible()) def test_changing_to_classifier_mode(self): """Test interface changing to classifier mode """ iface = self.create(InterfaceItem, UML.Interface) iface.drawing_style = iface.DRAW_ICON iface.drawing_style = iface.DRAW_COMPARTMENT self.assertEqual(iface.DRAW_COMPARTMENT, iface.drawing_style) # check if style information changed self.assertFalse(iface._name.style.text_outside) # handles are movable again for h in iface.handles(): self.assertTrue(h.movable) def test_assembly_connector_icon_mode(self): """Test interface in assembly connector icon mode """ iface = self.create(InterfaceItem, UML.Interface) assert iface._name.is_visible() iface.folded = iface.FOLDED_ASSEMBLY self.assertFalse(iface._name.is_visible()) def test_folded_interface_persistence(self): """Test folded interface saving/loading """ iface = self.create(InterfaceItem, UML.Interface) # note: assembly folded mode.. iface.folded = iface.FOLDED_REQUIRED data = self.save() self.load(data) interfaces = self.diagram.canvas.select(lambda e: isinstance(e, InterfaceItem)) self.assertEqual(1, len(interfaces)) # ... gives provided folded mode on load; # correct folded mode is determined by connections, which will be # recreated later, i.e. required folded mode will be set when # implementation connects to the interface self.assertEqual(iface.FOLDED_PROVIDED, interfaces[0].folded) PK!ͥgaphor/diagram/classifier.py""" Classifier diagram item. """ from gaphor.diagram.compartment import CompartmentItem class ClassifierItem(CompartmentItem): """ Base class for UML classifiers. Classifiers can be abstract and this feature is supported by this class. """ __style__ = { "name-font": "sans bold 10", "abstract-name-font": "sans bold italic 10", } def __init__(self, id=None): super(ClassifierItem, self).__init__(id) self.watch("subject.isAbstract", self.on_classifier_is_abstract) def on_classifier_is_abstract(self, event): self._name.font = ( self.style.abstract_name_font if self.subject and self.subject.isAbstract else self.style.name_font ) self.request_update() def postload(self): super(ClassifierItem, self).postload() self.on_classifier_is_abstract(None) # vim:sw=4:et PK!PB9gaphor/diagram/comment.py""" CommentItem diagram item """ from gaphas.item import NW from gaphor import UML from gaphor.diagram.elementitem import ElementItem from gaphor.diagram.textelement import text_multiline, text_extents class CommentItem(ElementItem): __uml__ = UML.Comment __style__ = {"font": "sans 10"} EAR = 15 OFFSET = 5 def __init__(self, id=None): ElementItem.__init__(self, id) self.min_width = CommentItem.EAR + 2 * CommentItem.OFFSET self.height = 50 self.width = 100 self.watch("subject.body") def edit(self): # self.start_editing(self._body) pass def pre_update(self, context): if not self.subject: return cr = context.cairo e = self.EAR o = self.OFFSET w, h = text_extents( cr, self.subject.body, self.style.font, width=self.width - e ) self.min_width = w + e + o * 2 self.min_height = h + o * 2 ElementItem.pre_update(self, context) def draw(self, context): if not self.subject: return c = context.cairo # Width and height, adjusted for line width... ox = float(self._handles[NW].pos.x) oy = float(self._handles[NW].pos.y) w = self.width + ox h = self.height + oy ear = CommentItem.EAR c.move_to(w - ear, oy) line_to = c.line_to line_to(w - ear, oy + ear) line_to(w, oy + ear) line_to(w - ear, oy) line_to(ox, oy) line_to(ox, h) line_to(w, h) line_to(w, oy + ear) c.stroke() if self.subject.body: off = self.OFFSET # Do not print empty string, since cairo-win32 can't handle it. text_multiline( c, off, off, self.subject.body, self.style.font, self.width - ear, self.height, ) PK!u igaphor/diagram/commentline.py""" CommentLine -- A line that connects a comment to another model element. """ from zope import component from gaphor.diagram.diagramline import DiagramLine from gaphor.diagram.interfaces import IConnect class CommentLineItem(DiagramLine): def __init__(self, id=None): DiagramLine.__init__(self, id) def save(self, save_func): DiagramLine.save(self, save_func) def load(self, name, value): DiagramLine.load(self, name, value) def postload(self): DiagramLine.postload(self) def unlink(self): canvas = self.canvas c1 = canvas.get_connection(self.head) c2 = canvas.get_connection(self.tail) if c1 and c2: query = (c1.connected, self) adapter = component.queryMultiAdapter(query, IConnect) adapter.disconnect(self.head) super(CommentLineItem, self).unlink() def draw(self, context): context.cairo.set_dash((7.0, 5.0), 0) DiagramLine.draw(self, context) # vim: sw=4:et:ai PK!AتJJgaphor/diagram/compartment.py""" Diagram item with compartments. """ import logging import cairo from gi.repository import Pango from gi.repository import PangoCairo from gaphas.freehand import FreeHandCairoContext from gaphas.state import observed, reversible_property from gaphor import UML from gaphor.diagram.diagramitem import DiagramItem from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.textelement import text_extents, text_align log = logging.getLogger(__name__) class FeatureItem(object): """ FeatureItems are model elements who reside inside a ClassifierItem, such as methods and attributes. Those items can have comments attached, but only on the left and right side. Note that features can also be used inside objects. """ def __init__(self, pattern="%s", order=0): super(FeatureItem, self).__init__() self.width = 0 self.height = 0 self.text = "" self.font = None self.subject = None self.order = order self.pattern = pattern def save(self, save_func): DiagramItem.save(self, save_func) def postload(self): if self.subject: self.text = self.render() self.on_feature_is_static(None) def get_size(self, update=False): """ Return the size of the feature. If update == True the item is directly updated. """ return self.width, self.height def get_text(self): return "" def update_size(self, text, context): if text: cr = context.cairo self.width, self.height = text_extents(cr, text) else: self.width, self.height = 0, 0 def pre_update(self, context): self.update_size(self.render(), context) def point(self, pos): """ """ return distance_rectangle_point((0, 0, self.width, self.height), pos) def render(self): """ Return a rendered feature, as a string. """ return UML.format(self.subject, pattern=self.pattern) or "" def draw(self, context): cr = context.cairo if isinstance(cr, FreeHandCairoContext): cr = cr.cr if isinstance(cr, cairo.Context): layout = PangoCairo.create_layout(cr) layout.set_font_description(Pango.FontDescription(self.font)) layout.set_text(self.render() or "", length=-1) if hasattr(self.subject, "isStatic") and self.subject.isStatic: attrlist = Pango.AttrList.new() # TODO: How to underline text? # attrlist.insert(Pango.Attribute(Pango.Underline.SINGLE, 2, -1)) layout.set_attributes(attrlist) PangoCairo.show_layout(cr, layout) class Compartment(list): """ Compartment in a classifier or named item (i.e. class, component, state). A compartment is a list of feature items. """ def __init__(self, name, owner, id=None): self.name = name self.owner = owner self.id = id self.visible = True self.width = 0 self.height = 0 self.title = None self.font = None self.title_height = 0 self.use_extra_space = False def save(self, save_func): # log.debug('Compartment.save: %s' % self) for item in self: save_func(None, item) def has_item(self, item): """ Check if the compartment already contains an item with the same subject as item. """ s = item.subject local_elements = [f.subject for f in self] return s and s in local_elements def get_size(self): """ Get width, height of the compartment. pre_update should have been called so width and height have been calculated. """ if self.visible: return self.width, self.height else: return 0, 0 def pre_update(self, context): """ Pre update, determine width and height of the compartment. """ self.width = self.height = 0 cr = context.cairo for item in self: item.pre_update(context) if self: # self (=list) contains items sizes = [(0, 0)] # to not throw exceptions by max and sum if self.title: w, h = text_extents(cr, self.title) self.title_height = h sizes.append((w, h)) sizes.extend(f.get_size(True) for f in self) self.width = max(size[0] for size in sizes) self.height = sum(size[1] for size in sizes) vspacing = self.owner.style.compartment_vspacing self.height += vspacing * (len(sizes) - 1) padding = self.owner.style.compartment_padding self.width += padding[1] + padding[3] self.height += padding[0] + padding[2] def post_update(self, context): for item in self: item.post_update(context) def draw(self, context): cr = context.cairo padding = self.owner.style.compartment_padding vspacing = self.owner.style.compartment_vspacing cr.translate(padding[1], padding[0]) offset = 0 if self.title: text_align( cr, self.owner.width / 2.0, padding[0], self.title, font=self.font, align_y=1, ) offset += self.title_height + vspacing for item in self: cr.save() try: cr.move_to(0, offset) item.draw(context) offset += vspacing + item.height finally: cr.restore() def item_at(self, x, y): if 0 > x > self.width: return None padding = self.owner.style.compartment_padding vspacing = self.owner.style.compartment_vspacing height = padding[0] if self.title: height += self.title_height + vspacing if y < height: return None vspacing = self.owner.style.compartment_vspacing for f in self: w, h = f.get_size(True) height += h + vspacing if y < height: return f return None class CompartmentItem(NamedItem): """ Abstract class for visualization of named items and classifiers, which have compartments, i.e. classes, interfaces, components, states. Compartment item has ability to display stereotypes attributes. They are displayed in separate compartments (one per stereotype). Compartment item has three drawing styles (changed with `ClassifierItem.drawing_style` property) - the compartment view - often used by classes - a compartment view, but with a little stereotype icon in the right corner - an icon - used by actor and interface items Methods pre_update/post_update/draw are defined to support drawing styles. Appropriate methods are called depending on drawing style. """ # Do not use preset drawing style DRAW_NONE = 0 # Draw the famous box style DRAW_COMPARTMENT = 1 # Draw compartment with little icon in upper right corner DRAW_COMPARTMENT_ICON = 2 # Draw as icon DRAW_ICON = 3 __style__ = { "min-size": (100, 50), "icon-size": (20, 20), "feature-font": "sans 10", "from-padding": (7, 2, 7, 2), "compartment-padding": (5, 5, 5, 5), # (top, right, bottom, left) "compartment-vspacing": 0, "name-padding": (10, 10, 10, 10), "stereotype-padding": (10, 10, 2, 10), # extra space can be used by header or a compartment; # we don't want to consume the extra space by compartments, which # contain stereotype information "extra-space": "header", # 'header' or 'compartment' } # Default size for small icons ICON_WIDTH = 15 ICON_HEIGHT = 25 ICON_MARGIN_X = 10 ICON_MARGIN_Y = 10 def __init__(self, id=None): NamedItem.__init__(self, id) self._compartments = [] self._drawing_style = CompartmentItem.DRAW_NONE self.watch("subject.appliedStereotype", self.on_stereotype_change).watch( "subject.appliedStereotype.slot", self.on_stereotype_attr_change ).watch("subject.appliedStereotype.slot.definingFeature.name").watch( "subject.appliedStereotype.slot.value" ) self._extra_space = 0 def on_stereotype_change(self, event): if self._show_stereotypes_attrs: if isinstance(event, UML.event.AssociationAddEvent): self._create_stereotype_compartment(event.new_value) elif isinstance(event, UML.event.AssociationDeleteEvent): self._remove_stereotype_compartment(event.old_value) def _find_stereotype_compartment(self, obj): for comp in self._compartments: if comp.id is obj: return comp def on_stereotype_attr_change(self, event): if ( event and self.subject and event.element in self.subject.appliedStereotype and self._show_stereotypes_attrs ): comp = self._find_stereotype_compartment(event.element) if comp is None: log.debug("No compartment found for %s" % event.element) return if isinstance( event, (UML.event.AssociationAddEvent, UML.event.AssociationDeleteEvent) ): self._update_stereotype_compartment(comp, event.element) self.request_update() def _create_stereotype_compartment(self, obj): st = obj.classifier[0].name c = Compartment(st, self, obj) c.title = UML.model.STEREOTYPE_FMT % st c.font = self.style.feature_font self._update_stereotype_compartment(c, obj) self._compartments.append(c) self.request_update() def _remove_stereotype_compartment(self, obj): comp = self._find_stereotype_compartment(obj) if comp is not None: self._compartments.remove(comp) self.request_update() def _update_stereotype_compartment(self, comp, obj): del comp[:] for slot in obj.slot: item = FeatureItem() item.subject = slot item.font = self.style.feature_font comp.append(item) comp.visible = len(obj.slot) > 0 def update_stereotypes_attrs(self): """ Display or hide stereotypes attributes. New compartment is created for every stereotype having attributes redefined. """ # remove all stereotype compartments first for comp in self._compartments: if isinstance(comp.id, UML.InstanceSpecification): self._compartments.remove(comp) if self._show_stereotypes_attrs: for obj in self.subject.appliedStereotype: self._create_stereotype_compartment(obj) log.debug("Showing stereotypes attributes enabled") else: log.debug("Showing stereotypes attributes disabled") def save(self, save_func): # Store the show- properties *before* the width/height properties, # otherwise the classes will unintentionally grow due to "visible" # attributes or operations. self.save_property(save_func, "drawing-style") NamedItem.save(self, save_func) @observed def set_drawing_style(self, style): """ Set the drawing style for this classifier: DRAW_COMPARTMENT, DRAW_COMPARTMENT_ICON or DRAW_ICON. """ if style != self._drawing_style: self._drawing_style = style self.request_update() # if self.canvas: # request_resolve = self.canvas.solver.request_resolve # for h in self._handles: # request_resolve(h.x) # request_resolve(h.y) if self._drawing_style == self.DRAW_COMPARTMENT: self.draw = self.draw_compartment self.pre_update = self.pre_update_compartment self.post_update = self.post_update_compartment elif self._drawing_style == self.DRAW_COMPARTMENT_ICON: self.draw = self.draw_compartment_icon self.pre_update = self.pre_update_compartment_icon self.post_update = self.post_update_compartment_icon elif self._drawing_style == self.DRAW_ICON: self.draw = self.draw_icon self.pre_update = self.pre_update_icon self.post_update = self.post_update_icon drawing_style = reversible_property( lambda self: self._drawing_style, set_drawing_style ) def create_compartment(self, name): """ Create a new compartment. Compartments contain data such as attributes and operations. It is common to create compartments during the construction of the diagram item. Their visibility can be toggled by Compartment.visible. """ c = Compartment(name, self) c.font = self.style.feature_font self._compartments.append(c) return c compartments = property(lambda s: s._compartments) def sync_uml_elements(self, elements, compartment, creator=None): """ This method synchronized a list of elements with the items in a compartment. A creator-function should be passed which is used for creating new compartment items. @elements: the list of attributes or operations in the model @compartment: our local representation @creator: factory method for creating new attr. or oper.'s """ # extract the UML elements from the compartment local_elements = [f.subject for f in compartment] # map local element with compartment element mapping = dict(list(zip(local_elements, compartment))) to_add = [el for el in elements if el not in local_elements] # sync local elements with elements del compartment[:] for el in elements: if el in to_add: creator(el) else: compartment.append(mapping[el]) # log.debug('elements order in model: %s' % [f.name for f in elements]) # log.debug('elements order in diagram: %s' % [f.subject.name for f in compartment]) assert tuple([f.subject for f in compartment]) == tuple(elements) self.request_update() def pre_update_compartment_icon(self, context): self.pre_update_compartment(context) # icon width plus right margin self.min_width = max( self.min_width, self._header_size[0] + self.ICON_WIDTH + 10 ) def pre_update_icon(self, context): super(CompartmentItem, self).pre_update(context) def pre_update_compartment(self, context): """ Update state for box-style presentation. Calculate minimal size, which is based on header and compartment sizes. """ super(CompartmentItem, self).pre_update(context) for comp in self._compartments: comp.pre_update(context) sizes = [comp.get_size() for comp in self._compartments if comp.visible] sizes.append((self.min_width, self._header_size[1])) self.min_width = max(size[0] for size in sizes) h = sum(size[1] for size in sizes) self.min_height = max(self.style.min_size[1], h) def post_update_compartment_icon(self, context): """ Update state for box-style w/ small icon. """ super(CompartmentItem, self).post_update(context) def post_update_icon(self, context): """ Update state for icon-only presentation. """ super(CompartmentItem, self).post_update(context) def post_update_compartment(self, context): super(CompartmentItem, self).post_update(context) assert abs(self.width - self.min_width) >= 0, "failed %s >= %s" % ( self.width, self.min_width, ) assert abs(self.height - self.min_height) >= 0, "failed %s >= %s" % ( self.height, self.min_height, ) def get_icon_pos(self): """ Get icon position. """ return self.width - self.ICON_MARGIN_X - self.ICON_WIDTH, self.ICON_MARGIN_Y def draw_compartment_border(self, context): """ Standard classifier border is a rectangle. """ cr = context.cairo cr.rectangle(0, 0, self.width, self.height) self.fill_background(context) cr.stroke() def draw_compartment(self, context): self.draw_compartment_border(context) super(CompartmentItem, self).draw(context) cr = context.cairo # make room for name, stereotype, etc. y = self._header_size[1] cr.translate(0, y) if self._drawing_style == self.DRAW_COMPARTMENT_ICON: width = self.width - self.ICON_WIDTH else: width = self.width extra_space = self.height - self.min_height # extra space is used by header if self.style.extra_space == "header": cr.translate(0, extra_space) # draw compartments and stereotype compartments extra_used = False for comp in self._compartments: if not comp.visible: continue cr.save() cr.move_to(0, 0) cr.line_to(self.width, 0) cr.stroke() try: comp.draw(context) finally: cr.restore() d = comp.height if ( not extra_used and comp.use_extra_space and self.style.extra_space == "compartment" ): d += extra_space extra_used = True cr.translate(0, d) # if extra space is used by last compartment, then do nothing def item_at(self, x, y): """ Find the composite item (attribute or operation) for the classifier. """ if self.drawing_style not in ( self.DRAW_COMPARTMENT, self.DRAW_COMPARTMENT_ICON, ): return self header_height = self._header_size[1] compartments = [comp for comp in self.compartments if comp.visible] # Edit is in name compartment -> edit name if y < header_height or not len(compartments): return self padding = self.style.compartment_padding vspacing = self.style.compartment_vspacing # place offset at top of first comparement y -= header_height y += vspacing / 2.0 for comp in compartments: item = comp.item_at(x, y) if item: return item y -= comp.height return None # vi:ai:sw=4:et PK!!eOLLgaphor/diagram/component.py""" Component item. """ from gaphor import UML from gaphor.diagram.classifier import ClassifierItem class ComponentItem(ClassifierItem): __uml__ = UML.Component __icon__ = True __style__ = {"name-padding": (10, 25, 10, 10)} BAR_WIDTH = 10 BAR_HEIGHT = 5 BAR_PADDING = 5 def __init__(self, id=None): ClassifierItem.__init__(self, id) # Set drawing style to compartment w// small icon self.drawing_style = self.DRAW_COMPARTMENT_ICON def draw_compartment_icon(self, context): cr = context.cairo cr.save() self.draw_compartment(context) cr.restore() ix, iy = self.get_icon_pos() cr.set_line_width(1.0) cr.rectangle(ix, iy, self.ICON_WIDTH, self.ICON_HEIGHT) cr.stroke() bx = ix - self.BAR_PADDING bar_upper_y = iy + self.BAR_PADDING bar_lower_y = iy + self.BAR_PADDING * 3 color = cr.get_source() cr.rectangle(bx, bar_lower_y, self.BAR_WIDTH, self.BAR_HEIGHT) cr.set_source_rgb(1, 1, 1) # white cr.fill_preserve() cr.set_source(color) cr.stroke() cr.rectangle(bx, bar_upper_y, self.BAR_WIDTH, self.BAR_HEIGHT) cr.set_source_rgb(1, 1, 1) # white cr.fill_preserve() cr.set_source(color) cr.stroke() # vim:sw=4:et PK!%gaphor/diagram/components/__init__.pyPK!f#!&gaphor/diagram/components/subsystem.py""" Subsystem item represents a component with stereotype subsystem (see table B.1 UML Keywords in UML 2.2 specification). Subsystem item is part of components Gaphor package because it will show components, nodes and other items within cotext of a subsystem. At the moment (in the future additionally) it makes only sense to use it on use cases diagram. """ from gaphor import UML from gaphor.diagram.component import ComponentItem from gaphor.diagram.style import ALIGN_LEFT, ALIGN_TOP from gaphor.diagram import uml @uml(UML.Component, stereotype="subsystem") class SubsystemItem(ComponentItem): __style__ = {"name-align": (ALIGN_LEFT, ALIGN_TOP)} def __init__(self, id=None): super(SubsystemItem, self).__init__(id) def draw(self, context): super(SubsystemItem, self).draw(context) cr = context.cairo cr.rectangle(0, 0, self.width, self.height) cr.stroke() # vim:sw=4:et PK!+gaphor/diagram/components/tests/__init__.pyPK!a0,gaphor/diagram/components/tests/test_node.pyfrom gaphor.diagram import items from gaphor import UML from gaphor.tests import TestCase class NodeTestCase(TestCase): pass PK!jPPgaphor/diagram/connector.py""" Implementation of connector from Composite Structures and Components. Only assembly connector (see chapter Components in UML specification) is supported at the moment. The implementation is based on `ConnectorItem` class and `InterfaceItem` class in assembly connector mode. Assembly Connector ================== To connect two components with assembly connector connect folded interface and component items using connector item. If component provides or requires connected interface, then assembly connection in UML data model will be created and connector item will display name of the interface. Otherwise, UML data model is not updated and connector item does not display interface name. Interface item in assembly connector mode does not display interface name as it is displayed by connectors. Connector item visualizes two UML metaclasses - ConnectorEnd metaclass when connecting to interface item in assembly mode - Connector metaclass in other cases Using property pages of connector item, user can change superinterface of connected interface. Assembly Connector Mode of Interface Item ----------------------------------------- Assembly connector notation is supported using interface item because of its simplicity - no need for additional assembly connector item - because connection is made to specific interface, there is no need for performing a search for common interface of all connected components - separate assembly connector item would require some rotation support, instead interface item's rotation capabilities are reused Implementation Alternatives --------------------------- There were several alternatives of assembly connector notation explored. In Gaphor 0.8.x there was assembly connector item, with additional handles and lines. User was dragging a handle of an additional line to connect to a component, disadvantages - item's connection behaviour is not consistent with other items - rotation needs to be implemented For Gaphor 0.14 and later, two other ideas were considered. First one required assembly connector item as well. Connector item could visualize ConnectorEnd and Connector UML metaclasses and it would be used to connect assembly connector item and items of components. It is very consistent with the rest of Gaphor application but - it proved to be very complicated in implementation - requires additional item Second alternative was to have connector item only. It is very simple concept in first place. When connector item connects two components, then draw assembly connector icon in the middle of a line. The solution is very simple in implementation and consistent with the rest of the application until multiple components have to be connected with one assembly connector. UML Specification Issues ======================== UML specification is not clear about interfaces as connectable elements and connector's `kind` attribute. Current implementation is subject to change in the future, when UML specification clarifies issues described below. See also http://www.omg.org/issues/uml2-rtf.open.html#Issue7251 Connector Kind -------------- Chapter Components of UML specification adds `kind` attribute to connector metaclass. This is enumeration with two possible values `assembly' and `delegation'. It is not clear what value should be assigned to `kind` attribute of connector, which is defined between connectable elements like ports (not characterized by interfaces), properties and parameters. Interfaces as Connectable Elements ---------------------------------- Chapter Composite Structures in UML Superstructure 2.1.2 document does not specify interfaces as connectable elements. But definition of assembly connector says: An assembly connector is a connector between two components that defines that one component provides the services that another component requires. An assembly connector is a connector that is defined from a required _interface_ or port to a provided _interface_ or port. Therefore, code of connector items is written with assumption, that interfaces are connectable elements. """ import logging from gaphor import UML from gaphor.diagram.diagramline import NamedLine from gaphor.diagram.style import ALIGN_CENTER, ALIGN_BOTTOM from operator import attrgetter logger = logging.getLogger(__name__) class ConnectorItem(NamedLine): """ Connector item line. Represents Connector UML metaclass. If connected to interface item in assembly connector mode, then `Connector.end` attribute represents appropriate `ConnectorEnd` UML metaclass instance. :Attributes: subject Connector UML metaclass instance. end ConnectorEnd UML metaclass instance. _interface Interface name, when connector is assembly connector. """ __uml__ = UML.Connector __style__ = {"name-align": (ALIGN_CENTER, ALIGN_BOTTOM), "name-outside": True} def __init__(self, id): super(ConnectorItem, self).__init__(id) self._interface = self.add_text( "end.role.name", style={"text-align-group": "stereotype"} ) self.watch("subject.end.role.name", self.on_interface_name) def postload(self): super(ConnectorItem, self).postload() self.on_interface_name(None) def on_interface_name(self, event): """ Callback used, when interface name changes (interface is referenced by `ConnectorItem.subject.end.role`). """ try: self._interface.text = self.subject.end["it.role", 0].role.name except (IndexError, AttributeError) as e: logger.error(e) self._interface.text = "" else: self.request_update(matrix=False) def draw_tail(self, context): cr = context.cairo cr.line_to(0, 0) if self.subject and self.subject.kind == "delegation": cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) def save(self, save_func): super(ConnectorItem, self).save(save_func) # save_func('end', self.end) def load(self, name, value): if name == "end": pass # self.end = value else: super(ConnectorItem, self).load(name, value) # def on_named_element_name(self, event): # if isinstance(self.subject, UML.Connector): # super(ConnectorItem, self).on_named_element_name(event) # vim:sw=4:et:ai PK!c c gaphor/diagram/diagramitem.py""" DiagramItem provides basic functionality for presentations. Such as a modifier 'subject' property and a unique id. """ from zope import component from gaphas.state import observed, reversible_property import logging from gaphor import UML from gaphor.services.elementdispatcher import EventWatcher from gaphor.core import inject from gaphor.diagram import DiagramItemMeta from gaphor.diagram.textelement import EditableTextSupport from gaphor.diagram.style import ALIGN_CENTER, ALIGN_TOP logger = logging.getLogger(__name__) class StereotypeSupport(object): """ Support for stereotypes for every diagram item. """ STEREOTYPE_ALIGN = { "text-align": (ALIGN_CENTER, ALIGN_TOP), "text-padding": (5, 10, 2, 10), "text-outside": False, "text-align-group": "stereotype", "line-width": 2, } def __init__(self): self._stereotype = self.add_text( "stereotype", style=self.STEREOTYPE_ALIGN, visible=lambda: self._stereotype.text, ) self._show_stereotypes_attrs = False @observed def _set_show_stereotypes_attrs(self, value): self._show_stereotypes_attrs = value self.update_stereotypes_attrs() show_stereotypes_attrs = reversible_property( fget=lambda s: s._show_stereotypes_attrs, fset=_set_show_stereotypes_attrs, doc=""" Diagram item should show stereotypes attributes when property is set to True. When changed, method `update_stereotypes_attrs` is called. """, ) def update_stereotypes_attrs(self): """ Update display of stereotypes attributes. The method does nothing at the moment. In the future it should probably display stereotypes attributes under stereotypes header. Abstract class for classifiers overrides this method to display stereotypes attributes in compartments. """ pass def set_stereotype(self, text=None): """ Set the stereotype text for the diagram item. Note, that text is not Stereotype object. @arg text: stereotype text """ self._stereotype.text = text self.request_update() stereotype = property(lambda s: s._stereotype, set_stereotype) def update_stereotype(self): """ Update the stereotype definitions (text) of this item. Note, that this method is also called from ExtensionItem.confirm_connect_handle method. """ # by default no stereotype, however check for __stereotype__ # attribute to assign some static stereotype see interfaces, # use case relationships, package or class for examples stereotype = getattr(self, "__stereotype__", ()) if stereotype: stereotype = self.parse_stereotype(stereotype) # Phew! :] :P stereotype = UML.model.stereotypes_str(self.subject, stereotype) self.set_stereotype(stereotype) def parse_stereotype(self, data): if isinstance(data, str): # return data as stereotype if it is a string return (data,) subject = self.subject for stereotype, condition in list(data.items()): if isinstance(condition, tuple): cls, predicate = condition elif isinstance(condition, type): cls = condition predicate = None elif callable(condition): cls = None predicate = condition else: assert False, "wrong conditional %s" % condition ok = True if cls: ok = isinstance(subject, cls) # isinstance(subject, cls) if predicate: ok = predicate(self) if ok: return (stereotype,) return () class DiagramItem( UML.Presentation, StereotypeSupport, EditableTextSupport, metaclass=DiagramItemMeta ): """ Basic functionality for all model elements (lines and elements!). This class contains common functionality for model elements and relationships. It provides an interface similar to UML.Element for connecting and disconnecting signals. This class is not very useful on its own. It contains some glue-code for diacanvas.DiaCanvasItem and gaphor.UML.Element. Example: class ElementItem(diacanvas.CanvasElement, DiagramItem): connect = DiagramItem.connect disconnect = DiagramItem.disconnect ... @cvar style: styles information (derived from DiagramItemMeta) """ dispatcher = inject("element_dispatcher") def __init__(self, id=None): UML.Presentation.__init__(self) EditableTextSupport.__init__(self) StereotypeSupport.__init__(self) self._id = id # properties, which should be saved in file self._persistent_props = set() def update(event): self.request_update() self.watcher = EventWatcher(self, default_handler=update) self.watch("subject").watch( "subject.appliedStereotype.classifier.name", self.on_element_applied_stereotype, ) id = property(lambda self: self._id, doc="Id") def set_prop_persistent(self, name): """ Specify property of diagram item, which should be saved in file. """ self._persistent_props.add(name) # TODO: Use adapters for load/save functionality def save(self, save_func): if self.subject: save_func("subject", self.subject) save_func("show_stereotypes_attrs", self.show_stereotypes_attrs) # save persistent properties for p in self._persistent_props: save_func(p, getattr(self, p.replace("-", "_"))) def load(self, name, value): if name == "subject": type(self).subject.load(self, value) elif name == "show_stereotypes_attrs": self._show_stereotypes_attrs = eval(value) else: try: setattr(self, name.replace("-", "_"), eval(value)) except: logger.warning( "%s has no property named %s (value %s)" % (self, name, value) ) def postload(self): if self.subject: self.update_stereotype() self.update_stereotypes_attrs() def save_property(self, save_func, name): """ Save a property, this is a shorthand method. """ save_func(name, getattr(self, name.replace("-", "_"))) def save_properties(self, save_func, *names): """ Save a property, this is a shorthand method. """ for name in names: self.save_property(save_func, name) def unlink(self): """ Remove the item from the canvas and set subject to None. """ if self.canvas: self.canvas.remove(self) super(DiagramItem, self).unlink() def request_update(self): """ Placeholder for gaphor.Item's request_update() method. """ pass def pre_update(self, context): EditableTextSupport.pre_update(self, context) def post_update(self, context): EditableTextSupport.post_update(self, context) def draw(self, context): EditableTextSupport.draw(self, context) def item_at(self, x, y): return self def on_element_applied_stereotype(self, event): if self.subject: self.update_stereotype() self.request_update() def watch(self, path, handler=None): """ Watch a certain path of elements starting with the DiagramItem. The handler is optional and will default to a simple self.request_update(). Watches should be set in the constructor, so they can be registered and unregistered in one shot. This interface is fluent(returns self). """ self.watcher.watch(path, handler) return self def register_handlers(self): self.watcher.register_handlers() def unregister_handlers(self): self.watcher.unregister_handlers() PK!}  gaphor/diagram/diagramline.py""" Basic functionality for canvas line based items on a diagram. """ from math import atan2, pi import gaphas from gaphor.diagram.diagramitem import DiagramItem from gaphor.diagram.style import get_text_point_at_line from gaphor.diagram.style import get_text_point_at_line2 from gaphor.diagram.style import ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT, ALIGN_TOP class DiagramLine(gaphas.Line, DiagramItem): """ Base class for diagram lines. """ def __init__(self, id=None): gaphas.Line.__init__(self) DiagramItem.__init__(self, id) self.fuzziness = 2 head = property(lambda self: self._handles[0]) tail = property(lambda self: self._handles[-1]) def setup_canvas(self): gaphas.Line.setup_canvas(self) self.register_handlers() def teardown_canvas(self): gaphas.Line.teardown_canvas(self) self.unregister_handlers() def pre_update(self, context): # first, update stereotype to know its text self.update_stereotype() gaphas.Line.pre_update(self, context) DiagramItem.pre_update(self, context) def post_update(self, context): gaphas.Line.post_update(self, context) DiagramItem.post_update(self, context) def draw(self, context): gaphas.Line.draw(self, context) DiagramItem.draw(self, context) def point(self, pos): d1 = gaphas.Line.point(self, pos) d2 = DiagramItem.point(self, pos) return min(d1, d2) def save(self, save_func): DiagramItem.save(self, save_func) save_func("matrix", tuple(self.matrix)) for prop in ("orthogonal", "horizontal"): save_func(prop, getattr(self, prop)) points = [] for h in self.handles(): points.append(tuple(map(float, h.pos))) save_func("points", points) canvas = self.canvas c = canvas.get_connection(self.head) if c: save_func("head-connection", c.connected, reference=True) c = canvas.get_connection(self.tail) if c: save_func("tail-connection", c.connected, reference=True) def load(self, name, value): if name == "matrix": self.matrix = eval(value) elif name == "points": points = eval(value) for x in range(len(points) - 2): h = self._create_handle((0, 0)) self._handles.insert(1, h) for i, p in enumerate(points): self.handles()[i].pos = p # Update connection ports of the line. Only handles are saved # in Gaphor file therefore ports need to be recreated after # handles information is loaded. self._update_ports() elif name == "orthogonal": self._load_orthogonal = eval(value) elif name in ("head_connection", "head-connection"): self._load_head_connection = value elif name in ("tail_connection", "tail-connection"): self._load_tail_connection = value else: DiagramItem.load(self, name, value) def _get_sink(self, handle, item): """ Instant port finder. This is not the nicest place for such method. TODO: figure out if part of this functionality can be provided by the storage code. """ from gaphas.aspect import ConnectionSink hpos = self.canvas.get_matrix_i2i(self, item).transform_point(*handle.pos) port = None dist = 10e6 for p in item.ports(): pos, d = p.glue(hpos) if not port or d < dist: port = p dist = d return ConnectionSink(item, port) def _postload_connect(self, handle, item): """ Postload connect method. """ from gaphas.aspect import Connector connector = Connector(self, handle) sink = self._get_sink(handle, item) connector.connect(sink) def postload(self): if hasattr(self, "_load_orthogonal"): # Ensure there are enough handles if self._load_orthogonal and len(self._handles) < 3: p0 = self._handles[-1].pos self._handles.insert(1, self._create_handle(p0)) self.orthogonal = self._load_orthogonal del self._load_orthogonal # First update matrix and solve constraints (NE and SW handle are # lazy and are resolved by the constraint solver rather than set # directly. self.canvas.update_matrix(self) self.canvas.solver.solve() if hasattr(self, "_load_head_connection"): self._postload_connect(self.head, self._load_head_connection) del self._load_head_connection if hasattr(self, "_load_tail_connection"): self._postload_connect(self.tail, self._load_tail_connection) del self._load_tail_connection DiagramItem.postload(self) def _get_middle_segment(self): """ Get middle line segment. """ handles = self._handles m = len(handles) // 2 assert m - 1 >= 0 and m < len(handles) return handles[m - 1], handles[m] def _get_center_pos(self, inverted=False): """ Return position in the centre of middle segment of a line. Angle of the middle segment is also returned. """ h0, h1 = self._get_middle_segment() pos = (h0.pos.x + h1.pos.x) / 2, (h0.pos.y + h1.pos.y) / 2 angle = atan2(h1.pos.y - h0.pos.y, h1.pos.x - h0.pos.x) if inverted: angle += pi return pos, angle def text_align(self, extents, align, padding, outside): handles = self._handles halign, valign = align if halign == ALIGN_LEFT: p1 = handles[0].pos p2 = handles[-1].pos x, y = get_text_point_at_line(extents, p1, p2, align, padding) elif halign == ALIGN_CENTER: h0, h1 = self._get_middle_segment() p1 = h0.pos p2 = h1.pos x, y = get_text_point_at_line2(extents, p1, p2, align, padding) elif halign == ALIGN_RIGHT: p1 = handles[-1].pos p2 = handles[-2].pos x, y = get_text_point_at_line(extents, p1, p2, align, padding) return x, y class NamedLine(DiagramLine): __style__ = { "name-align": (ALIGN_CENTER, ALIGN_TOP), "name-padding": (5, 5, 5, 5), "name-outside": True, "name-align-str": None, } def __init__(self, id=None): DiagramLine.__init__(self, id) self._name = self.add_text( "name", style={ "text-align": self.style.name_align, "text-padding": self.style.name_padding, "text-outside": self.style.name_outside, "text-align-str": self.style.name_align_str, "text-align-group": "stereotype", }, editable=True, ) self.watch("subject.name", self.on_named_element_name) def postload(self): super(NamedLine, self).postload() self.on_named_element_name(None) def on_named_element_name(self, event): self._name.text = self.subject and self.subject.name or "" self.request_update() # vim:sw=4:et:ai PK!x x gaphor/diagram/elementitem.py""" Abstract classes for element-like Diagram items. """ import cairo import gaphas from gaphor.diagram.diagramitem import DiagramItem from gaphor.diagram.style import get_text_point class ElementItem(gaphas.Element, DiagramItem): __style__ = { "min-size": (0, 0), "stereotype-padding": (5, 10, 5, 10), "background": "solid", "background-color": (1, 1, 1, 0.8), "highlight-color": (0, 0, 1, 0.4), "background-gradient": ((0.8, 0.8, 0.8, 0.5), (1.0, 1.0, 1.0, 0.5)), } def __init__(self, id=None): gaphas.Element.__init__(self) DiagramItem.__init__(self, id) self.min_width = self.style.min_size[0] self.min_height = self.style.min_size[1] self.auto_resize = 0 def save(self, save_func): save_func("matrix", tuple(self.matrix)) for prop in ("width", "height"): self.save_property(save_func, prop) DiagramItem.save(self, save_func) def load(self, name, value): if name == "matrix": self.matrix = eval(value) else: DiagramItem.load(self, name, value) def setup_canvas(self): gaphas.Element.setup_canvas(self) self.register_handlers() def teardown_canvas(self): gaphas.Element.teardown_canvas(self) self.unregister_handlers() def pre_update(self, context): # super(ElementItem, self).pre_update(context) self.update_stereotype() DiagramItem.pre_update(self, context) gaphas.Element.pre_update(self, context) def point(self, pos): d1 = gaphas.Element.point(self, pos) d2 = DiagramItem.point(self, pos) return min(d1, d2) def post_update(self, context): gaphas.Element.post_update(self, context) DiagramItem.post_update(self, context) def fill_background(self, context): cr = context.cairo cr.save() try: if self.style.background == "solid": cr.set_source_rgba(*self.style.background_color) cr.fill_preserve() elif self.style.background == "gradient": # TODO: check if style is gradient g = cairo.LinearGradient(0, 0, self.width, self.height) for i, c in enumerate(self.style.background_gradient): g.add_color_stop_rgba(i, *c) cr.set_source(g) cr.fill_preserve() finally: cr.restore() def highlight(self, context): cr = context.cairo cr.save() try: if context.dropzone: cr.set_source_rgba(*self.style.highlight_color) cr.set_line_width(cr.get_line_width() * 3.141) cr.stroke_preserve() finally: cr.restore() def draw(self, context): self.fill_background(context) self.highlight(context) gaphas.Element.draw(self, context) DiagramItem.draw(self, context) def text_align(self, extents, align, padding, outside): x, y = get_text_point(extents, self.width, self.height, align, padding, outside) return x, y PK!d.Agaphor/diagram/extend.py""" Use case extension relationship. """ from gaphor import UML from gaphor.diagram.include import IncludeItem class ExtendItem(IncludeItem): """ Use case extension relationship. """ __uml__ = UML.Extend __stereotype__ = "extend" # vim:sw=4:et PK!2gaphor/diagram/extension.py""" ExtensionItem -- Graphical representation of an association. """ # TODO: for Extension.postload(): in some cases where the association ends # are connected to the same Class, the head_end property is connected to the # tail end and visa versa. from gaphor import UML from gaphor.diagram.diagramline import NamedLine class ExtensionItem(NamedLine): """ ExtensionItem represents associations. An ExtensionItem has two ExtensionEnd items. Each ExtensionEnd item represents a Property (with Property.association == my association). """ __uml__ = UML.Extension def __init__(self, id=None): NamedLine.__init__(self, id) self.watch("subject.ownedEnd") def draw_head(self, context): cr = context.cairo cr.move_to(0, 0) cr.line_to(15, -10) cr.line_to(15, 10) cr.line_to(0, 0) cr.set_source_rgb(0, 0, 0) cr.fill() cr.move_to(15, 0) PK!4&ӭgaphor/diagram/include.py""" Use case inclusion relationship. """ from gaphor import UML from gaphor.diagram.diagramline import DiagramLine class IncludeItem(DiagramLine): """ Use case inclusion relationship. """ __uml__ = UML.Include __stereotype__ = "include" def __init__(self, id=None): DiagramLine.__init__(self, id) def draw_head(self, context): cr = context.cairo cr.set_dash((), 0) cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) cr.stroke() cr.move_to(0, 0) def draw(self, context): context.cairo.set_dash((7.0, 5.0), 0) super(IncludeItem, self).draw(context) # vim:sw=4:et PK!i$gaphor/diagram/interaction.py""" Interaction diagram item. """ from gaphor import UML from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.style import ALIGN_LEFT, ALIGN_TOP class InteractionItem(NamedItem): __uml__ = UML.Interaction __style__ = {"min-size": (150, 100), "name-align": (ALIGN_TOP, ALIGN_LEFT)} def draw(self, context): cr = context.cairo cr.rectangle(0, 0, self.width, self.height) super(InteractionItem, self).draw(context) # draw pentagon w, h = self._header_size h2 = h / 2.0 cr.move_to(0, h) cr.line_to(w - 4, h) cr.line_to(w, h2) cr.line_to(w, 0) cr.stroke() # vim:sw=4:et PK!ך gaphor/diagram/interfaces.py""" This module describes the interfaces specific to the gaphor.diagram module. These interfaces are: - IConnect Use to define adapters for connecting - IEditor Text editor interface """ from zope import interface class IEditor(interface.Interface): """ Provide an interface for editing text with the TextEditTool. """ def is_editable(self, x, y): """ Is this item editable in it's current state. x, y represent the cursors (x, y) position. (this method should be called before get_text() is called. """ def get_text(self): """ Get the text to be updated """ def get_bounds(self): """ Get the bounding box of the (current) text. The edit tool is not required to do anything with this information but it might help for some nicer displaying of the text widget. Returns: a gaphas.geometry.Rectangle """ def update_text(self, text): """ Update with the new text. """ def key_pressed(self, pos, key): """ Called every time a key is pressed. Allows for 'Enter' as escape character in single line editing. """ class IConnect(interface.Interface): """ This interface is used by the HandleTool to allow connecting lines to element items. For each specific case (Element, Line) an adapter could be written. """ def connect(self, handle, port): """ Connect a line's handle to element. Note that at the moment of the connect, handle.connected_to may point to some other item. The implementor should do the disconnect of the other element themselves. """ def disconnect(self, handle): """ The true disconnect. Disconnect a handle.connected_to from an element. This requires that the relationship is also removed at model level. """ def connect_constraints(self, handle): """ Connect a handle to the element. """ def disconnect_constraints(self, handle): """ Disconnect a line's handle from an element. This is called whenever a handle is dragged. """ def glue(self, handle): """ Determine if a handle can glue to a specific element. Returns a tuple (x, y) if the line and element may connect, None otherwise. """ # TODO: I think this should have been called Namespacing or something similar, # since that's the modeling concept. class IGroup(interface.Interface): """ Provide interface for adding one UML object to another, i.e. interactions contain lifelines and components contain classes objects. """ def pre_can_contain(self): """ Determine if parent can contain item, which is instance of given class. Method called before item creation. """ def can_contain(self): """ Determine if parent can contain item. """ def group(self): """ Perform grouping of items. """ def ungroup(self): """ Perform ungrouping of items. """ # vim: sw=4:et:ai PK!L gaphor/diagram/items.py""" All Item's defined in the diagram package. This module is a shorthand for importing each module individually. """ # Base classes: from gaphor.diagram.diagramitem import DiagramItem from gaphor.diagram.diagramline import DiagramLine, NamedLine from gaphor.diagram.elementitem import ElementItem from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.compartment import CompartmentItem, FeatureItem from gaphor.diagram.classifier import ClassifierItem # General: from gaphor.diagram.comment import CommentItem from gaphor.diagram.commentline import CommentLineItem from gaphor.diagram.simpleitem import Line, Box, Ellipse # Classes: from gaphor.diagram.classes.klass import ClassItem, OperationItem from gaphor.diagram.classes.interface import InterfaceItem from gaphor.diagram.classes.package import PackageItem from gaphor.diagram.classes.association import AssociationItem from gaphor.diagram.classes.dependency import DependencyItem from gaphor.diagram.classes.generalization import GeneralizationItem from gaphor.diagram.classes.implementation import ImplementationItem # Components: from gaphor.diagram.artifact import ArtifactItem from gaphor.diagram.connector import ConnectorItem from gaphor.diagram.component import ComponentItem from gaphor.diagram.node import NodeItem from gaphor.diagram.components.subsystem import SubsystemItem # Actions: from gaphor.diagram.activitynodes import ActivityNodeItem from gaphor.diagram.activitynodes import InitialNodeItem, ActivityFinalNodeItem from gaphor.diagram.activitynodes import FlowFinalNodeItem from gaphor.diagram.activitynodes import DecisionNodeItem from gaphor.diagram.activitynodes import ForkNodeItem from gaphor.diagram.objectnode import ObjectNodeItem from gaphor.diagram.actions.action import ( ActionItem, SendSignalActionItem, AcceptEventActionItem, ) from gaphor.diagram.actions.flow import FlowItem from gaphor.diagram.actions.partition import PartitionItem # Interactions from gaphor.diagram.interaction import InteractionItem from gaphor.diagram.lifeline import LifelineItem from gaphor.diagram.message import MessageItem # States from gaphor.diagram.states import VertexItem from gaphor.diagram.states.state import StateItem from gaphor.diagram.states.transition import TransitionItem from gaphor.diagram.states.finalstate import FinalStateItem from gaphor.diagram.states.pseudostates import ( InitialPseudostateItem, HistoryPseudostateItem, ) # Use Cases: from gaphor.diagram.actor import ActorItem from gaphor.diagram.usecase import UseCaseItem from gaphor.diagram.include import IncludeItem from gaphor.diagram.extend import ExtendItem # Stereotypes: from gaphor.diagram.extension import ExtensionItem from gaphor.diagram.profiles.metaclass import MetaclassItem PK!U--gaphor/diagram/lifeline.py""" Lifeline diagram item. Implementation Details ====================== Represented Classifier ---------------------- It is not clear how to attach a connectable element to a lifeline. For now, ``Lifeline.represents`` is ``None``. Ideas: - drag and drop classifier from tree onto a lifeline - match lifeline's name with classifier's name (what about namespace?) - connect message to classifier, then classifier becomes a lifeline Destruction Event ----------------- Occurence specification is not implemented, therefore destruction event cannot be supported. Still, destruction event notation is shown at the bottom of the lifeline's lifetime when delete message is connected to a lifeline. """ from gaphas.item import SW, SE from gaphas.connector import Handle, LinePort from gaphas.solver import STRONG from gaphas.geometry import distance_line_point, Rectangle from gaphas.constraint import ( LessThanConstraint, EqualsConstraint, CenterConstraint, LineAlignConstraint, ) from gaphor import UML from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.style import ALIGN_CENTER, ALIGN_MIDDLE class LifetimePort(LinePort): def constraint(self, canvas, item, handle, glue_item): """ Create connection line constraint between item's handle and the port. """ line = canvas.project(glue_item, self.start, self.end) point = canvas.project(item, handle.pos) x, y = canvas.get_matrix_i2c(item).transform_point(*handle.pos) x, y = canvas.get_matrix_c2i(glue_item).transform_point(x, y) # keep message at the same distance from head or bottom of lifetime # line depending on situation height = self.end.y - self.start.y if y / height < 0.5: delta = y - self.start.y align = 0 else: delta = y - self.end.y align = 1 return LineAlignConstraint(line, point, align, delta) class LifetimeItem(object): """ Lifeline's lifetime object. Provides basic properties of lifeline's lifetime. :Attributes: top Top handle. bottom Bottom handle. port Lifetime connection port. visible Determines port visibility. min_length Minimum length of lifetime. length Length of lifetime. """ MIN_LENGTH = 10 MIN_LENGTH_VISIBLE = 3 * MIN_LENGTH def __init__(self): super(LifetimeItem, self).__init__() self.top = Handle(strength=STRONG - 1) self.bottom = Handle(strength=STRONG) self.top.movable = False self.top.visible = False self.port = LifetimePort(self.top.pos, self.bottom.pos) self.visible = False self._c_min_length = None # to be set by lifeline item def _set_length(self, length): """ Set lifeline's lifetime length. """ self.bottom.pos.y = self.top.pos.y + length length = property(lambda s: s.bottom.pos.y - s.top.pos.y, _set_length) def _set_min_length(self, length): assert self._c_min_length is not None self._c_min_length.delta = length min_length = property(lambda s: s._c_min_length.delta, _set_min_length) def _set_connectable(self, connectable): self.port.connectable = connectable self.bottom.movable = connectable connectable = property(lambda s: s.port.connectable, _set_connectable) def _is_visible(self): return self.length > self.MIN_LENGTH def _set_visible(self, visible): """ Set lifetime visibility. """ if visible: self.bottom.pos.y = self.top.pos.y + 3 * self.MIN_LENGTH else: self.bottom.pos.y = self.top.pos.y + self.MIN_LENGTH visible = property(_is_visible, _set_visible) class LifelineItem(NamedItem): """ Lifeline item. The item represents head of lifeline. Lifeline's lifetime is represented by `lifetime` instance. :Attributes: lifetime Lifeline's lifetime part. is_destroyed Check if delete message is connected. """ __uml__ = UML.Lifeline __style__ = {"name-align": (ALIGN_CENTER, ALIGN_MIDDLE)} def __init__(self, id=None): NamedItem.__init__(self, id) self.is_destroyed = False self.lifetime = LifetimeItem() top = self.lifetime.top bottom = self.lifetime.bottom self._handles.append(top) self._handles.append(bottom) self._ports.append(self.lifetime.port) def setup_canvas(self): super(LifelineItem, self).setup_canvas() top = self.lifetime.top bottom = self.lifetime.bottom # create constraints to: # - keep bottom handle below top handle # - keep top and bottom handle in the middle of the head c1 = CenterConstraint( self._handles[SW].pos.x, self._handles[SE].pos.x, bottom.pos.x ) c2 = EqualsConstraint(top.pos.x, bottom.pos.x, delta=0.0) c3 = EqualsConstraint(self._handles[SW].pos.y, top.pos.y, delta=0.0) self.lifetime._c_min_length = LessThanConstraint( top.pos.y, bottom.pos.y, delta=LifetimeItem.MIN_LENGTH ) self.__constraints = (c1, c2, c3, self.lifetime._c_min_length) list(map(self.canvas.solver.add_constraint, self.__constraints)) def teardown_canvas(self): super(LifelineItem, self).teardown_canvas() list(map(self.canvas.solver.remove_constraint, self.__constraints)) def save(self, save_func): super(LifelineItem, self).save(save_func) save_func("lifetime-length", self.lifetime.length) def load(self, name, value): if name == "lifetime-length": self.lifetime.bottom.pos.y = self.height + float(value) else: super(LifelineItem, self).load(name, value) def draw(self, context): """ Draw lifeline. Lifeline's head is always drawn. Lifeline's lifetime is drawn when lifetime is visible. """ super(LifelineItem, self).draw(context) cr = context.cairo cr.rectangle(0, 0, self.width, self.height) cr.stroke() if context.hovered or context.focused or self.lifetime.visible: top = self.lifetime.top bottom = self.lifetime.bottom cr = context.cairo cr.save() cr.set_dash((7.0, 5.0), 0) cr.move_to(top.pos.x, top.pos.y) cr.line_to(bottom.pos.x, bottom.pos.y) cr.stroke() cr.restore() # draw destruction event if self.is_destroyed: d1 = 8 d2 = d1 * 2 cr.move_to(bottom.pos.x - d1, bottom.pos.y - d2) cr.line_to(bottom.pos.x + d1, bottom.pos.y) cr.move_to(bottom.pos.x - d1, bottom.pos.y) cr.line_to(bottom.pos.x + d1, bottom.pos.y - d2) cr.stroke() def point(self, pos): """ Find distance to lifeline item. Distance to lifeline's head and lifeline's lifetime is calculated and minimum is returned. """ d1 = super(LifelineItem, self).point(pos) top = self.lifetime.top bottom = self.lifetime.bottom d2 = distance_line_point(top.pos, bottom.pos, pos)[0] return min(d1, d2) # vim:sw=4:et PK!#(ɠ&&gaphor/diagram/message.py""" Sequence and communication diagram messages. Messages are implemented according to UML 2.1.1 specification. Implementation Details ====================== Message sort is supported but occurence specification is not implemented. This means that model drawn on a diagram is not complete on UML datamodel level, still it is valid UML diagram (see Lifelines Diagram in UML specification, page 461). Reply Messages -------------- Different sources show that reply message has filled arrow, including UML 2.0. UML 2.1.1 specification says that reply message should be drawn with an open arrow. This is visible on examples in UML 2.0 and UML 2.1.1 specifications. Asynchronous Signal -------------------- It is not clear how to draw signals. It is usually drawn with a half-open arrow. This approach is used in Gaphor, too. Delete Message -------------- Different sources show that delete message has a "X" at the tail. It does not seem to be correct solution. A "X" should be shown at the end of lifeline's lifetime instead (see ``lifeline`` module documentation for more information). Events ------ Occurence specification is not implemented, therefore - no events implemented (i.e. destroy event) - no message sequence number on communication diagram Operations ---------- ``Lifeline.represents`` attribute is ``None``, so it is not possible to specify operation (or signal) for a message. Instead, one has to put operation information in message's name. See also ``lifeline`` module documentation. """ from math import pi from gaphas.util import path_ellipse from gaphor import UML from gaphor.diagram.diagramline import NamedLine from gaphor.misc.odict import odict from gaphor.diagram.style import ALIGN_CENTER, ALIGN_BOTTOM PI_2 = pi / 2 class MessageItem(NamedLine): """ Message item is drawn on sequence and communication diagrams. On communication diagram, message item is decorated with an arrow in the middle of a line. Attributes: - _is_communication: check if message is on communication diagram - _arrow_pos: decorating arrow position - _arrow_angle: decorating arrow angle """ __style__ = {"name-align-str": ":"} # name padding on sequence diagram SD_PADDING = NamedLine.style.name_padding # name padding on communication diagram CD_PADDING = (10, 10, 10, 10) def __init__(self, id=None): super(MessageItem, self).__init__(id) self._is_communication = False self._arrow_pos = 0, 0 self._arrow_angle = 0 self._messages = odict() self._inverted_messages = odict() def pre_update(self, context): """ Update communication diagram information. """ self._is_communication = self.is_communication() if self._is_communication: self._name.style.text_padding = self.CD_PADDING else: self._name.style.text_padding = self.SD_PADDING super(MessageItem, self).pre_update(context) def post_update(self, context): """ Update communication diagram information. """ super(MessageItem, self).post_update(context) if self._is_communication: pos, angle = self._get_center_pos() self._arrow_pos = pos self._arrow_angle = angle def save(self, save_func): save_func("message", list(self._messages), reference=True) save_func("inverted", list(self._inverted_messages), reference=True) super(MessageItem, self).save(save_func) def load(self, name, value): if name == "message": # print 'message! value =', value self.add_message(value, False) elif name == "inverted": # print 'inverted! value =', value self.add_message(value, True) else: super(MessageItem, self).load(name, value) def postload(self): for message in self._messages: self.set_message_text(message, message.name, False) for message in self._inverted_messages: self.set_message_text(message, message.name, True) super(MessageItem, self).postload() def _draw_circle(self, cr): """ Draw circle for lost/found messages. """ # method is called by draw_head or by draw_tail methods, # so draw in (0, 0)) cr.set_line_width(0.01) cr.arc(0.0, 0.0, 4, 0.0, 2 * pi) cr.fill() def _draw_arrow(self, cr, half=False, filled=True): """ Draw an arrow. Parameters: - half: draw half-open arrow - filled: draw filled arrow """ cr.move_to(15, 6) cr.line_to(0, 0) if not half: cr.line_to(15, -6) if filled: cr.close_path() cr.fill_preserve() def draw_head(self, context): cr = context.cairo # no head drawing in case of communication diagram if self._is_communication: cr.move_to(0, 0) return cr.move_to(0, 0) subject = self.subject if subject and subject.messageKind == "found": self._draw_circle(cr) cr.stroke() cr.move_to(0, 0) def draw_tail(self, context): cr = context.cairo # no tail drawing in case of communication diagram if self._is_communication: cr.line_to(0, 0) return subject = self.subject if subject and subject.messageSort in ("createMessage", "reply"): cr.set_dash((7.0, 5.0), 0) cr.line_to(0, 0) cr.stroke() cr.set_dash((), 0) if subject: w = cr.get_line_width() if subject.messageKind == "lost": self._draw_circle(cr) cr.stroke() cr.set_line_width(w) half = subject.messageSort == "asynchSignal" filled = subject.messageSort in ("synchCall", "deleteMessage") self._draw_arrow(cr, half, filled) else: self._draw_arrow(cr) cr.stroke() def _draw_decorating_arrow(self, cr, inverted=False): cr.save() try: angle = self._arrow_angle hint = -1 # rotation hint, keep arrow on the same side as message text # elements if abs(angle) >= PI_2 and angle != -PI_2: hint = 1 if inverted: angle += hint * pi x, y = self._arrow_pos # move to arrow pos and rotate, below we operate in horizontal # mode cr.translate(x, y) cr.rotate(angle) # add some padding cr.translate(0, 6 * hint) # draw decorating arrow d = 15 dr = d - 4 r = 3 cr.set_line_width(1.5) cr.move_to(-d, 0) cr.line_to(d, 0) cr.line_to(dr, r) cr.move_to(dr, -r) cr.line_to(d, 0) cr.stroke() finally: cr.restore() def draw(self, context): super(MessageItem, self).draw(context) # on communication diagram draw decorating arrows for messages and # inverted messages if self._is_communication: cr = context.cairo self._draw_decorating_arrow(cr) if len(self._inverted_messages) > 0: self._draw_decorating_arrow(cr, True) def is_communication(self): """ Check if message is connecting to lifelines on communication diagram. """ canvas = self.canvas c1 = canvas.get_connection(self.head) c2 = canvas.get_connection(self.tail) return ( c1 and not c1.connected.lifetime.visible or c2 and not c2.connected.lifetime.visible ) def add_message(self, message, inverted): """ Add message onto communication diagram. """ if inverted: messages = self._inverted_messages style = { "text-align-group": "inverted", "text-align": (ALIGN_CENTER, ALIGN_BOTTOM), } else: messages = self._messages group = "stereotype" style = {"text-align-group": "stereotype"} style["text-align-str"] = ":" style["text-padding"] = self.CD_PADDING txt = self.add_text("name", style=style) txt.text = message.name messages[message] = txt self.request_update() def remove_message(self, message, inverted): """ Remove message from communication diagram. """ if inverted: messages = self._inverted_messages else: messages = self._messages txt = messages[message] self.remove_text(txt) del messages[message] self.request_update() def set_message_text(self, message, text, inverted): """ Set text of message on communication diagram. """ if inverted: messages = self._inverted_messages else: messages = self._messages messages[message].text = text self.request_update() def swap_messages(self, m1, m2, inverted): """ Swap order of two messages on communication diagram. """ if inverted: messages = self._inverted_messages else: messages = self._messages t1 = messages[m1] t2 = messages[m2] self.swap_texts(t1, t2) messages.swap(m1, m2) self.request_update() return True # vim:sw=4:et PK!\gaphor/diagram/nameditem.py""" Base classes related to items, which represent UML classes deriving from NamedElement. """ from gaphor import UML from gaphor.UML.interfaces import IAttributeChangeEvent from gaphor.diagram.elementitem import ElementItem from gaphor.diagram.style import get_min_size, ALIGN_CENTER, ALIGN_TOP class NamedItem(ElementItem): __style__ = { "min-size": (100, 50), "from-font": "sans 8", "name-font": "sans 10", "name-align": (ALIGN_CENTER, ALIGN_TOP), "name-padding": (5, 10, 5, 10), "name-outside": False, "name-align-str": None, "name-rotated": False, } def __init__(self, id=None): """ Create named item. """ ElementItem.__init__(self, id) # create (from ...) text to distinguish diagram items from # different namespace self._from = self.add_text( "from", pattern="(from %s)", style={"text-align-group": "stereotype", "font": self.style.from_font}, visible=self.is_namespace_info_visible, ) self._name = self.add_text( "name", style={ "font": self.style.name_font, "text-align": self.style.name_align, "text-padding": self.style.name_padding, "text-outside": self.style.name_outside, "text-rotated": self.style.name_rotated, "text-align-str": self.style.name_align_str, "text-align-group": "stereotype", }, editable=True, ) # size of stereotype, namespace and name text self._header_size = 0, 0 self.watch("subject.name", self.on_named_element_name).watch( "subject.namespace", self.on_named_element_namespace ) def postload(self): self.on_named_element_name(None) self.on_named_element_namespace(None) super(NamedItem, self).postload() def is_namespace_info_visible(self): """ Display name space info when it is different, then diagram's or parent's namespace. """ subject = self.subject canvas = self.canvas if not subject or not canvas: return False if not self._name.is_visible(): return False namespace = subject.namespace parent = canvas.get_parent(self) # if there is a parent (i.e. interaction) if parent and parent.subject and parent.subject.namespace is not namespace: return False return self._from.text and namespace is not canvas.diagram.namespace def on_named_element_name(self, event): """ Callback to be invoked, when named element name is changed. """ if self.subject: self._name.text = self.subject.name self.request_update() def on_named_element_namespace(self, event): """ Add a line '(from ...)' to the class item if subject's namespace is not the same as the namespace of this diagram. """ subject = self.subject if subject and subject.namespace: self._from.text = subject.namespace.name else: self._from.text = "" self.request_update() def pre_update(self, context): """ Calculate minimal size and header size. """ super(NamedItem, self).pre_update(context) style = self._name.style # we can determine minimal size and header size only # when name is aligned inside an item if not style.text_outside: # at this stage stereotype text group should be already updated assert "stereotype" in self._text_groups_sizes nw, nh = self._text_groups_sizes["stereotype"] self._header_size = get_min_size(nw, nh, self.style.name_padding) self.min_width = max(self.style.min_size[0], self._header_size[0]) self.min_height = max(self.style.min_size[1], self._header_size[1]) # vim:sw=4:et:ai PK!"^^gaphor/diagram/node.py""" Node item may represent a node or a device UML metamodel classes. Grouping ======== Node item can group following items - other nodes, which are represented with Node.nestedNode on UML metamodel level - deployed artifacts using deployment - components, which are parts of a node acting as structured classifier (nodes may have internal structures) Node item grouping logic is implemented in `gaphor.adapters.grouping` module. """ from gaphor import UML from gaphor.diagram.classifier import ClassifierItem class NodeItem(ClassifierItem): """ Representation of node or device from UML Deployment package. """ __uml__ = UML.Node, UML.Device __stereotype__ = {"device": UML.Device} DEPTH = 10 def __init__(self, id=None): ClassifierItem.__init__(self, id) self.drawing_style = self.DRAW_COMPARTMENT self.height = 50 self.width = 120 def draw_compartment(self, context): cr = context.cairo cr.save() super(NodeItem, self).draw_compartment(context) cr.restore() d = self.DEPTH w = self.width h = self.height cr.move_to(0, 0) cr.line_to(d, -d) cr.line_to(w + d, -d) cr.line_to(w + d, h - d) cr.line_to(w, h) cr.move_to(w, 0) cr.line_to(w + d, -d) cr.stroke() # vim:sw=4:et PK!kdgaphor/diagram/objectnode.py""" Object node item. """ import itertools from gaphas.state import observed, reversible_property from gaphor import UML from gaphor.core import inject from gaphor.diagram.nameditem import NamedItem from gaphor.diagram.style import ALIGN_CENTER, ALIGN_BOTTOM DEFAULT_UPPER_BOUND = "*" class ObjectNodeItem(NamedItem): """ Representation of object node. Object node is ordered and has upper bound specification. Ordering information can be hidden by user. """ element_factory = inject("element_factory") __uml__ = UML.ObjectNode STYLE_BOTTOM = { "text-align": (ALIGN_CENTER, ALIGN_BOTTOM), "text-outside": True, "text-align-group": "bottom", } def __init__(self, id=None): NamedItem.__init__(self, id) self._show_ordering = False self._upper_bound = self.add_text( "upperBound", pattern="{ upperBound = %s }", style=self.STYLE_BOTTOM, visible=self.is_upper_bound_visible, ) self._ordering = self.add_text( "ordering", pattern="{ ordering = %s }", style=self.STYLE_BOTTOM, visible=self._get_show_ordering, ) self.watch( "subject.upperBound", self.on_object_node_upper_bound ).watch("subject.ordering", self.on_object_node_ordering) def on_object_node_ordering(self, event): if self.subject: self._ordering.text = self.subject.ordering self.request_update() def on_object_node_upper_bound(self, event): subject = self.subject if subject and subject.upperBound: self._upper_bound.text = subject.upperBound self.request_update() def is_upper_bound_visible(self): """ Do not show upper bound, when it's set to default value. """ subject = self.subject return subject and subject.upperBound != DEFAULT_UPPER_BOUND @observed def _set_show_ordering(self, value): self._show_ordering = value self.request_update() def _get_show_ordering(self): return self._show_ordering show_ordering = reversible_property(_get_show_ordering, _set_show_ordering) def save(self, save_func): save_func("show-ordering", self._show_ordering) super(ObjectNodeItem, self).save(save_func) def load(self, name, value): if name == "show-ordering": self._show_ordering = eval(value) else: super(ObjectNodeItem, self).load(name, value) def postload(self): if self.subject and self.subject.upperBound: self._upper_bound.text = self.subject.upperBound if self.subject and self._show_ordering: self.set_ordering(self.subject.ordering) super(ObjectNodeItem, self).postload() def draw(self, context): cr = context.cairo cr.rectangle(0, 0, self.width, self.height) cr.stroke() super(ObjectNodeItem, self).draw(context) def set_upper_bound(self, value): """ Set upper bound value of object node. """ subject = self.subject if subject: if not value: value = DEFAULT_UPPER_BOUND subject.upperBound = value # self._upper_bound.text = value def set_ordering(self, value): """ Set object node ordering value. """ subject = self.subject subject.ordering = value self._ordering.text = value # vim:sw=4:et:ai PK!#gaphor/diagram/profiles/__init__.pyPK!>e$gaphor/diagram/profiles/metaclass.py""" Metaclass item for Metaclass UML metaclass :) from profiles. """ from gaphor.diagram.classes.klass import ClassItem from gaphor.diagram import uml from gaphor import UML @uml(UML.Component, stereotype="metaclass") class MetaclassItem(ClassItem): pass PK!kgaphor/diagram/simpleitem.py""" Trivial drawing aids (box, line, ellipse). """ from gaphas.item import Element, NW from gaphas.item import Line as _Line from gaphas.util import path_ellipse from gaphor.diagram.style import Style class Line(_Line): __style__ = {"line-width": 2, "line-color": (0, 0, 0, 1)} def __init__(self, id=None): super(Line, self).__init__() self.style = Style(Line.__style__) self._id = id self.fuzziness = 2 self._handles[0].connectable = False self._handles[-1].connectable = False id = property(lambda self: self._id, doc="Id") def save(self, save_func): save_func("matrix", tuple(self.matrix)) for prop in ("orthogonal", "horizontal"): save_func(prop, getattr(self, prop)) points = [] for h in self.handles(): points.append(tuple(map(float, h.pos))) save_func("points", points) def load(self, name, value): if name == "matrix": self.matrix = eval(value) elif name == "points": points = eval(value) for x in range(len(points) - 2): h = self._create_handle((0, 0)) self._handles.insert(1, h) for i, p in enumerate(points): self.handles()[i].pos = p self._update_ports() elif name == "horizontal": self.horizontal = eval(value) elif name == "orthogonal": self._load_orthogonal = eval(value) def postload(self): if hasattr(self, "_load_orthogonal"): self.orthogonal = self._load_orthogonal del self._load_orthogonal def draw(self, context): cr = context.cairo style = self.style cr.set_line_width(style.line_width) cr.set_source_rgba(*style.line_color) super(Line, self).draw(context) class Box(Element): """ A Box has 4 handles (for a start):: NW +---+ NE SW +---+ SE """ __style__ = { "border-width": 2, "border-color": (0, 0, 0, 1), "fill-color": (1, 1, 1, 0), } def __init__(self, id=None): super(Box, self).__init__(10, 10) self.style = Style(Box.__style__) self._id = id id = property(lambda self: self._id, doc="Id") def save(self, save_func): save_func("matrix", tuple(self.matrix)) save_func("width", self.width) save_func("height", self.height) def load(self, name, value): if name == "matrix": self.matrix = eval(value) elif name == "width": self.width = eval(value) elif name == "height": self.height = eval(value) def postload(self): pass def draw(self, context): cr = context.cairo nw = self._handles[NW] style = self.style cr.rectangle(nw.pos.x, nw.pos.y, self.width, self.height) cr.set_source_rgba(*style.fill_color) cr.fill_preserve() cr.set_source_rgba(*style.border_color) cr.set_line_width(style.border_width) cr.stroke() class Ellipse(Element): """ """ __style__ = { "border-width": 2, "border-color": (0, 0, 0, 1), "fill-color": (1, 1, 1, 0), } def __init__(self, id=None): super(Ellipse, self).__init__() self.style = Style(Ellipse.__style__) self._id = id id = property(lambda self: self._id, doc="Id") def save(self, save_func): save_func("matrix", tuple(self.matrix)) save_func("width", self.width) save_func("height", self.height) def load(self, name, value): if name == "matrix": self.matrix = eval(value) elif name == "width": self.width = eval(value) elif name == "height": self.height = eval(value) def postload(self): pass def draw(self, context): cr = context.cairo nw = self._handles[NW] style = self.style rx = self.width / 2.0 ry = self.height / 2.0 cr.move_to(self.width, ry) path_ellipse(cr, rx, ry, self.width, self.height) cr.set_source_rgba(*style.fill_color) cr.fill_preserve() cr.set_source_rgba(*style.border_color) cr.set_line_width(style.border_width) cr.stroke() # vim:sw=4:et:ai PK!ozNN!gaphor/diagram/states/__init__.py""" Package gaphor.diagram.states implements diagram items for UML state machines. Pseudostates ============ There are some similarities between activities and state machines, for example - initial node and initial psuedostate - final node and final state Of course, they differ in many aspects, but the similarities drive user interface of state machines. This is with respect of minimization of the set of diagram items (i.e. there is only one diagram item for both join and fork nodes in activities implemented in Gaphor). There are separate diagram items for pseudostates - initial pseudostate item as there exists initial node item @todo: Probably, history pseudostates will be implemented as one diagram item with an option deep/shallow. [This section is going to be extended as we start to implement more pseudostates]. """ from gaphor.diagram.nameditem import NamedItem class VertexItem(NamedItem): """ Abstract class for all vertices. All state, pseudostate items derive from VertexItem, which simplifies transition connection adapters. """ pass PK!;>>#gaphor/diagram/states/finalstate.py""" Final state diagram item. """ from gaphor import UML from gaphor.diagram.style import ALIGN_RIGHT, ALIGN_BOTTOM from gaphas.util import path_ellipse from gaphor.diagram.states import VertexItem class FinalStateItem(VertexItem): __uml__ = UML.FinalState __style__ = { "min-size": (30, 30), "name-align": (ALIGN_RIGHT, ALIGN_BOTTOM), "name-padding": (2, 2, 2, 2), "name-outside": True, } RADIUS_1 = 10 RADIUS_2 = 15 def __init__(self, id=None): super(FinalStateItem, self).__init__(id) for h in self.handles(): h.movable = False def draw(self, context): """ Draw final state symbol. """ cr = context.cairo r = self.RADIUS_2 + 1 d = self.RADIUS_1 * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.fill() d = r * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.set_line_width(2) cr.stroke() super(FinalStateItem, self).draw(context) # vim:sw=4:et PK! v`%gaphor/diagram/states/pseudostates.py""" Pseudostate diagram items. See also gaphor.diagram.states package description. """ from gaphor import UML from gaphor.diagram.style import ALIGN_LEFT, ALIGN_TOP from gaphas.util import path_ellipse from gaphor.diagram.textelement import text_center from gaphor.diagram.states import VertexItem class InitialPseudostateItem(VertexItem): """ Initial pseudostate diagram item. """ __uml__ = UML.Pseudostate __style__ = { "min-size": (20, 20), "name-align": (ALIGN_LEFT, ALIGN_TOP), "name-padding": (2, 2, 2, 2), "name-outside": True, } RADIUS = 10 def __init__(self, id=None): super(InitialPseudostateItem, self).__init__(id) for h in self.handles(): h.movable = False def draw(self, context): """ Draw intial pseudostate symbol. """ super(InitialPseudostateItem, self).draw(context) cr = context.cairo r = self.RADIUS d = r * 2 path_ellipse(cr, r, r, d, d) cr.set_line_width(0.01) cr.fill() class HistoryPseudostateItem(VertexItem): """ History pseudostate diagram item. """ __uml__ = UML.Pseudostate __style__ = { "min-size": (30, 30), "name-align": (ALIGN_LEFT, ALIGN_TOP), "name-padding": (2, 2, 2, 2), "name-outside": True, } RADIUS = 15 def __init__(self, id=None): super(HistoryPseudostateItem, self).__init__(id) for h in self.handles(): h.movable = False def draw(self, context): """ Draw intial pseudostate symbol. """ super(HistoryPseudostateItem, self).draw(context) cr = context.cairo r = self.RADIUS d = r * 2 path_ellipse(cr, r, r, d, d) # cr.set_line_width(1) cr.stroke() text_center(cr, r, r, "H", self.style.name_font) # vim:sw=4:et PK! | gaphor/diagram/states/state.py""" State diagram item. """ import operator from gaphor import UML from gaphor.diagram.style import ALIGN_LEFT, ALIGN_CENTER, ALIGN_TOP from gaphor.diagram.states import VertexItem from gaphor.diagram.classifier import CompartmentItem from gaphor.diagram.compartment import FeatureItem from gaphor.core import inject DX = 15 DY = 8 DDX = 0.4 * DX DDY = 0.4 * DY class StateItem(CompartmentItem, VertexItem): element_factory = inject("element_factory") __uml__ = UML.State __style__ = { "min-size": (50, 30), "name-align": (ALIGN_CENTER, ALIGN_TOP), "extra-space": "compartment", } def __init__(self, id): super(StateItem, self).__init__(id) self.drawing_style = self.DRAW_COMPARTMENT self._activities = self.create_compartment("activities") self._activities.use_extra_space = True # non-visible by default, show when at least one item is visible self._activities.visible = False self._entry = FeatureItem(pattern="entry / %s", order=1) self._exit = FeatureItem(pattern="exit / %s", order=2) self._do_activity = FeatureItem(pattern="do / %s", order=3) def _set_activity(self, act, attr, text): if text and act not in self._activities: self._activities.append(act) act.subject = self.element_factory.create(UML.Activity) act.subject.name = text setattr(self.subject, attr, act.subject) # sort the activities according to defined order self._activities.sort(key=operator.attrgetter("order")) elif text and act in self._activities: act.subject.name = text elif not text and act in self._activities: self._activities.remove(act) act.subject.unlink() self._activities.visible = len(self._activities) > 0 self.request_update() def set_entry(self, text): self._set_activity(self._entry, "entry", text) def set_exit(self, text): self._set_activity(self._exit, "exit", text) def set_do_activity(self, text): self._set_activity(self._do_activity, "doActivity", text) def postload(self): super(StateItem, self).postload() if self.subject.entry: self.set_entry(self.subject.entry.name) if self.subject.exit: self.set_exit(self.subject.exit.name) if self.subject.doActivity: self.set_do_activity(self.subject.doActivity.name) def draw_compartment_border(self, context): """ Draw state item. """ c = context.cairo c.move_to(0, DY) c.curve_to(0, DDY, DDX, 0, DX, 0) c.line_to(self.width - DX, 0) c.curve_to(self.width - DDX, 0, self.width, DDY, self.width, DY) c.line_to(self.width, self.height - DY) c.curve_to( self.width, self.height - DDY, self.width - DDX, self.height, self.width - DX, self.height, ) c.line_to(DX, self.height) c.curve_to(DDX, self.height, 0, self.height - DDY, 0, self.height - DY) c.close_path() c.stroke() # vim:sw=4:et PK!'gaphor/diagram/states/tests/__init__.pyPK!Ȁ]]0gaphor/diagram/states/tests/test_pseudostates.py""" Test pseudostates. """ from gaphor import UML from gaphor.diagram.states.pseudostates import ( InitialPseudostateItem, HistoryPseudostateItem, ) from gaphor.tests.testcase import TestCase class InitialPseudostate(TestCase): """ Initial pseudostate item test cases. """ def test_initial_pseudostate(self): """Test creation of initial pseudostate """ item = self.create(InitialPseudostateItem, UML.Pseudostate) self.assertEqual("initial", item.subject.kind) def test_history_pseudostate(self): """Test creation of initial pseudostate """ item = self.create(HistoryPseudostateItem, UML.Pseudostate) # history setting is done in the DiagramToolbox factory: item.subject.kind = "shallowHistory" self.assertEqual("shallowHistory", item.subject.kind) PK!AA*gaphor/diagram/states/tests/test_states.py""" Test state items. """ from gaphor import UML from gaphor.diagram.states.state import StateItem from gaphor.tests.testcase import TestCase class StateTestCase(TestCase): def test_state(self): """Test creation of states """ self.create(StateItem, UML.State) def test_activities_persistence(self): """Test state activities saving/loading """ # all activities s1 = self.create(StateItem, UML.State) s1.subject.name = "s1" s1.set_entry("test 1 entry") s1.set_exit("test 1 exit") s1.set_do_activity("test 1 do") # not all activities s2 = self.create(StateItem, UML.State) s2.subject.name = "s2" s2.set_entry("test 2 entry") s2.set_do_activity("test 2 do") data = self.save() self.load(data) states = self.diagram.canvas.select(lambda e: isinstance(e, StateItem)) self.assertEqual(2, len(states)) s1, s2 = states if s1.subject.name == "s2": s1, s2 = s2, s1 self.assertEqual("test 1 entry", s1.subject.entry.name) self.assertEqual("test 1 exit", s1.subject.exit.name) self.assertEqual("test 1 do", s1.subject.doActivity.name) self.assertEqual(3, len(s1._activities)) self.assertTrue(s1._entry in s1._activities) self.assertTrue(s1._exit in s1._activities) self.assertTrue(s1._do_activity in s1._activities) self.assertEqual("test 2 entry", s2.subject.entry.name) self.assertTrue(s2.subject.exit is None) self.assertEqual("test 2 do", s2.subject.doActivity.name) self.assertEqual(2, len(s2._activities)) self.assertTrue(s2._entry in s2._activities) self.assertFalse(s2._exit in s2._activities) self.assertTrue(s2._do_activity in s2._activities) PK!ff.gaphor/diagram/states/tests/test_transition.py""" Test transitions. """ from gaphor import UML from gaphor.diagram.states.transition import TransitionItem from gaphor.tests.testcase import TestCase class TransitionTestCase(TestCase): """ Test the working of transitions """ def test_transition_guard(self): """Test events of transition.guard. """ item = self.create(TransitionItem, UML.Transition) assert item._guard.text == "" c = self.element_factory.create(UML.Constraint) c.specification = "blah" assert item._guard.text == "" item.subject.guard = c assert item.subject.guard is c assert item._guard.text == "blah", item._guard.text del c.specification assert item._guard.text == "", item._guard.text c.specification = "foo" assert item._guard.text == "foo", item._guard.text PK!)7|oo#gaphor/diagram/states/transition.py""" State transition implementation. """ from gaphor import UML from gaphor.core import inject from gaphor.diagram.diagramline import NamedLine from gaphor.diagram.style import ALIGN_LEFT, ALIGN_RIGHT, ALIGN_TOP class TransitionItem(NamedLine): """ Representation of state transition. """ __uml__ = UML.Transition __style__ = {"name-align": (ALIGN_RIGHT, ALIGN_TOP), "name-padding": (5, 15, 5, 5)} element_factory = inject("element_factory") def __init__(self, id=None): NamedLine.__init__(self, id) self._guard = self.add_text("guard.specification", editable=True) self.watch("subject.guard.specification", self.on_guard) def postload(self): """ Load guard specification information. """ try: self._guard.text = self.subject.guard.specification or "" except AttributeError: self._guard.text = "" super(TransitionItem, self).postload() def on_guard(self, event): try: self._guard.text = self.subject.guard.specification or "" except AttributeError: self._guard.text = "" self.request_update() def draw_tail(self, context): cr = context.cairo cr.line_to(0, 0) cr.stroke() cr.move_to(15, -6) cr.line_to(0, 0) cr.line_to(15, 6) PK!0 ##gaphor/diagram/style.py""" Style classes and constants. """ # padding PADDING_TOP, PADDING_RIGHT, PADDING_BOTTOM, PADDING_LEFT = list(range(4)) # horizontal align ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT = -1, 0, 1 # vertical align ALIGN_TOP, ALIGN_MIDDLE, ALIGN_BOTTOM = -1, 0, 1 # hint tuples to move text depending on quadrant WIDTH_HINT = (0, 0, -1) # width hint tuple R_WIDTH_HINT = (-1, -1, 0) # width hint tuple PADDING_HINT = (1, 1, -1) # padding hint tuple EPSILON = 1e-6 class Style(object): """ Item style information. Style information is provided through object's attributes, i.e.:: >>> from gaphor.diagram import DiagramItemMeta >>> class InitialNodeItem(object, metaclass=DiagramItemMeta): ... __style__ = { ... 'name-align': ('center', 'top'), ... } is translated to:: >>> print(InitialNodeItem().style.name_align) ('center', 'top') """ def __init__(self, *args, **kwargs): super(Style, self).__init__() for d in args: self.update(d) if kwargs: self.update(kwargs) def add(self, name, value): """ Add style variable. Variable name can contain hyphens, which is converted to underscode, i.e. 'name-align' -> 'name_align'. @param name: style variable name @param value: style variable value """ name = name.replace("-", "_") setattr(self, name, value) def update(self, style): for name, value in list(style.items()): self.add(name, value) def items(self): """ Return iterator of (name, value) style information items. """ return iter(self.__dict__.items()) def get_min_size(width, height, padding): """ Get minimal size of an object using padding information. @param width: object width @param height: object height @param padding: padding information as a tuple (top, right, bottom, left) """ width += padding[PADDING_LEFT] + padding[PADDING_RIGHT] height += padding[PADDING_TOP] + padding[PADDING_BOTTOM] return width, height def get_text_point(extents, width, height, align, padding, outside): """ Calculate position of the text relative to containing box defined by tuple (0, 0, width, height). Text is aligned using align and padding information. It can be also placed outside the box if ``outside'' parameter is set to ``True''. Parameters: - extents: text extents like width, height, etc. - width: width of the containing box - height: height of the containing box - align: text align information (center, top, etc.) - padding: text padding - outside: should text be put outside containing box """ # x_bear, y_bear, w, h, x_adv, y_adv = extents w, h = extents halign, valign = align if outside: if halign == ALIGN_LEFT: x = -w - padding[PADDING_LEFT] elif halign == ALIGN_CENTER: x = (width - w) / 2 elif halign == ALIGN_RIGHT: x = width + padding[PADDING_RIGHT] else: assert False if valign == ALIGN_TOP: y = -h - padding[PADDING_TOP] elif valign == ALIGN_MIDDLE: y = (height - h) / 2 elif valign == ALIGN_BOTTOM: y = height + padding[PADDING_BOTTOM] else: assert False else: if halign == ALIGN_LEFT: x = padding[PADDING_LEFT] elif halign == ALIGN_CENTER: x = (width - w) / 2 + padding[PADDING_LEFT] - padding[PADDING_RIGHT] elif halign == ALIGN_RIGHT: x = width - w - padding[PADDING_RIGHT] else: assert False if valign == ALIGN_TOP: y = padding[PADDING_TOP] elif valign == ALIGN_MIDDLE: y = (height - h) / 2 elif valign == ALIGN_BOTTOM: y = height - h - padding[PADDING_BOTTOM] else: assert False return x, y def get_text_point_at_line(extents, p1, p2, align, padding): """ Calculate position of the text relative to a line defined by points (p1, p2). Text is aligned using align and padding information. Parameters: - extents: text extents like width, height, etc. - p1: beginning of line - p2: end of line - align: text align information (center, top, etc.) - padding: text padding """ name_dx = 0.0 name_dy = 0.0 ofs = 5 dx = float(p2[0]) - float(p1[0]) dy = float(p2[1]) - float(p1[1]) name_w, name_h = extents if dy == 0: rc = 1000.0 # quite a lot... else: rc = dx / dy abs_rc = abs(rc) h = dx > 0 # right side of the box v = dy > 0 # bottom side if abs_rc > 6: # horizontal line if h: name_dx = ofs name_dy = -ofs - name_h else: name_dx = -ofs - name_w name_dy = -ofs - name_h elif 0 <= abs_rc <= 0.2: # vertical line if v: name_dx = -ofs - name_w name_dy = ofs else: name_dx = -ofs - name_w name_dy = -ofs - name_h else: # Should both items be placed on the same side of the line? r = abs_rc < 1.0 # Find out alignment of text (depends on the direction of the line) align_left = (h and not r) or (r and not h) align_bottom = (v and not r) or (r and not v) if align_left: name_dx = ofs else: name_dx = -ofs - name_w if align_bottom: name_dy = -ofs - name_h else: name_dy = ofs return p1[0] + name_dx, p1[1] + name_dy def get_text_point_at_line2(extents, p1, p2, align, padding): """ Calculate position of the text relative to a line defined by points (p1, p2). Text is aligned using align and padding information. TODO: merge with get_text_point_at_line function Parameters: - extents: text extents like width, height, etc. - p1: beginning of line - p2: end of line - align: text align information (center, top, etc.) - padding: text padding """ x0 = (p1[0] + p2[0]) / 2.0 y0 = (p1[1] + p2[1]) / 2.0 dx = p2[0] - p1[0] dy = p2[1] - p1[1] if abs(dx) < EPSILON: d1 = -1.0 d2 = 1.0 elif abs(dy) < EPSILON: d1 = 0.0 d2 = 0.0 else: d1 = dy / dx d2 = abs(d1) width, height = extents halign, valign = align # move to center and move by delta depending on line angle if d2 < 0.5774: # <0, 30>, <150, 180>, <-180, -150>, <-30, 0> # horizontal mode w2 = width / 2.0 hint = w2 * d2 x = x0 - w2 if valign == ALIGN_TOP: y = y0 - height - padding[PADDING_BOTTOM] - hint else: y = y0 + padding[PADDING_TOP] + hint else: # much better in case of vertical lines # determine quadrant, we are interested in 1 or 3 and 2 or 4 # see hint tuples below h2 = height / 2.0 q = (d1 > 0) - (d1 < 0) if abs(dx) < EPSILON: hint = 0 else: hint = h2 / d2 if valign == ALIGN_TOP: x = ( x0 + PADDING_HINT[q] * (padding[PADDING_LEFT] + hint) + width * WIDTH_HINT[q] ) else: x = ( x0 - PADDING_HINT[q] * (padding[PADDING_RIGHT] + hint) + width * R_WIDTH_HINT[q] ) y = y0 - h2 return x, y # vim:sw=4:et PK! gaphor/diagram/tests/__init__.pyPK!*gaphor/diagram/tests/test_activitynodes.pyimport gaphor.UML as UML from gaphor.diagram import items from gaphor.tests.testcase import TestCase class ActivityNodesTestCase(TestCase): def test_decision_node(self): """Test creation of decision node """ self.create(items.DecisionNodeItem, UML.DecisionNode) def test_fork_node(self): """Test creation of fork node """ self.create(items.ForkNodeItem, UML.ForkNode) def test_decision_node_persistence(self): """Test saving/loading of decision node """ factory = self.element_factory item = self.create(items.DecisionNodeItem, UML.DecisionNode) data = self.save() self.load(data) item = self.diagram.canvas.select( lambda e: isinstance(e, items.DecisionNodeItem) )[0] self.assertTrue(item.combined is None, item.combined) merge_node = factory.create(UML.MergeNode) item.combined = merge_node data = self.save() self.load(data) item = self.diagram.canvas.select( lambda e: isinstance(e, items.DecisionNodeItem) )[0] self.assertTrue(item.combined is not None, item.combined) self.assertTrue(isinstance(item.combined, UML.MergeNode)) def test_fork_node_persistence(self): """Test saving/loading of fork node """ factory = self.element_factory item = self.create(items.ForkNodeItem, UML.ForkNode) data = self.save() self.load(data) item = self.diagram.canvas.select(lambda e: isinstance(e, items.ForkNodeItem))[ 0 ] self.assertTrue(item.combined is None, item.combined) merge_node = factory.create(UML.JoinNode) item.combined = merge_node data = self.save() self.load(data) item = self.diagram.canvas.select(lambda e: isinstance(e, items.ForkNodeItem))[ 0 ] self.assertTrue(item.combined is not None, item.combined) self.assertTrue(isinstance(item.combined, UML.JoinNode)) PK!zVd!d!3gaphor/diagram/tests/test_classifier_stereotypes.py""" Test classifier stereotypes attributes using component items. """ from gaphor import UML from gaphor.diagram.component import ComponentItem from gaphor.tests import TestCase class StereotypesAttributesTestCase(TestCase): def setUp(self): """ Create two stereotypes and extend component UML metaclass using them. """ super(StereotypesAttributesTestCase, self).setUp() factory = self.element_factory cls = factory.create(UML.Class) cls.name = "Component" st1 = self.st1 = factory.create(UML.Stereotype) st1.name = "st1" st2 = self.st2 = factory.create(UML.Stereotype) st2.name = "st2" attr = factory.create(UML.Property) attr.name = "st1_attr_1" st1.ownedAttribute = attr attr = factory.create(UML.Property) attr.name = "st1_attr_2" st1.ownedAttribute = attr attr = factory.create(UML.Property) attr.name = "st2_attr_1" st2.ownedAttribute = attr self.ext1 = UML.model.extend_with_stereotype(factory, cls, st1) self.ext2 = UML.model.extend_with_stereotype(factory, cls, st2) def tearDown(self): del self.st1 del self.st2 def test_applying_stereotype(self): """Test if stereotype compartment is created when stereotype is applied """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) # test precondition assert len(c._compartments) == 0 c.show_stereotypes_attrs = True UML.model.apply_stereotype(factory, c.subject, self.st1) self.assertEqual(1, len(c._compartments)) self.assertFalse(c._compartments[0].visible) def test_adding_slot(self): """Test if stereotype attribute information is added when slot is added """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True obj = UML.model.apply_stereotype(factory, c.subject, self.st1) # test precondition assert not c._compartments[0].visible slot = UML.model.add_slot(factory, obj, self.st1.ownedAttribute[0]) compartment = c._compartments[0] self.assertTrue(compartment.visible) self.assertEqual(1, len(compartment)) def test_removing_last_slot(self): """Test removing last slot """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True obj = UML.model.apply_stereotype(factory, c.subject, self.st1) slot = UML.model.add_slot(factory, obj, self.st1.ownedAttribute[0]) compartment = c._compartments[0] # test precondition assert compartment.visible del obj.slot[slot] self.assertFalse(compartment.visible) def test_removing_stereotype(self): """Test if stereotype compartment is destroyed when stereotype is removed """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True UML.model.apply_stereotype(factory, c.subject, self.st1) # test precondition assert len(c._compartments) == 1 UML.model.remove_stereotype(c.subject, self.st1) self.assertEqual(0, len(c._compartments)) def test_deleting_extension(self): """Test if stereotype is removed when extension is deleteded """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True st1 = self.st1 ext1 = self.ext1 UML.model.apply_stereotype(factory, c.subject, st1) # test precondition assert len(c._compartments) == 1 assert len(c.subject.appliedStereotype) == 1 ext1.unlink() self.assertEqual(0, len(c.subject.appliedStereotype)) self.assertEqual(0, len(c._compartments)) def test_deleting_stereotype(self): """Test if stereotype is removed when stereotype is deleteded """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True st1 = self.st1 UML.model.apply_stereotype(factory, c.subject, st1) # test precondition assert len(c._compartments) == 1 assert len(c.subject.appliedStereotype) == 1 st1.unlink() self.assertEqual(0, len(c.subject.appliedStereotype)) self.assertEqual(0, len(c._compartments)) def test_removing_stereotype_attribute(self): """Test if stereotype instance specification is destroyed when stereotype attribute is removed """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True # test precondition assert len(c._compartments) == 0 obj = UML.model.apply_stereotype(factory, c.subject, self.st1) # test precondition assert len(c._compartments) == 1 assert len(self.kindof(UML.Slot)) == 0 attr = self.st1.ownedAttribute[0] slot = UML.model.add_slot(factory, obj, attr) assert len(obj.slot) == 1 assert len(self.kindof(UML.Slot)) == 1 self.assertTrue(slot.definingFeature) compartment = c._compartments[0] assert compartment.visible attr.unlink() self.assertEqual(0, len(obj.slot)) self.assertEqual(0, len(self.kindof(UML.Slot))) self.assertFalse(compartment.visible) def test_stereotype_attributes_status_saving(self): """Test stereotype attributes status saving """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True UML.model.apply_stereotype(factory, c.subject, self.st1) obj = UML.model.apply_stereotype(factory, c.subject, self.st2) # change attribute of 2nd stereotype attr = self.st2.ownedAttribute[0] slot = UML.model.add_slot(self.element_factory, obj, attr) slot.value = "st2 test21" data = self.save() self.load(data) item = self.diagram.canvas.select(lambda e: isinstance(e, ComponentItem))[0] self.assertTrue(item.show_stereotypes_attrs) self.assertEqual(2, len(item._compartments)) # first stereotype has no attributes changed, so compartment # invisible self.assertFalse(item._compartments[0].visible) self.assertTrue(item._compartments[1].visible) def test_saving_stereotype_attributes(self): """Test stereotype attributes saving """ factory = self.element_factory c = self.create(ComponentItem, UML.Component) c.show_stereotypes_attrs = True UML.model.apply_stereotype(factory, c.subject, self.st1) UML.model.apply_stereotype(factory, c.subject, self.st2) self.assertEqual(3, len(self.st1.ownedAttribute)) attr1, attr2, attr3 = self.st1.ownedAttribute assert attr1.name == "st1_attr_1", attr1.name assert attr2.name == "st1_attr_2", attr2.name assert attr3.name == "baseClass", attr3.name obj = c.subject.appliedStereotype[0] slot = UML.model.add_slot(self.element_factory, obj, attr1) slot.value = "st1 test1" slot = UML.model.add_slot(self.element_factory, obj, attr2) slot.value = "st1 test2" data = self.save() self.load(data) item = self.diagram.canvas.select(lambda e: isinstance(e, ComponentItem))[0] el = item.subject self.assertEqual(2, len(el.appliedStereotype)) # check if stereotypes are properly applied names = sorted(obj.classifier[0].name for obj in el.appliedStereotype) self.assertEqual(["st1", "st2"], names) # two attributes were changed for stereotype st1, so 2 slots obj = el.appliedStereotype[0] self.assertEqual(2, len(obj.slot)) self.assertEqual("st1_attr_1", obj.slot[0].definingFeature.name) self.assertEqual("st1 test1", obj.slot[0].value) self.assertEqual("st1_attr_2", obj.slot[1].definingFeature.name) self.assertEqual("st1 test2", obj.slot[1].value) # no stereotype st2 attribute changes, no slots obj = el.appliedStereotype[1] self.assertEqual(0, len(obj.slot)) # vim:sw=4:et PK!`cc&gaphor/diagram/tests/test_connector.py""" Test connector item. """ from gaphor import UML from gaphor.diagram.connector import ConnectorItem from gaphor.tests.testcase import TestCase class ConnectorItemTestCase(TestCase): """ Connector item basic tests. """ def test_create(self): """Test creation of connector item """ conn = self.create(ConnectorItem, UML.Connector) self.assertFalse(conn.subject is None) # self.assertTrue(conn.end is None) def test_name(self): """Test connected interface name """ conn = self.create(ConnectorItem, UML.Connector) end = self.element_factory.create(UML.ConnectorEnd) iface = self.element_factory.create(UML.Interface) end.role = iface conn.subject.end = end # conn.end = end # self.assertTrue(conn._end is end) self.assertEqual("", conn._interface.text) iface.name = "RedSea" self.assertEqual("RedSea", conn._interface.text) def test_setting_end(self): """Test creation of connector item """ conn = self.create(ConnectorItem, UML.Connector) end = self.element_factory.create(UML.ConnectorEnd) iface = self.element_factory.create(UML.Interface) end.role = iface iface.name = "RedSea" conn.subject.end = end # conn.end = end # self.assertTrue(conn._end is end) self.assertEqual("RedSea", conn._interface.text) del conn.subject.end[end] conn.end = None self.assertEqual("", conn._interface.text) def test_persistence(self): """Test connector item saving/loading """ conn = self.create(ConnectorItem, UML.Connector) end = self.element_factory.create(UML.ConnectorEnd) # conn.end = end data = self.save() self.assertTrue(end.id in data) self.load(data) connectors = self.diagram.canvas.select(lambda e: isinstance(e, ConnectorItem)) ends = self.kindof(UML.ConnectorEnd) # self.assertTrue(connectors[0].end is not None) # self.assertTrue(connectors[0].end is ends[0]) PK!ֺhh(gaphor/diagram/tests/test_diagramitem.py""" Test basic diagram item functionality like styles, etc. """ import unittest from gaphor.diagram.diagramitem import DiagramItem class ItemTestCase(unittest.TestCase): def setUp(self): class ItemA(DiagramItem): __style__ = {"a-01": 1, "a-02": 2} self.ItemA = ItemA def test_style_assign(self): """ Test style assign """ item_a = self.ItemA() self.assertEqual(self.ItemA.style.a_01, 1) self.assertEqual(self.ItemA.style.a_02, 2) self.assertEqual(item_a.style.a_01, 1) self.assertEqual(item_a.style.a_02, 2) def test_style_override(self): """ Test style override """ class ItemB(self.ItemA): __style__ = {"b-01": 3, "b-02": 4, "a-01": 5} item_b = ItemB() self.assertEqual(ItemB.style.b_01, 3) self.assertEqual(ItemB.style.b_02, 4) self.assertEqual(ItemB.style.a_01, 5) self.assertEqual(ItemB.style.a_02, 2) self.assertEqual(item_b.style.b_01, 3) self.assertEqual(item_b.style.b_02, 4) self.assertEqual(item_b.style.a_01, 5) self.assertEqual(item_b.style.a_02, 2) # check ItemA style, it should remain unaffected by ItemB style # changes self.assertEqual(self.ItemA.style.a_01, 1) self.assertEqual(self.ItemA.style.a_02, 2) PK!0|'gaphor/diagram/tests/test_interfaces.py""" Test Interfaces. """ import unittest from zope import interface from gaphor import diagram from gaphor import diagram from gaphor.tests import TestCase class InterfacesTestCase(TestCase): def test_comment(self): # self.assertTrue(diagram.interfaces.ICommentItem.implementedBy(diagram.comment.CommentItem)) item = diagram.comment.CommentItem() editor = diagram.interfaces.IEditor(item) self.assertTrue(editor) self.assertTrue(editor._item is item) # vim: sw=4:et PK!qm$gaphor/diagram/tests/test_message.py""" Test messages. """ from gaphor import UML from gaphor.diagram.message import MessageItem from gaphor.tests.testcase import TestCase class MessageTestCase(TestCase): def test_message(self): """Test creation of messages """ self.create(MessageItem, UML.Message) def test_adding_message(self): """Test adding message on communication diagram """ factory = self.element_factory item = self.create(MessageItem, UML.Message) message = factory.create(UML.Message) message.name = "test-message" item.add_message(message, False) self.assertTrue(message in item._messages) self.assertTrue(message not in item._inverted_messages) self.assertEqual(item._messages[message].text, "test-message") message = factory.create(UML.Message) message.name = "test-inverted-message" item.add_message(message, True) self.assertTrue(message in item._inverted_messages) self.assertTrue(message not in item._messages) self.assertEqual(item._inverted_messages[message].text, "test-inverted-message") def test_changing_message_text(self): """Test changing message text """ factory = self.element_factory item = self.create(MessageItem, UML.Message) message = factory.create(UML.Message) message.name = "test-message" item.add_message(message, False) self.assertEqual(item._messages[message].text, "test-message") item.set_message_text(message, "test-message-changed", False) self.assertEqual(item._messages[message].text, "test-message-changed") message = factory.create(UML.Message) message.name = "test-message" item.add_message(message, True) self.assertEqual(item._inverted_messages[message].text, "test-message") item.set_message_text(message, "test-message-changed", True) self.assertEqual(item._inverted_messages[message].text, "test-message-changed") def test_message_removal(self): """Test message removal """ factory = self.element_factory item = self.create(MessageItem, UML.Message) message = factory.create(UML.Message) item.add_message(message, False) self.assertTrue(message in item._messages) item.remove_message(message, False) self.assertTrue(message not in item._messages) message = factory.create(UML.Message) item.add_message(message, True) self.assertTrue(message in item._inverted_messages) item.remove_message(message, True) self.assertTrue(message not in item._inverted_messages) def test_messages_swapping(self): """Test messages swapping """ factory = self.element_factory item = self.create(MessageItem, UML.Message) m1 = factory.create(UML.Message) m2 = factory.create(UML.Message) item.add_message(m1, False) item.add_message(m2, False) item.swap_messages(m1, m2, False) m1 = factory.create(UML.Message) m2 = factory.create(UML.Message) item.add_message(m1, True) item.add_message(m2, True) item.swap_messages(m1, m2, True) def test_message_persistence(self): """Test message saving/loading """ factory = self.element_factory item = self.create(MessageItem, UML.Message) m1 = factory.create(UML.Message) m2 = factory.create(UML.Message) m3 = factory.create(UML.Message) m4 = factory.create(UML.Message) m1.name = "m1" m2.name = "m2" m3.name = "m3" m4.name = "m4" item.add_message(m1, False) item.add_message(m2, False) item.add_message(m3, True) item.add_message(m4, True) data = self.save() self.load(data) item = self.diagram.canvas.select(lambda e: isinstance(e, MessageItem))[0] self.assertEqual(len(item._messages), 2) self.assertEqual(len(item._inverted_messages), 2) # check for loaded messages and order of messages self.assertEqual(["m1", "m2"], [m.name for m in item._messages]) self.assertEqual(["m3", "m4"], [m.name for m in item._inverted_messages]) PK!K Oss'gaphor/diagram/tests/test_objectnode.pyimport gaphor.UML as UML from gaphor.diagram import items from gaphor.tests.testcase import TestCase class ObjectNodeTestCase(TestCase): def test_object_node(self): self.create(items.ObjectNodeItem, UML.ObjectNode) def test_name(self): """ Test updating of object node name """ node = self.create(items.ObjectNodeItem, UML.ObjectNode) node.subject.name = "Blah" self.assertEqual("Blah", node._name.text) node.subject = None # Undefined def test_upper_bound(self): """ TODO: Test upper bound """ pass def test_ordering(self): """ Test updating of ObjectNodeItem.ordering. """ node = self.create(items.ObjectNodeItem, UML.ObjectNode) node.subject.ordering = "unordered" self.assertEqual("{ ordering = unordered }", node._ordering.text) node.show_ordering = True self.assertEqual("{ ordering = unordered }", node._ordering.text) def test_persistence(self): """ TODO: Test connector item saving/loading """ pass PK!Y\\'gaphor/diagram/tests/test_simpleitem.py""" Unnit tests for simple items. """ import unittest from gaphor.tests import TestCase from gaphor import UML from gaphor.diagram.simpleitem import Line, Box, Ellipse from gaphas import View class SimpleItemTestCase(TestCase): def setUp(self): super(SimpleItemTestCase, self).setUp() self.view = View(self.diagram.canvas) def test_line(self): """ """ self.diagram.create(Line) def test_box(self): """ """ self.diagram.create(Line) def test_ellipse(self): """ """ self.diagram.create(Ellipse) PK!%%"gaphor/diagram/tests/test_style.py""" Test item styles. """ import unittest from gaphor.diagram.style import ( get_text_point, get_text_point_at_line, get_text_point_at_line2, get_min_size, ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, ALIGN_TOP, ALIGN_MIDDLE, ALIGN_BOTTOM, ) class StyleTestCase(unittest.TestCase): def test_min_size(self): """ Test minimum size calculation """ width, height = get_min_size(10, 10, (1, 2, 3, 4)) self.assertEqual(width, 16) self.assertEqual(height, 14) def test_align_box(self): """ Test aligned text position calculation """ extents = 80, 12 padding = (1, 2, 3, 4) data = { (ALIGN_LEFT, ALIGN_TOP, False): (4, 1), (ALIGN_LEFT, ALIGN_MIDDLE, False): (4, 14), (ALIGN_LEFT, ALIGN_BOTTOM, False): (4, 25), (ALIGN_CENTER, ALIGN_TOP, False): (42, 1), (ALIGN_CENTER, ALIGN_MIDDLE, False): (42, 14), (ALIGN_CENTER, ALIGN_BOTTOM, False): (42, 25), (ALIGN_RIGHT, ALIGN_TOP, False): (78, 1), (ALIGN_RIGHT, ALIGN_MIDDLE, False): (78, 14), (ALIGN_RIGHT, ALIGN_BOTTOM, False): (78, 25), (ALIGN_LEFT, ALIGN_TOP, True): (-84, -13), (ALIGN_LEFT, ALIGN_MIDDLE, True): (-84, 14), (ALIGN_LEFT, ALIGN_BOTTOM, True): (-84, 43), (ALIGN_CENTER, ALIGN_TOP, True): (40, -13), (ALIGN_CENTER, ALIGN_MIDDLE, True): (40, 14), (ALIGN_CENTER, ALIGN_BOTTOM, True): (40, 43), (ALIGN_RIGHT, ALIGN_TOP, True): (162, -13), (ALIGN_RIGHT, ALIGN_MIDDLE, True): (162, 14), (ALIGN_RIGHT, ALIGN_BOTTOM, True): (162, 43), } for halign in range(-1, 2): for valign in range(-1, 2): for outside in (True, False): align = (halign, valign) point_expected = data[(halign, valign, outside)] point = get_text_point(extents, 160, 40, align, padding, outside) self.assertEqual( point[0], point_expected[0], "%s, %s -> %s" % (align, outside, point[0]), ) self.assertEqual( point[1], point_expected[1], "%s, %s -> %s" % (align, outside, point[1]), ) def test_align_line(self): """ Test aligned at the line text position calculation """ p1 = 0, 0 p2 = 20, 20 extents = 10, 5 x, y = get_text_point_at_line( extents, p1, p2, (ALIGN_LEFT, ALIGN_TOP), (2, 2, 2, 2) ) self.assertEqual(x, 5) self.assertEqual(y, -10) x, y = get_text_point_at_line( extents, p1, p2, (ALIGN_RIGHT, ALIGN_TOP), (2, 2, 2, 2) ) self.assertEqual(x, 5) self.assertEqual(y, -10) p2 = -20, 20 x, y = get_text_point_at_line( extents, p1, p2, (ALIGN_LEFT, ALIGN_TOP), (2, 2, 2, 2) ) self.assertEqual(x, -15) self.assertEqual(y, -10) x, y = get_text_point_at_line( extents, p1, p2, (ALIGN_RIGHT, ALIGN_TOP), (2, 2, 2, 2) ) self.assertEqual(x, -15) self.assertEqual(y, -10) def test_align_line2_h(self): """ Test aligned at the line text position calculation, horizontal mode """ extents = 10, 5 p1 = 2.0, 2.0 # align top p2 = 22.0, 7.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, -4.75) p2 = 22.0, -3.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, -9.75) p2 = -18.0, 7.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13) self.assertAlmostEqual(y, -4.75) p2 = -18.0, -3.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13) self.assertAlmostEqual(y, -9.75) # align bottom p2 = 22.0, 7.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, 8.75) p2 = 22.0, -3.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, 3.75) p2 = -18.0, 7.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13) self.assertAlmostEqual(y, 8.75) p2 = -18.0, -3.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13) self.assertAlmostEqual(y, 3.75) def test_align_line2_v(self): """ Test aligned at the line text position calculation, vertical mode """ extents = 10, 5 p1 = 2.0, 2.0 # top align p2 = 7.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7.125) self.assertAlmostEqual(y, 9.5) p2 = 7.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -8.125) self.assertAlmostEqual(y, -10.5) p2 = -3.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13.125) self.assertAlmostEqual(y, 9.5) p2 = -3.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 2.125) self.assertAlmostEqual(y, -10.5) # bottom align p2 = 7.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -8.125) self.assertAlmostEqual(y, 9.5) p2 = 7.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7.125) self.assertAlmostEqual(y, -10.5) p2 = -3.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 2.125) self.assertAlmostEqual(y, 9.5) p2 = -3.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13.125) self.assertAlmostEqual(y, -10.5) def test_align_line2_o(self): """ Test aligned at the line text position calculation, orthogonal lines """ extents = 10, 5 p1 = 2.0, 2.0 # top align p2 = 22.0, 2.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, -6) p2 = -18.0, 2.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13) self.assertAlmostEqual(y, -6) p2 = 2.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -10.0) self.assertAlmostEqual(y, 9.5) p2 = 2.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_TOP), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -10.0) self.assertAlmostEqual(y, -10.5) # bottom align p2 = 22.0, 2.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 7) self.assertAlmostEqual(y, 5) p2 = -18.0, 2.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, -13.0) self.assertAlmostEqual(y, 5.0) p2 = 2.0, 22.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 4.0) self.assertAlmostEqual(y, 9.5) p2 = 2.0, -18.0 x, y = get_text_point_at_line2( extents, p1, p2, (ALIGN_CENTER, ALIGN_BOTTOM), (3, 2, 3, 2) ) self.assertAlmostEqual(x, 4.0) self.assertAlmostEqual(y, -10.5) PK!Bz44gaphor/diagram/textelement.py""" Support for editable text, a part of a diagram item, i.e. name of named item, guard of flow item, etc. """ import math import cairo import gi from gaphas.freehand import FreeHandCairoContext from gaphas.geometry import distance_rectangle_point, Rectangle from gi.repository import Pango gi.require_version("PangoCairo", "1.0") from gi.repository import PangoCairo from gaphor.diagram.style import ALIGN_CENTER, ALIGN_TOP from gaphor.diagram.style import Style DEFAULT_TEXT_FONT = "sans 10" def swap(list, el1, el2): """ Swap two elements on the list. """ i1 = list.index(el1) i2 = list.index(el2) list[i1] = el2 list[i2] = el1 def _text_layout(cr, text, font, width): layout = PangoCairo.create_layout(cr) if font: layout.set_font_description(Pango.FontDescription.from_string(font)) layout.set_text(text, length=-1) layout.set_width(int(width * Pango.SCALE)) # layout.set_height(height) return layout def text_extents(cr, text, font=None, width=-1, height=-1): if not text: return 0, 0 layout = _text_layout(cr, text, font, width) return layout.get_pixel_size() def text_align( cr, x, y, text, font, width=-1, height=-1, align_x=0, align_y=0, padding_x=0, padding_y=0, ): """ Draw text relative to (x, y). x, y - coordinates text - text to print (utf8) font - The font to render in width height align_x - 1 (top), 0 (middle), -1 (bottom) align_y - 1 (left), 0 (center), -1 (right) padding_x - padding (extra offset), always > 0 padding_y - padding (extra offset), always > 0 """ if not isinstance(cr, cairo.Context): return if not text: return layout = _text_layout(cr, text, font, width) w, h = layout.get_pixel_size() if align_x == 0: x = 0.5 - (w / 2) + x elif align_x < 0: x = -w + x - padding_x else: x = x + padding_x if align_y == 0: y = 0.5 - (h / 2) + y elif align_y < 0: y = -h + y - padding_y else: y = y + padding_y cr.move_to(x, y) PangoCairo.show_layout(cr, layout) def text_center(cr, x, y, text, font): text_align(cr, x, y, text, font=font, align_x=0, align_y=0) def text_multiline(cr, x, y, text, font, width=-1, height=-1): text_align( cr, x, y, text, font=font, width=width, height=height, align_x=1, align_y=1 ) class EditableTextSupport(object): """ Editable text support to allow display and edit text parts of a diagram item. Attributes: - _texts: list of diagram item text elements - _text_groups: grouping information of text elements (None - ungrouped) """ def __init__(self): self._texts = [] self._text_groups = {None: []} self._text_groups_sizes = {} def postload(self): super(EditableTextSupport, self).postload() def texts(self): """ Return list of diagram item text elements. """ return self._texts def add_text(self, attr, style=None, pattern=None, visible=None, editable=False): """ Create and add a text element. For parameters description and more information see TextElement class documentation. If style information contains 'text-align-group' data, then text element is grouped. Returns created text element. """ txt = TextElement( attr, style=style, pattern=pattern, visible=visible, editable=editable ) self._texts.append(txt) # try to group text element gname = style and style.get("text-align-group") or None if gname not in self._text_groups: self._text_groups[gname] = [] group = self._text_groups[gname] group.append(txt) return txt def remove_text(self, txt): """ Remove a text element from diagram item. Parameters: - txt: text to be removed """ # remove from align group style = txt.style if style and hasattr(style, "text_align_group"): gname = style.text_align_group else: gname = None group = self._text_groups[gname] group.remove(txt) # remove text element from diagram item self._texts.remove(txt) def swap_texts(self, txt1, txt2): """ Swap two text elements. """ swap(self._texts, txt1, txt2) style = txt1.style if style and hasattr(style, "text_align_group"): gname = style.text_align_group else: gname = None group = self._text_groups[gname] swap(group, txt1, txt2) def _get_visible_texts(self, texts): """ Get list of visible texts. """ return [txt for txt in texts if txt.is_visible()] def _get_text_groups(self): """ Get text groups. """ tg = self._text_groups groups = self._text_groups return ((name, tg[name]) for name in groups if name) def _set_text_sizes(self, context, texts): """ Calculate size for every text in the list. Parameters: - context: cairo context - texts: list of texts """ cr = context.cairo for txt in texts: w, h = text_extents(cr, txt.text, font=txt.style.font) txt.bounds.width = max(15, w) txt.bounds.height = max(10, h) def _set_text_group_size(self, context, name, texts): """ Calculate size of a group. Parameters: - context: cairo context - name: group name - texts: list of group texts """ cr = context.cairo texts = self._get_visible_texts(texts) if not texts: self._text_groups_sizes[name] = (0, 0) return # find maximum width and total height width = max(txt.bounds.width for txt in texts) height = sum(txt.bounds.height for txt in texts) self._text_groups_sizes[name] = width, height def pre_update(self, context): """ Calculate sizes of text elements and text groups. """ cr = context.cairo # calculate sizes of text groups for name, texts in self._get_text_groups(): self._set_text_sizes(context, texts) self._set_text_group_size(context, name, texts) # calculate sizes of ungrouped texts texts = self._text_groups[None] texts = self._get_visible_texts(texts) self._set_text_sizes(context, texts) def _text_group_align(self, context, name, texts): """ Align group of text elements making vertical stack of strings. Parameters: - context: cairo context - name: group name - texts: list of group texts """ cr = context.cairo texts = self._get_visible_texts(texts) if not texts: return # align according to style of last text in the group style = texts[-1]._style extents = self._text_groups_sizes[name] x, y = self.text_align( extents, style.text_align, style.text_padding, style.text_outside ) max_hint = 0 if style.text_align_str: for txt in texts: txt._hint = self._get_text_align_hint(cr, txt) max_hint = max(max_hint, txt._hint) # stack texts dy = 0 dw = extents[0] for txt in texts: bounds = txt.bounds width, height = bounds.width, bounds.height # center stacked texts if max_hint: txt.bounds.x = x + max_hint - txt._hint else: txt.bounds.x = x + (dw - width) / 2.0 txt.bounds.y = y + dy dy += height def _get_text_align_hint(self, cr, txt): """ Calculate hint value for text element depending on ``text_align_str`` style property. """ style = txt.style chunks = txt.text.split(style.text_align_str, 1) hint = 0 if len(chunks) > 1: hint, _ = text_extents(cr, chunks[0], font=txt.style.font) return hint def post_update(self, context): """ Calculate position and sizes of all text elements of a diagram item. """ cr = context.cairo # align groups of text elements for name, texts in self._get_text_groups(): assert name in self._text_groups_sizes, 'No text group "%s"' % name self._text_group_align(context, name, texts) # align ungrouped text elements texts = self._get_visible_texts(self._text_groups[None]) for txt in texts: style = txt.style extents = txt.bounds.width, txt.bounds.height x, y = self.text_align( extents, style.text_align, style.text_padding, style.text_outside ) bounds = txt.bounds # fixme: gaphor rectangle problem width, height = ( bounds.width, bounds.height, ) # fixme: gaphor rectangle problem txt.bounds.x = x txt.bounds.y = y def point(self, pos): """ Return the distance to the nearest editable and visible text element. """ def distances(): yield 10000.0 for txt in self._texts: if txt.is_visible() and txt.editable: yield distance_rectangle_point(txt.bounds, pos) return min(distances()) def draw(self, context): """ Draw all text elements of a diagram item. """ cr = context.cairo cr.save() try: # fixme: do it on per group basis if any( txt._style.text_rotated for txt in self._get_visible_texts(self._texts) ): cr.rotate(-math.pi / 2) if self.subject: for txt in self._get_visible_texts(self._texts): txt.draw(context) finally: cr.restore() class TextElement(object): """ Representation of an editable text, which is part of a diagram item. Text element is aligned according to style information. It also displays and allows to edit value of an attribute of UML class (DiagramItem.subject). Attribute name can be recursive, all below attribute names are valid: - name (named item name) - guard.value (flow item guard) Attributes and properties: - attr: name of displayed and edited UML class attribute - bounds: text bounds - _style: text style (i.e. align information, padding) - text: rendered string to be displayed - pattern: print pattern of text - editable: True if text should be editable See also EditableTextSupport.add_text. """ bounds = property(lambda self: self._bounds) def __init__(self, attr, style=None, pattern=None, visible=None, editable=False): """ Create new text element with bounds (0, 0, 10, 10) and empty text. Parameters: - visible: function, which evaluates to True/False if text should be visible """ super(TextElement, self).__init__() self._bounds = Rectangle(0, 0, width=15, height=10) # create default style for a text element self._style = Style() self._style.add("text-padding", (2, 2, 2, 2)) self._style.add("text-align", (ALIGN_CENTER, ALIGN_TOP)) self._style.add("text-outside", False) self._style.add("text-rotated", False) self._style.add("text-align-str", None) self._style.add("font", DEFAULT_TEXT_FONT) if style: self._style.update(style) self.attr = attr self._text = "" if visible: self.is_visible = visible if pattern: self._pattern = pattern else: self._pattern = "%s" self.editable = editable def _set_text(self, value): """ Render text value using pattern. """ self._text = value and self._pattern % value or "" text = property(lambda s: s._text, _set_text) style = property(lambda s: s._style) def is_visible(self): """ Display text by default. """ return True def draw(self, context): bounds = self.bounds x, y = bounds.x, bounds.y width, height = bounds.width, bounds.height cr = context.cairo cr.save() try: if isinstance(cr, FreeHandCairoContext): cr = cr.cr if isinstance(cr, cairo.Context) and self.text: cr.move_to(x, y) layout = PangoCairo.create_layout(cr) layout.set_font_description(Pango.FontDescription(self._style.font)) layout.set_text(text=self.text, length=-1) PangoCairo.show_layout(cr, layout) if self.editable and (context.hovered or context.focused): cr.set_source_rgb(0.6, 0.6, 0.6) cr.set_line_width(0.5) cr.rectangle(x - 5, y - 1, width + 10, height + 2) cr.stroke() finally: cr.restore() PK!Fgaphor/diagram/usecase.py""" Use case diagram item. """ from gaphas.util import path_ellipse from gaphor import UML from gaphor.diagram.classifier import ClassifierItem from gaphor.diagram.style import ALIGN_CENTER, ALIGN_MIDDLE from gaphor.diagram.textelement import text_extents class UseCaseItem(ClassifierItem): """ Presentation of gaphor.UML.UseCase. """ __uml__ = UML.UseCase __style__ = {"min-size": (50, 30), "name-align": (ALIGN_CENTER, ALIGN_MIDDLE)} def __init__(self, id=None): super(UseCaseItem, self).__init__(id) self.drawing_style = -1 def pre_update(self, context): cr = context.cairo text = self.subject.name if text: width, height = text_extents(cr, text) self.min_width, self.min_height = width + 10, height + 20 super(UseCaseItem, self).pre_update(context) def draw(self, context): cr = context.cairo rx = self.width / 2.0 ry = self.height / 2.0 cr.move_to(self.width, ry) path_ellipse(cr, rx, ry, self.width, self.height) cr.stroke() super(UseCaseItem, self).draw(context) # vim:sw=4:et PK!Hj;gaphor/event.py""" Application wide events are managed here. """ from zope.interface import implementer from gaphor.interfaces import * @implementer(IServiceEvent) class ServiceInitializedEvent(object): """ This event is emitted every time a new service has been initialized. """ def __init__(self, name, service): self.name = name self.service = service @implementer(IServiceEvent) class ServiceShutdownEvent(object): """ This event is emitted every time a service has been shut down. """ def __init__(self, name, service): self.name = name self.service = service @implementer(ITransactionEvent) class TransactionBegin(object): """ This event denotes the beginning of a transaction. Nested (sub-) transactions should not emit this signal. """ pass @implementer(ITransactionEvent) class TransactionCommit(object): """ This event is emitted when a transaction (toplevel) is successfully committed. """ pass @implementer(ITransactionEvent) class TransactionRollback(object): """ If a set of operations fail (e.i. due to an exception) the transaction should be marked for rollback. This event is emitted to tell the operation has failed. """ pass @implementer(IActionExecutedEvent) class ActionExecuted(object): """ Once an operation has successfully been executed this event is raised. """ def __init__(self, name, action): self.name = name self.action = action PK!jgaphor/i18n.py"""Internationalization (i18n) support for Gaphor. Here the _() function is defined that is used to translate text into your native language.""" __all__ = ["_"] import os import gettext import pkg_resources localedir = os.path.join( pkg_resources.get_distribution("gaphor").location, "gaphor", "data", "locale" ) try: catalog = gettext.Catalog("gaphor", localedir=localedir) _ = catalog.gettext except IOError as e: def _(s): return s PK!-7~~gaphor/interfaces.py""" Top level interface definitions for Gaphor. """ from zope import interface class IService(interface.Interface): """ Base interface for all services in Gaphor. """ def init(self, application): """ Initialize the service, this method is called after all services are instantiated. """ def shutdown(self): """ Shutdown the services, free resources. """ class IServiceEvent(interface.Interface): """ An event emitted by a service. """ service = interface.Attribute("The service that emits the event") class ITransaction(interface.Interface): """ The methods each transaction should adhere. """ def commit(self): """ Commit the transaction. """ def rollback(self): """ Roll back the transaction. """ class ITransactionEvent(interface.Interface): """ Events related to transaction workflow (begin/commit/rollback) implements this interface. """ class IActionProvider(interface.Interface): """ An action provider is a special service that provides actions (see gaphor/action.py) and the accompanying XML for the UI manager. """ menu_xml = interface.Attribute("The menu XML") action_group = interface.Attribute("The accompanying ActionGroup") class IActionExecutedEvent(interface.Interface): """ An event emitted when an action has been performed. """ name = interface.Attribute("Name of the action performed, if any") action = interface.Attribute("The performed action") class IEventFilter(interface.Interface): """ Filter events when they're about to be handled. """ def filter(self): """ Return a value (e.g. message/reason) why the event is filtered. Returning `None` or `False` will propagate the event. """ # vim:sw=4:et PK!11gaphor/misc/__init__.pyimport os from gi.repository import GLib def get_config_dir(): """Return the directory where the user's config is stored. This varies depending on platform.""" config_dir = os.path.join(GLib.get_user_config_dir(), "gaphor") os.makedirs(config_dir, exist_ok=True) return config_dir PK!0XE  gaphor/misc/colorbutton.py""" A version of the standard Gtk.ColorButton tweaked towards Gaphor. Gaphor is using color values from 0 to 1 (cairo standard), so that required some tweaks on the color widget. The standard format is `(red, green, blue, alpha)`. """ from gi.repository import Gtk class ColorButton(Gtk.ColorButton): __gtype_name__ = "GaphorColorButton" def __init__(self, r, g, b, a): GObject.GObject.__init__(self) self.set_color(Gdk.Color(int(r * 65535), int(g * 65535), int(b * 65535))) self.set_use_alpha(True) self.set_alpha(int(a * 65535)) def get_color_float(self): c = self.get_color() return (c.red_float, c.green_float, c.blue_float, self.get_alpha() / 65535.0) color = property(lambda s: s.get_color_float()) PK!:##gaphor/misc/console.py#!/usr/bin/env python # GTK Interactive Console # (C) 2003, Jon Anderson # See www.python.org/2.2/license.html for # license details. # import code import sys import pydoc from rlcompleter import Completer if __name__ == "__main__": import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Pango from gi.repository import GLib banner = ( """Gaphor Interactive Python Console %s Type "help" for more information. """ % sys.version ) class Help(object): def __call__(self, obj=None): if obj: pydoc.help(obj) else: str(self) def __str__(self): return "Usage: help(object)" def __repr__(self): return str(self) class TextViewWriter(object): """ A Multiplexing output stream. It can replace another stream, and tee output to the original stream and too a GTK textview. """ def __init__(self, name, view, style): self.name = name self.out = getattr(sys, name) self.view = view self.style = style def write(self, text): buffer = self.view.get_buffer() end = buffer.get_end_iter() buffer.insert_with_tags(end, text, self.style) def __enter__(self): setattr(sys, self.name, self) return self def __exit__(self, exception_type, exception_value, traceback): setattr(sys, self.name, self.out) class GTKInterpreterConsole(Gtk.ScrolledWindow): """ An InteractiveConsole for GTK. It's an actual widget, so it can be dropped in just about anywhere. """ __gtype_name__ = "GTKInterpreterConsole" def __init__(self, locals=None, banner=banner): Gtk.ScrolledWindow.__init__(self) self.set_min_content_width(640) self.set_min_content_height(480) self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.text = Gtk.TextView() self.text.set_wrap_mode(True) self.text.set_monospace(True) self.text.set_border_width(4) self.interpreter = code.InteractiveInterpreter(locals) self.interpreter.locals["help"] = Help() self.completer = Completer(self.interpreter.locals) self.buffer = [] self.history = [] self.banner = banner self.ps1 = ">>> " self.ps2 = "... " self.text.add_events(Gdk.EventMask.KEY_PRESS_MASK) self.text.connect("key_press_event", self.key_pressed) self.current_history = -1 self.mark = self.text.get_buffer().create_mark( "End", self.text.get_buffer().get_end_iter(), False ) # setup colors self.style_banner = Gtk.TextTag.new("banner") self.style_banner.set_property("foreground", "saddle brown") self.style_ps1 = Gtk.TextTag.new("ps1") self.style_ps1.set_property("foreground", "DarkOrchid4") self.style_ps1.set_property("editable", False) self.style_ps2 = Gtk.TextTag.new("ps2") self.style_ps2.set_property("foreground", "DarkOliveGreen") self.style_ps2.set_property("editable", False) self.style_out = Gtk.TextTag.new("stdout") self.style_out.set_property("foreground", "midnight blue") self.style_out.set_property("editable", False) self.style_err = Gtk.TextTag.new("stderr") self.style_err.set_property("style", Pango.Style.ITALIC) self.style_err.set_property("foreground", "red") self.style_err.set_property("editable", False) self.text.get_buffer().get_tag_table().add(self.style_banner) self.text.get_buffer().get_tag_table().add(self.style_ps1) self.text.get_buffer().get_tag_table().add(self.style_ps2) self.text.get_buffer().get_tag_table().add(self.style_out) self.text.get_buffer().get_tag_table().add(self.style_err) self.stdout = TextViewWriter("stdout", self.text, self.style_out) self.stderr = TextViewWriter("stderr", self.text, self.style_err) self.current_prompt = None self.add(self.text) self.text.show() self.write_line(self.banner, self.style_banner) GLib.idle_add(self.prompt_ps1) def reset_history(self): self.history = [] def reset_buffer(self): self.buffer = [] def prompt_ps1(self): self.current_prompt = self.prompt_ps1 self.write_line(self.ps1, self.style_ps1) def prompt_ps2(self): self.current_prompt = self.prompt_ps2 self.write_line(self.ps2, self.style_ps2) def write_line(self, text, style=None): start, end = self.text.get_buffer().get_bounds() if style: self.text.get_buffer().insert_with_tags(end, text, style) else: self.text.get_buffer().insert(end, text) self.text.scroll_to_mark(self.mark, 0, True, 1, 1) def push(self, line): self.buffer.append(line) if len(line) > 0: self.history.append(line) source = "\n".join(self.buffer) with self.stdout, self.stderr: more = self.interpreter.runsource(source, "<>") if not more: self.reset_buffer() return more def key_pressed(self, widget, event): if event.keyval == Gdk.keyval_from_name("Return"): return self.execute_line() if event.keyval == Gdk.keyval_from_name("Up"): self.current_history = self.current_history - 1 if self.current_history < -len(self.history): self.current_history = -len(self.history) return self.show_history() elif event.keyval == Gdk.keyval_from_name("Down"): self.current_history = self.current_history + 1 if self.current_history > 0: self.current_history = 0 return self.show_history() elif event.keyval == Gdk.keyval_from_name("Home"): l = self.text.get_buffer().get_line_count() - 1 start = self.text.get_buffer().get_iter_at_line_offset(l, 4) self.text.get_buffer().place_cursor(start) return True elif event.keyval == Gdk.keyval_from_name("Tab"): return self.complete_line() return False def show_history(self): if self.current_history == 0: return True else: self.replace_line(self.history[self.current_history]) return True def current_line(self): start, end = self.current_line_bounds() return self.text.get_buffer().get_text(start, end, True) def current_line_bounds(self): txt_buffer = self.text.get_buffer() l = txt_buffer.get_line_count() - 1 start = txt_buffer.get_iter_at_line(l) if start.get_chars_in_line() >= 4: start.forward_chars(4) end = txt_buffer.get_end_iter() return start, end def replace_line(self, txt): start, end = self.current_line_bounds() self.text.get_buffer().delete(start, end) self.write_line(txt) def execute_line(self): line = self.current_line() self.write_line("\n") more = self.push(line) self.text.get_buffer().place_cursor(self.text.get_buffer().get_end_iter()) if more: self.prompt_ps2() else: self.prompt_ps1() self.current_history = 0 return True def complete_line(self): line = self.current_line() tokens = line.split() if tokens: token = tokens[-1] completions = [] p = self.completer.complete(token, len(completions)) while p != None: completions.append(p) p = self.completer.complete(token, len(completions)) else: completions = list(self.interpreter.locals.keys()) if len(completions) > 1: max_len = max(map(len, completions)) + 2 per_line = 80 // max_len for i, c in enumerate(completions): if i % per_line == 0: self.write_line("\n") self.write_line(c, self.style_ps1) self.write_line(" " * (max_len - len(c)), self.style_ps1) self.write_line("\n") self.current_prompt() self.write_line(line) elif len(completions) == 1: i = line.rfind(token) line = line[0:i] + completions[0] self.replace_line(line) return True def main(): w = Gtk.Window() console = GTKInterpreterConsole() w.add(console) def destroy(arg=None): Gtk.main_quit() def key_event(widget, event): if ( Gdk.keyval_name(event.keyval) == "d" and event.get_state() & Gdk.ModifierType.CONTROL_MASK ): destroy() return False w.connect("destroy", destroy) w.add_events(Gdk.EventMask.KEY_PRESS_MASK) w.connect("key_press_event", key_event) w.show_all() Gtk.main() if __name__ == "__main__": main() PK!oE%gaphor/misc/errorhandler.py"""A generic way to handle errors in GUI applications. This module also contains a ErrorHandlerAspect, which can be easily attached to a class' method and will raise the error dialog when the method exits with an exception. """ from gi.repository import Gtk import sys import pdb from gaphor.i18n import _ def error_handler(message=None, exc_info=None): exc_type, exc_value, exc_traceback = exc_info or sys.exc_info() if not exc_type: return if not message: message = _("An error occurred.") buttons = Gtk.ButtonsType.OK message = "%s\n\nTechnical details:\n\t%s\n\t%s" % (message, exc_type, exc_value) if __debug__ and sys.stdin.isatty(): buttons = Gtk.ButtonsType.YES_NO message += _( "\n\nDo you want to debug?\n(Gaphor should have been started from the command line)" ) dialog = Gtk.MessageDialog( None, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, buttons, message, ) answer = dialog.run() dialog.destroy() if answer == Gtk.ResponseType.YES: pdb.post_mortem(exc_traceback) PK!wwEEgaphor/misc/gidlethread.py# vim:sw=4:et: """This module contains some helpers that can be used to execute generator functions in the GLib main loop. This module provided the following classes: GIdleThread - Thread like behavior for generators in a main loop Queue - A simple queue implementation suitable for use with GIdleThread Exceptions: QueueEmpty - raised when one tried to get a value of an empty queue QueueFull - raised when the queue reaches it's max size and the oldest item may not be disposed. """ import types import sys from gi.repository import GLib import time class GIdleThread(object): """This is a pseudo-"thread" for use with the GTK+ main loop. This class does act a bit like a thread, all code is executed in the callers thread though. The provided function should be a generator (or iterator). It can be started with start(). While the "thread" is running is_alive() can be called to see if it's alive. wait([timeout]) will wait till the generator is finished, or timeout seconds. If an exception is raised from within the generator, it is stored in the exc_info property. Execution of the generator is finished. The exc_info property contains a tuple (exc_type, exc_value, exc_traceback), see sys.exc_info() for details. Note that this routine runs in the current thread, so there is no need for nasty locking schemes. Example (runs a counter through the GLib main loop routine): >>> def counter(max): ... for x in xrange(max): ... yield x >>> t = GIdleThread(counter(123)) >>> id = t.start() >>> main = GLib.main_context_default() >>> t.is_alive() True >>> main.iteration(False) # doctest: +ELLIPSIS True """ def __init__(self, generator, queue=None): assert isinstance( generator, types.GeneratorType ), "The generator should be an iterator" self._generator = generator self._queue = queue self._idle_id = 0 self._exc_info = (None, None, None) def start(self, priority=GLib.PRIORITY_LOW): """Start the generator. Default priority is low, so screen updates will be allowed to happen. """ idle_id = GLib.idle_add(self.__generator_executer, priority=priority) self._idle_id = idle_id return idle_id def wait(self, timeout=0): """Wait until the corouine is finished or return after timeout seconds. This is achieved by running the GTK+ main loop. """ clock = time.clock start_time = clock() main = GLib.main_context_default() while self.is_alive(): main.iteration(False) if timeout and (clock() - start_time >= timeout): return def interrupt(self): """Force the generator to stop running. """ if self.is_alive(): GLib.source_remove(self._idle_id) self._idle_id = 0 def is_alive(self): """Returns True if the generator is still running. """ return self._idle_id != 0 error = property( lambda self: self._exc_info[0], doc="Return a possible exception that had occured " "during execution of the generator", ) exc_info = property( lambda self: self._exc_info, doc="Return a exception information as provided by " "sys.exc_info()", ) def reraise(self): """Rethrow the error that occurred during execution of the idle process. """ exc_info = self._exc_info exctype, value = exc_info[:2] if exctype: raise exctype(value) def __generator_executer(self): try: result = next(self._generator) if self._queue: try: self._queue.put(result) except QueueFull: self.wait(0.5) # If this doesn't work... self._queue.put(result) return True except StopIteration: self._idle_id = 0 return False except: self._exc_info = sys.exc_info() self._idle_id = 0 return False class QueueEmpty(Exception): """Exception raised whenever the queue is empty and someone tries to fetch a value. """ pass class QueueFull(Exception): """Exception raised when the queue is full and the oldest item may not be disposed. """ pass class Queue(object): """A FIFO queue. If the queue has a max size, the oldest item on the queue is dropped if that size id exceeded. """ def __init__(self, size=0, dispose_oldest=True): self._queue = [] self._size = size self._dispose_oldest = dispose_oldest def put(self, item): """Put item on the queue. If the queue size is limited ... """ if self._size > 0 and len(self._queue) >= self._size: if self._dispose_oldest: self.get() else: raise QueueFull self._queue.insert(0, item) def get(self): """Get the oldest item off the queue. QueueEmpty is raised if no items are left on the queue. """ try: return self._queue.pop() except IndexError: raise QueueEmpty if __name__ == "__main__": def counter(max): for i in range(max): yield i def shower(queue): # Never stop reading the queue: while True: try: cnt = queue.get() print("cnt =", cnt) except QueueEmpty: pass yield None print("Test 1: (should print range 0..22)") queue = Queue() c = GIdleThread(counter(23), queue) s = GIdleThread(shower(queue)) main = GLib.main_context_default() c.start() s.start() s.wait(2) print("Test 2: (should only print 22)") queue = Queue(size=1) c = GIdleThread(counter(23), queue) s = GIdleThread(shower(queue)) main = GLib.main_context_default() c.start(priority=GLib.PRIORITY_DEFAULT) s.start() s.wait(3) PK!vDŽhgaphor/misc/listmixins.py""" This module contains some support code for queries on lists. Two mixin classes are provided: 1. ``querymixin`` 2. ``recursemixin`` See the documentation on the mixins. """ __all__ = ["querymixin", "recursemixin", "getslicefix"] import sys class Matcher(object): """ Returns True if the expression returns True. The context for the expression is the element. Given a class: >>> class A(object): ... def __init__(self, name): self.name = name We can create a path for each object: >>> a = A('root') >>> a.a = A('level1') >>> a.b = A('b') >>> a.a.text = 'help' If we want to match, ``it`` is used to refer to the subjected object: >>> Matcher('it.name=="root"')(a) True >>> Matcher('it.b.name=="b"')(a) True >>> Matcher('it.name=="blah"')(a) False >>> Matcher('it.nonexistent=="root"')(a) False NOTE: the object ``it`` was introduced since properties (descriptors) can not be executed from within a dictionary context. """ def __init__(self, expr): self.expr = compile(expr, "", "eval") def __call__(self, element): try: return eval(self.expr, {}, {"it": element}) except (AttributeError, NameError): # attribute does not (yet) exist # print 'No attribute', expr, d return False class querymixin(object): """ Implementation of the matcher as a mixin for lists. Given a class: >>> class A(object): ... def __init__(self, name): self.name = name We can do nice things with this list: >>> class MList(querymixin, list): ... pass >>> m = MList() >>> m.append(A('one')) >>> m.append(A('two')) >>> m.append(A('three')) >>> m[1].name 'two' >>> m['it.name=="one"'] # doctest: +ELLIPSIS [] >>> m['it.name=="two"', 0].name 'two' """ def __getitem__(self, key): try: # See if the list can deal with it (don't change default behaviour) return super(querymixin, self).__getitem__(key) except TypeError: # Nope, try our matcher trick if isinstance(key, tuple): key, remainder = key[0], key[1:] else: remainder = None matcher = Matcher(key) matched = list(filter(matcher, self)) if remainder: return type(self)(matched).__getitem__(*remainder) else: return type(self)(matched) def issafeiterable(obj): """ Checks if the object is iterable, but not a string. >>> issafeiterable([]) True >>> issafeiterable(set()) True >>> issafeiterable({}) True >>> issafeiterable(1) False >>> issafeiterable('text') False """ try: return iter(obj) and not isinstance(obj, str) except TypeError: pass return False class recurseproxy(object): """ Proxy object (helper) for the recusemixin. The proxy has limited capabilities compared to a real list (or set): it can be iterated and a getitem can be performed. On the other side, the type of the original sequence is maintained, so getitem operations act as if they're executed on the original list. """ def __init__(self, sequence): self.__sequence = sequence def __getitem__(self, key): return self.__sequence.__getitem__(key) def __iter__(self): """ Iterate over the items. If there is some level of nesting, the parent items are iterated as well. """ return iter(self.__sequence) def __getattr__(self, key): """ Create a new proxy for the attribute. """ def mygetattr(): for e in self.__sequence: try: obj = getattr(e, key) if issafeiterable(obj): for i in obj: yield i else: yield obj except AttributeError: pass # Create a copy of the proxy type, inclusing a copy of the sequence type return type(self)(type(self.__sequence)(mygetattr())) class recursemixin(object): """ Mixin class for lists, sets, etc. If data is requested using ``[:]``, a ``recurseproxy`` instance is created. The basic idea is to have a class that can contain children: >>> class A(object): ... def __init__(self, name, *children): ... self.name = name ... self.children = list(children) ... def dump(self, level=0): ... print(' ' * level, self.name) ... for c in self.children: c.dump(level+1) Now if we make a (complex) structure out of it: >>> a = A('root', A('a', A('b'), A('c'), A('d')), A('e', A('one'), A('two'))) >>> a.dump() # doctest: +ELLIPSIS root a b c d e one two >>> a.children[1].name 'e' Given ``a``, I want to iterate all grand-children (b, c, d, one, two) and the structure I want to do that with is: ``a.children[:].children`` In order to do this we have to use a special list class, so we can handle our specific case. ``__getslice__`` should be overridden, so we can make it behave like a normal python object (legacy, yes...). >>> class rlist(recursemixin, getslicefix, list): ... pass >>> class A(object): ... def __init__(self, name, *children): ... self.name = name ... self.children = rlist(children) ... def dump(self, level=0): ... print(' ' * level, self.name) ... for c in self.children: c.dump(level+1) >>> a = A('root', A('a', A('b'), A('c'), A('d')), A('e', A('one'), A('two'))) >>> a.children[1].name 'e' Invoking ``a.children[:]`` should now return a recurseproxy object: >>> a.children[:] # doctest: +ELLIPSIS >>> list(a.children[:].name) # doctest: +ELLIPSIS ['a', 'e'] Now calling a child on the list will return a list of all children: >>> a.children[:].children # doctest: +ELLIPSIS >>> list(a.children[:].children) # doctest: +ELLIPSIS [, , , , ] And of course we're interested in the names: >>> a.children[:].children.name # doctest: +ELLIPSIS >>> list(a.children[:].children.name) ['b', 'c', 'd', 'one', 'two'] """ _recursemixin_trigger = slice(None, None, None) def proxy_class(self): return recurseproxy def __getitem__(self, key): if key == self._recursemixin_trigger: return self.proxy_class()(self) else: return super(recursemixin, self).__getitem__(key) class getslicefix(object): """ C-Python classes still use __getslice__. This behaviour is depricated and getitem should be called instead. """ def __getslice__(self, a, b, c=None): """ ``__getslice__`` is deprecated. Calls are redirected to ``__getitem__()``. """ if a == 0: a = None if b == sys.maxsize: b = None return self.__getitem__(slice(a, b, c)) # vim: sw=4:et:ai PK!'gaphor/misc/odict.py# from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747 class odict(dict): def __init__(self, dict=()): self._keys = [] super(odict, self).__init__(dict) def __delitem__(self, key): dict.__delitem__(self, key) self._keys.remove(key) def __setitem__(self, key, item): dict.__setitem__(self, key, item) if key not in self._keys: self._keys.append(key) def clear(self): dict.clear(self) self._keys = [] def copy(self): dict = dict.copy(self) dict._keys = self._keys[:] return dict def items(self): return list(zip(self._keys, list(self.values()))) def keys(self): return self._keys def popitem(self): try: key = self._keys[-1] except IndexError: raise KeyError("dictionary is empty") val = self[key] del self[key] return (key, val) def setdefault(self, key, failobj=None): dict.setdefault(self, key, failobj) if key not in self._keys: self._keys.append(key) def update(self, dict): dict.update(self, dict) for key in list(dict.keys()): if key not in self._keys: self._keys.append(key) def values(self): return list(map(self.get, self._keys)) def swap(self, k1, k2): """ Swap two elements using their keys. """ i1 = self._keys.index(k1) i2 = self._keys.index(k2) self._keys[i1], self._keys[i2] = self._keys[i2], self._keys[i1] def __iter__(self): for k in self._keys: yield k PK!,gaphor/misc/rattr.py"""Recursive attribute access functions.""" def rgetattr(obj, attr): """ Get named attribute from an object, i.e. getattr(obj, 'a.a') is equivalent to ``obj.a.a''. - obj: object - attr: attribute name(s) >>> class A(object): pass >>> a = A() >>> a.a = A() >>> a.a.a = 1 >>> rgetattr(a, 'a.a') 1 >>> rgetattr(a, 'a.c') Traceback (most recent call last): ... AttributeError: 'A' object has no attribute 'c' """ attrs = attr.split(".") obj = getattr(obj, attrs[0]) for name in attrs[1:]: obj = getattr(obj, name) return obj def rsetattr(obj, attr, val): """ Set named attribute value on an object, i.e. setattr(obj, 'a.a', 1) is equivalent to ``obj.a.a = 1''. - obj: object - attr: attribute name(s) - val: attribute value >>> class A(object): pass >>> a = A() >>> a.a = A() >>> a.a.a = 1 >>> rsetattr(a, 'a.a', 2) >>> print(a.a.a) 2 >>> rsetattr(a, 'a.c', 3) >>> print(a.a.c) 3 """ attrs = attr.split(".") if len(attrs) > 1: obj = getattr(obj, attrs[0]) for name in attrs[1:-1]: obj = getattr(obj, name) setattr(obj, attrs[-1], val) PK!0  #gaphor/misc/tests/test_xmlwriter.pyimport sys import unittest from gaphor.misc.xmlwriter import XMLWriter class Writer(object): def __init__(self): self.s = "" def write(self, text): self.s += text class XMLWriterTestCase(unittest.TestCase): def test_elements_1(self): w = Writer() xml_w = XMLWriter(w) xml_w.startDocument() xml_w.startElement("foo", {}) xml_w.endElement("foo") xml = ( """\n""" % sys.getdefaultencoding() ) assert w.s == xml, w.s + " != " + xml def test_elements_2(self): w = Writer() xml_w = XMLWriter(w) xml_w.startDocument() xml_w.startElement("foo", {}) xml_w.startElement("bar", {}) xml_w.endElement("bar") xml_w.endElement("foo") xml = ( """\n\n\n""" % sys.getdefaultencoding() ) assert w.s == xml, w.s def test_elements_test(self): w = Writer() xml_w = XMLWriter(w) xml_w.startDocument() xml_w.startElement("foo", {}) xml_w.startElement("bar", {}) xml_w.characters("hello") xml_w.endElement("bar") xml_w.endElement("foo") xml = ( """\n\nhello\n""" % sys.getdefaultencoding() ) assert w.s == xml, w.s def test_elements_ns_default(self): w = Writer() xml_w = XMLWriter(w) xml_w.startDocument() xml_w.startPrefixMapping(None, "http://gaphor.devjavu.com/schema") xml_w.startElementNS(("http://gaphor.devjavu.com/schema", "foo"), "qn", {}) xml_w.endElementNS(("http://gaphor.devjavu.com/schema", "foo"), "qn") xml = ( """\n""" % sys.getdefaultencoding() ) assert w.s == xml, w.s def test_elements_ns_1(self): w = Writer() xml_w = XMLWriter(w) xml_w.startDocument() xml_w.startPrefixMapping("g", "http://gaphor.devjavu.com/schema") xml_w.startElementNS(("http://gaphor.devjavu.com/schema", "foo"), "qn", {}) xml_w.endElementNS(("http://gaphor.devjavu.com/schema", "foo"), "qn") xml = ( """\n""" % sys.getdefaultencoding() ) assert w.s == xml, w.s PK!}ٹImmgaphor/misc/xmlwriter.pyimport sys import xml.sax.handler from xml.sax.saxutils import escape, quoteattr # See whether the xmlcharrefreplace error handler is # supported try: from codecs import xmlcharrefreplace_errors _error_handling = "xmlcharrefreplace" del xmlcharrefreplace_errors except ImportError: _error_handling = "strict" class XMLWriter(xml.sax.handler.ContentHandler): def __init__(self, out=None, encoding=None): if out is None: out = sys.stdout xml.sax.handler.ContentHandler.__init__(self) self._out = out self._ns_contexts = [{}] # contains uri -> prefix dicts self._current_context = self._ns_contexts[-1] self._undeclared_ns_maps = [] self._encoding = encoding or sys.getdefaultencoding() self._in_cdata = False self._in_start_tag = False self._next_newline = False def _write(self, text, start_tag=False, end_tag=False): """ Write data. Tags should not be escaped. They should be marked by setting either ``start_tag`` or ``end_tag`` to ``True``. Only the tag should be marked this way. Other stuff, such as namespaces and attributes can be written directly to the file. """ if not isinstance(text, str): text = text.decode(self._encoding, _error_handling) if self._next_newline: self._out.write(u"\n") self._next_newline = False if start_tag and not self._in_start_tag: self._in_start_tag = True self._out.write(u"<") elif start_tag and self._in_start_tag: self._out.write(u">") self._out.write(u"\n") self._out.write(u"<") elif end_tag and self._in_start_tag: self._out.write(u"/>") self._in_start_tag = False self._next_newline = True return elif not start_tag and self._in_start_tag: self._out.write(u">") self._in_start_tag = False elif end_tag: self._out.write(u"") self._in_start_tag = False self._next_newline = True return self._out.write(text) def _qname(self, name): """Builds a qualified name from a (ns_url, localname) pair""" if name[0]: # The name is in a non-empty namespace prefix = self._current_context[name[0]] if prefix: # If it is not the default namespace, prepend the prefix return prefix + ":" + name[1] # Return the unqualified name return name[1] # ContentHandler methods def startDocument(self): self._write(u'\n' % self._encoding) def startPrefixMapping(self, prefix, uri): self._ns_contexts.append(self._current_context.copy()) self._current_context[uri] = prefix self._undeclared_ns_maps.append((prefix, uri)) def endPrefixMapping(self, prefix): self._current_context = self._ns_contexts[-1] del self._ns_contexts[-1] def startElement(self, name, attrs): self._write(name, start_tag=True) for (name, value) in list(attrs.items()): self._out.write(u" %s=%s" % (name, quoteattr(value))) def endElement(self, name): self._write(name, end_tag=True) def startElementNS(self, name, qname, attrs): self._write(self._qname(name), start_tag=True) for prefix, uri in self._undeclared_ns_maps: if prefix: self._out.write(u' xmlns:%s="%s"' % (prefix, uri)) else: self._out.write(u' xmlns="%s"' % uri) self._undeclared_ns_maps = [] for (name, value) in list(attrs.items()): self._out.write(u" %s=%s" % (self._qname(name), quoteattr(value))) def endElementNS(self, name, qname): self._write(u"%s" % self._qname(name), end_tag=True) def characters(self, content): if self._in_cdata: self._write(content.replace(u"]]>", u"] ]>")) else: self._write(escape(content)) def ignorableWhitespace(self, content): self._write(content) def processingInstruction(self, target, data): self._write(u"" % (target, data)) def comment(self, comment): self._write(u"", u"- ->")) self._write(u" -->") def startCDATA(self): self._write(u"") self._in_cdata = False PK!]]gaphor/plugins/__init__.py""" Plugins ======= This module contains a bunch of standard plugins. Plugins can be registered in Gaphor by declaring them as service entry point:: entry_points = { 'gaphor.services': [ 'xmi_export = gaphor.plugins.xmiexport:XMIExport', ], }, There is a thin line between a service and a plugin. A service typically performs some basic need for the applications (such as the element factory or the undo mechanism). A plugin is more of an add-on. For example a plugin can depend on external libraries or provide a cross-over function between two applications. """ PK!n$gaphor/plugins/alignment/__init__.py""" This plugin extends Gaphor with XMI alignment actions. """ from zope import component from zope.interface import implementer from gaphor.core import inject, transactional, action, build_action_group from gaphor.interfaces import IService, IActionProvider from gaphor.ui.interfaces import IDiagramSelectionChange @implementer(IService, IActionProvider) class Alignment(object): component_registry = inject("component_registry") menu_xml = """ """ def __init__(self): self.action_group = build_action_group(self) self._last_update = None def init(self, app): self.component_registry.register_handler(self.update) self.update() def shutdown(self): self.component_registry.unregister_handler(self.update) @component.adapter(IDiagramSelectionChange) def update(self, event=None): self._last_update = event sensitive = event and len(event.selected_items) > 1 self.action_group.get_action("align-left").set_sensitive(sensitive) self.action_group.get_action("align-center").set_sensitive(sensitive) self.action_group.get_action("align-right").set_sensitive(sensitive) self.action_group.get_action("align-top").set_sensitive(sensitive) self.action_group.get_action("align-middle").set_sensitive(sensitive) self.action_group.get_action("align-bottom").set_sensitive(sensitive) def get_items(self): return (self._last_update and self._last_update.selected_items) or [] def get_focused_item(self): return self._last_update.focused_item def getXCoordsLeft(self, items): return [item.matrix[4] for item in items] def getXCoordsRight(self, items): return [item.matrix[4] + item.width for item in items] def getYCoordsTop(self, items): return [item.matrix[5] for item in items] def getYCoordsBottom(self, items): return [item.matrix[5] + item.height for item in items] @action( name="align-left", label="Left", tooltip="Vertically align diagram elements on the left", accel="l", ) @transactional def align_left(self): items = self.get_items() fitem = self.get_focused_item() target_x = fitem.matrix[4] for item in items: x = target_x - item.matrix[4] item.matrix.translate(x, 0) item.request_update() @action( name="align-center", label="Center", tooltip="Vertically align diagram elements on their centers", accel="c", ) @transactional def align_center(self): items = self.get_items() fitem = self.get_focused_item() min_x = min(self.getXCoordsLeft(items)) max_x = max(self.getXCoordsRight(items)) center_x = fitem.matrix[4] + (fitem.width / 2) for item in items: x = center_x - (item.width / 2) - item.matrix[4] item.matrix.translate(x, 0) item.request_update() @action( name="align-right", label="Right", tooltip="Vertically align diagram elements on the right", accel="r", ) @transactional def align_right(self): items = self.get_items() fitem = self.get_focused_item() target_x = fitem.matrix[4] + fitem.width for item in items: x = target_x - item.width - item.matrix[4] item.matrix.translate(x, 0) item.request_update() @action( name="align-top", label="Top", tooltip="Horizontally align diagram elements on their tops", accel="t", ) @transactional def align_top(self): items = self.get_items() fitem = self.get_focused_item() target_y = fitem.matrix[5] for item in items: y = target_y - item.matrix[5] item.matrix.translate(0, y) item.request_update() @action( name="align-middle", label="Middle", tooltip="Horizontally align diagram elements on their middles", accel="m", ) @transactional def align_middle(self): items = self.get_items() fitem = self.get_focused_item() middle_y = fitem.matrix[5] + (fitem.height / 2) for item in items: y = middle_y - (item.height / 2) - item.matrix[5] item.matrix.translate(0, y) item.request_update() @action( name="align-bottom", label="Bottom", tooltip="Horizontally align diagram elements on their bottoms", accel="b", ) @transactional def align_bottom(self): items = self.get_items() fitem = self.get_focused_item() target_y = fitem.matrix[5] + fitem.height for item in items: y = target_y - item.height - item.matrix[5] item.matrix.translate(0, y) item.request_update() PK!ydgII)gaphor/plugins/checkmetamodel/__init__.pyfrom gaphor.plugins.checkmetamodel.checkmodelgui import CheckModelWindow PK!+gaphor/plugins/checkmetamodel/checkmodel.pyfrom gaphor import UML from os import path def report(element, message): print("%s: %s" % (type(element).__name__, message)) def get_subsets(tagged_value): subsets = [] if tagged_value.find("subsets") != -1: subsets = tagged_value[tagged_value.find("subsets") + len("subsets") :] subsets = subsets.replace(" ", "").replace("\n", "").replace("\r", "") subsets = subsets.split(",") return subsets def get_redefine(tagged_value): redefines = tag[tag.find("redefines") + len("redefines") :] # remove all whitespaces and stuff redefines = redefines.replace(" ", "").replace("\n", "").replace("\r", "") redefines = redefines.split(",") assert len(redefines) == 1 return redefines[0] def get_superclasses(class_): for superclass in class_.superClass: gen = 1 def check_classes(element_factory): classes = element_factory.select(lambda e: e.isKindOf(UML.Class)) names = [c.name for c in classes] for c in classes: if names.count(c.name) > 1: report(c, "Class name %s used more than once" % c.name) def check_association_end_subsets(element_factory, end): # TODO: don't use Tagged values, use Stereotype values or something subsets = get_subsets(end.taggedValue and end.taggedValue[0].value or "") opposite_subsets = get_subsets( end.opposite.taggedValue and end.opposite.taggedValue[0].value or "" ) subset_properties = element_factory.select( lambda e: e.isKindOf(UML.Property) and e.name in subsets ) # TODO: check if properties belong to a superclass of the end's class # TODO: check if the association end is navigable when the subsets are navigable for p in subset_properties: pass # Check if bi-directional derived unions are bi-directional on this association for p in subset_properties: if p.opposite.name and p.opposite.name not in opposite_subsets: report( end, "Subset not defined on both sides. (%s, %s)" % (p.name, p.opposite.name), ) # Check if multiplicity of the properties matches the multiplicity of the subsets for p in subset_properties: if p.upperValue: if not end.upperValue: report( end, "Association end %s has no upper value, but the subset %s has" % (end.name, p.name), ) elif p.upperValue.value < end.upperValue.value: report( end, "Association end %s has has a bigger upper value than subse %s" % (end.name, p.name), ) def check_association_end(element_factory, end): check_association_end_subsets(element_factory, end) def check_associations(element_factory): for a in element_factory.select(lambda e: e.isKindOf(UML.Association)): assert len(a.memberEnd) == 2 head = a.memberEnd[0] tail = a.memberEnd[1] check_association_end(element_factory, head) check_association_end(element_factory, tail) def check_attributes(element_factory): for a in element_factory.select( lambda e: e.isKindOf(UML.Property) and not e.association ): if not a.typeValue or not a.typeValue.value: report(a, "Attribute has no type: %s" % a.name) elif a.typeValue.value.lower() not in ( "string", "boolean", "integer", "unlimitednatural", ): report(a, "Invalid attribute type: %s" % a.typeValue.value) # TODO: Check the sanity of the generated data model. def check_UML_module(): all_classes = list(map(getattr, [UML] * len(dir(UML)), dir(UML))) for c in all_classes: if not isinstance(c, UML.Element): continue # TODO: check derived unions. if __name__ == "__main__": from gaphor.UML import ElementFactory from gaphor import storage element_factory = ElementFactory() storage.load(path.join("gaphor", "UML", "uml2.gaphor"), factory=element_factory) check_associations(element_factory) check_attributes(element_factory) # vim:sw=4:et PK!N+...gaphor/plugins/checkmetamodel/checkmodelgui.py""" A GUI for the checkmodel plugin. """ import logging import gi from gi.repository import GObject from gi.repository import Gtk from zope.interface import implementer from gaphor.core import inject, action, build_action_group from gaphor.ui.diagrampage import DiagramPage from gaphor.interfaces import IService, IActionProvider from gaphor.plugins.checkmetamodel import checkmodel PYELEMENT_COLUMN = 0 ELEMENT_COLUMN = 1 REASON_COLUMN = 2 log = logging.getLogger(__name__) @implementer(IService, IActionProvider) class CheckModelWindow(object): element_factory = inject("element_factory") main_window = inject("main_window") menu_xml = """ """ def __init__(self): # Override the report method checkmodel.report = self.on_report self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass @action(name="tools-open-check-model", label="Check UML model") def open(self): self.construct() self.run() def construct(self): model = Gtk.ListStore( GObject.TYPE_PYOBJECT, GObject.TYPE_STRING, GObject.TYPE_STRING ) treeview = Gtk.TreeView(model) treeview.connect("row-activated", self.on_row_activated) selection = treeview.get_selection() selection.set_mode(Gtk.SelectionMode.SINGLE) treeview.set_size_request(200, -1) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.add(treeview) scrolled_window.show() cell = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Element", cell, text=ELEMENT_COLUMN) treeview.append_column(column) cell = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Reason", cell, text=REASON_COLUMN) treeview.append_column(column) treeview.show() # self._construct_window(name='checkmodel', # title='Check Model', # size=(400, 400), # contents=scrolled_window) self.model = model self.treeview = treeview self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) self.window.connect("destroy", self.on_destroy) self.window.set_title("Gaphor - Check Model") self.window.add(scrolled_window) self.window.set_size_request(400, 400) self.window.show() def run(self): # TODO: Let this run in a Thread(?) checkmodel.check_classes(self.element_factory) checkmodel.check_attributes(self.element_factory) checkmodel.check_associations(self.element_factory) def on_report(self, element, message): log.info("%s: %s" % (type(element).__name__, message)) model = self.model iter = model.append() model.set_value(iter, PYELEMENT_COLUMN, element) model.set_value(iter, ELEMENT_COLUMN, type(element).__name__) model.set_value(iter, REASON_COLUMN, message) main = GObject.main_context_default() main.iteration(False) def on_row_activated(self, treeview, row, column): iter = self.model.get_iter(row) element = self.model.get_value(iter, PYELEMENT_COLUMN) print("Looking for element", element) if element.presentation: presentation = element.presentation[0] try: diagram = presentation.canvas.diagram except AttributeError: presentation = element.namespace.presentation[0] diagram = presentation.canvas.diagram diagram_page = DiagramPage(diagram) diagram_page.view.focused_item = presentation def on_destroy(self, window): self.window = None self.treeview = None # vim:sw=4:et PK!(gaphor/plugins/diagramlayout/__init__.py""" This module provides a means to automatically layout diagrams. The layout is done like this: - First all nodes (Classes, packages, comments) on a diagram are determined - A vertical ordering is determined based on the inheritance - A horizontal ordering is determined based on associations and dependencies - The nodes are moved to their place - Lines are reconnected to the nodes, so everything looks pretty. """ import logging import random from zope.interface import implementer from gaphor.core import inject, action, build_action_group, transactional from gaphor.diagram import items from gaphor.interfaces import IService, IActionProvider from gaphor.plugins.diagramlayout import toposort from gaphor.ui.interfaces import IUIComponent log = logging.getLogger(__name__) @implementer(IService, IActionProvider) class DiagramLayout(object): component_registry = inject("component_registry") main_window = inject("main_window") menu_xml = """ """ def __init__(self): self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass def get_current_diagram(self): return self.component_registry.get_utility( IUIComponent, "diagrams" ).get_current_diagram() def update(self): self.sensitive = bool(self.get_current_diagram()) @action( name="diagram-layout", label="La_yout diagram", tooltip="simple diagram layout" ) def execute(self): d = self.get_current_diagram() self.layout_diagram(d) @transactional def layout_diagram(self, diag): layout_diagram(diag) MARGIN = 100 def layout_diagram(diag): """ So an attempt to layout (order) the items on a diagram. The items should already be placed on the diagram and the items should already be connected. This function works on the diagram items (hence it does not check relations in the datamodel, only the ones drawn on the diagram) to produce a decent layout. """ nodes = [] primary_nodes = [] relations = [] other_relations = [] # Make sure all items are updated diag.canvas.update_now() # First extract data from the diagram (which ones are the nodes, and # the relationships). for item in diag.canvas.get_root_items(): if isinstance(item, (items.GeneralizationItem, items.ImplementationItem)): # Primary relationships, should be drawn top-down try: relations.append( (item.handles[0].connected_to, item.handles[-1].connected_to) ) primary_nodes.extend(relations[-1]) except Exception as e: log.error(e) elif isinstance(item, items.DiagramLine): # Secondary (associations, dependencies) may be drawn top-down # or left-right try: other_relations.append( (item.handles[0].connected_to, item.handles[-1].connected_to) ) # other_relations.append((item.handles[-1].connected_to, # item.handles[0].connected_to)) except Exception as e: log.error(e) else: nodes.append(item) # Add some randomness: random.shuffle(other_relations) primary_nodes = uniq(primary_nodes) # Find out our horizontal and vertical sorting sorted = toposort.toposort(nodes, relations, 0) other_sorted = toposort.toposort(nodes, other_relations, 0) if not sorted: return # Move nodes from the first (generalization) row to the other rows # if they are not superclasses for some other class # Run the list twice, just to ensure no items are left behind. for item in list(sorted[0]) * 2: if item not in primary_nodes and item in sorted[0]: # Find nodes that do have a relation to this one related = find_related_nodes(item, other_relations) # Figure out what row(s) they're on row = find_row(item, related, sorted[1:]) if row: # print 'moving', item.subject.name, 'to row', sorted.index(row) sorted[0].remove(item) row.append(item) # Order each row based on the sort order of other_sorted # (the secondary sort alg.). for row in sorted: for other_row in other_sorted: for other_item in other_row: if other_item in row: row.remove(other_item) row.append(other_item) # Place the nodes on the diagram. y = MARGIN / 2 for row in sorted: x = MARGIN / 2 maxy = 0 for item in row: if not item: continue maxy = max(maxy, item.height) a = item.matrix a = (a[0], a[1], a[2], a[3], x, y) item.matrix = a item.request_update() x += item.width + MARGIN y += maxy + MARGIN # Reattach the relationships to the nodes, in a way that it looks nice. simple_layout_lines(diag) def simple_layout_lines(diag): """ Just do the layout of the lines in a diagram. The nodes (class, package) are left where they are (use layout_diagram() if you want to reorder everything). The line layout is basically very simple: just draw straight lines between nodes on the diagram. """ lines = {} for item in diag.canvas.get_root_items(): if isinstance(item, items.DiagramLine): # Secondary (associations, dependencies) may be drawn top-down # or left-right try: lines[item] = ( item.handles[0].connected_to, item.handles[-1].connected_to, ) except Exception as e: log.error(e) # Now we have the lines, let's first ensure we only have a begin and an # end handle for line in list(lines.keys()): while len(line.handles) > 2: line.set_property("del_segment", 0) # Strategy 1: # Now we have nice short lines. Let's move them to a point between # both nodes and let the connect() do the work: for line, nodes in list(lines.items()): if not nodes[0] or not nodes[1]: # loose end continue center0 = find_center(nodes[0]) center1 = find_center(nodes[1]) center = ((center0[0] + center1[0]) / 2.0, (center0[1] + center1[1]) / 2.0) line.handles[0].set_pos_w(*center) line.handles[-1].set_pos_w(*center) nodes[0].connect_handle(line.handles[0]) nodes[1].connect_handle(line.handles[-1]) def uniq(lst): d = {} for l in lst: d[l] = None return list(d.keys()) def find_related_nodes(item, relations): """ Find related nodes of item, given a list of tuples. References to itself are ignored. """ related = [] for pair in relations: if pair[0] is item: if pair[1] is not item: related.append(pair[1]) elif pair[1] is item: if pair[0] is not item: related.append(pair[0]) return uniq(related) def find_row(item, related_items, sorted): """ Find the row that contains the most references to item. """ max_refs = 0 max_row = None for row in sorted: cnt = len([i for i in row if i in related_items]) if cnt > max_refs: max_row = row max_refs = cnt return max_row def find_center(item): """ Find the center point of the item, in world coordinates """ x = item.width / 2.0 y = item.height / 2.0 return item.canvas.get_matrix_i2c(item).transform_point(x, y) # vim:sw=4:et PK!.gaphor/plugins/diagramlayout/tests/__init__.pyPK!\8gaphor/plugins/diagramlayout/tests/test_diagramlayout.pyimport unittest from gaphor import UML from gaphor.diagram import items from gaphor.plugins.diagramlayout import DiagramLayout from gaphor.application import Application from gaphor.tests.testcase import TestCase class DiagramLayoutTestCase(TestCase): services = TestCase.services + [ "main_window", "ui_manager", "properties", "action_manager", "diagram_layout", ] def testDiagramLayout(self): elemfact = Application.get_service("element_factory") diagram_layout = Application.get_service("diagram_layout") diagram = elemfact.create(UML.Diagram) c1 = diagram.create(items.ClassItem, subject=elemfact.create(UML.Class)) c2 = diagram.create(items.ClassItem, subject=elemfact.create(UML.Class)) c2.matrix.translate(100, 100) c2.request_update() diagram_layout.layout_diagram(diagram) PK!(gaphor/plugins/diagramlayout/toposort.pyclass RecursionError(OverflowError, ValueError): """Unable to calculate result because of recursive structure""" def sort(nodes, routes, noRecursion=1): """Passed a list of node IDs and a list of source,dest ID routes attempt to create a list of stages where each sub list is one stage in a process. """ children, parents = _buildChildrenLists(routes) # first stage is those nodes # having no incoming routes... stage = [] stages = [stage] taken = [] for node in nodes: if not parents.get(node): stage.append(node) if nodes and not stage: # there is no element which does not depend on # some other element!!! stage.append(nodes[0]) taken.extend(stage) nodes = list(filter(lambda x, l=stage: x not in l, nodes)) while nodes: previousStageChildren = [] nodelen = len(nodes) # second stage are those nodes # which are direct children of the first stage for node in stage: for child in children.get(node, []): if child not in previousStageChildren and child not in taken: previousStageChildren.append(child) elif child in taken and noRecursion: raise RecursionError((child, node)) # unless they are children of other direct children... # TODO, actually do that... stage = previousStageChildren removes = [] for current in stage: currentParents = parents.get(current, []) for parent in currentParents: if parent in stage and parent != current: # might wind up removing current... if not current in parents.get(parent, []): # is not mutually dependent... removes.append(current) for remove in removes: while remove in stage: stage.remove(remove) stages.append(stage) taken.extend(stage) nodes = list(filter(lambda x, l=stage: x not in l, nodes)) if nodelen == len(nodes): if noRecursion: raise RecursionError(nodes) else: stages.append(nodes[:]) nodes = [] return stages def _buildChildrenLists(routes): childrenTable = {} parentTable = {} for sourceID, destinationID in routes: currentChildren = childrenTable.get(sourceID, []) currentParents = parentTable.get(destinationID, []) if not destinationID in currentChildren: currentChildren.append(destinationID) if not sourceID in currentParents: currentParents.append(sourceID) childrenTable[sourceID] = currentChildren parentTable[destinationID] = currentParents return childrenTable, parentTable def toposort(nodes, routes, noRecursion=1): """Topological sort from Tim Peters, fairly efficient in comparison (it seems).""" # first calculate the recursion depth dependencies = {} inversedependencies = {} if not nodes: return [] if not routes: return [nodes] for node in nodes: dependencies[node] = (0, node) inversedependencies[node] = [] for depended, depends in routes: # is it a null rule try: newdependencylevel, object = dependencies.get(depends, (0, depends)) except TypeError: print(depends) raise dependencies[depends] = (newdependencylevel + 1, depends) # "dependency (existence) of depended-on" newdependencylevel, object = dependencies.get(depended, (0, depended)) dependencies[depended] = (newdependencylevel, depended) # Inverse dependency set up dependencieslist = inversedependencies.get(depended, []) dependencieslist.append(depends) inversedependencies[depended] = dependencieslist ### Now we do the actual sorting # The first task is to create the sortable # list of dependency-levels sortinglist = sorted(dependencies.values()) output = [] while sortinglist: deletelist = [] generation = [] output.append(generation) while sortinglist and sortinglist[0][0] == 0: number, object = sortinglist[0] generation.append(object) deletelist.append(object) for inverse in inversedependencies.get(object, ()): try: oldcount, inverse = dependencies[inverse] if oldcount > 0: # will be dealt with on later pass dependencies[inverse] = (oldcount - 1, inverse) else: # will be dealt with on this pass, # so needs not to be in the sorting list next time deletelist.append(inverse) # just in case a loop comes through inversedependencies[object] = [] except KeyError: # dealing with a recursion-breaking run... pass del sortinglist[0] # if no elements could be deleted, then # there is something which depends upon itself if not deletelist: if noRecursion: raise RecursionError(sortinglist) else: # hack so that something gets deleted... ## import pdb ## pdb.set_trace() dependencies[sortinglist[0][1]] = (0, sortinglist[0][1]) # delete the items that were dealt with for item in deletelist: try: del dependencies[item] except KeyError: pass # need to recreate the sortinglist sortinglist = list(dependencies.values()) if not generation: output.remove(generation) sortinglist.sort() return output if __name__ == "__main__": import pprint, traceback nodes = [0, 1, 2, 3, 4, 5] testingValues = [ [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], [(0, 1), (0, 2), (1, 2), (3, 4), (4, 5)], [(0, 1), (0, 2), (0, 2), (2, 4), (2, 5), (3, 2), (0, 3)], [ (0, 1), # 3-element cycle test, no orphan nodes (1, 2), (2, 0), (2, 4), (2, 5), (3, 2), (0, 3), ], [(0, 1), (1, 1), (1, 1), (1, 4), (1, 5), (1, 2), (3, 1), (2, 1), (2, 0)], [(0, 1), (1, 0), (0, 2), (0, 3)], [(0, 1), (1, 0), (0, 2), (3, 1)], ] print("sort, no recursion allowed") for index in range(len(testingValues)): ## print ' %s -- %s'%( index, testingValues[index]) try: print(" ", sort(nodes, testingValues[index])) except: print("exception raised") print("toposort, no recursion allowed") for index in range(len(testingValues)): ## print ' %s -- %s'%( index, testingValues[index]) try: print(" ", toposort(nodes, testingValues[index])) except: print("exception raised") print("sort, recursion allowed") for index in range(len(testingValues)): ## print ' %s -- %s'%( index, testingValues[index]) try: print(" ", sort(nodes, testingValues[index], 0)) except: print("exception raised") print("toposort, recursion allowed") for index in range(len(testingValues)): ## print ' %s -- %s'%( index, testingValues[index]) try: print(" ", toposort(nodes, testingValues[index], 0)) except: print("exception raised") PK!ZB;;,gaphor/plugins/liveobjectbrowser/__init__.py""" Plugin based on the Live Object browser (http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/300304). It shows the state of the data model at the time the browser is activated. """ from zope.interface import implementer from gaphor.core import inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider from gaphor.plugins.liveobjectbrowser.browser import Browser @implementer(IService, IActionProvider) class LiveObjectBrowser(object): element_factory = inject("element_factory") menu_xml = """ """ def __init__(self): self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass @action(name="tools-life-object-browser", label="Life object browser") def execute(self): browser = Browser("resource", self.element_factory.lselect()) # vim:sw=4:et PK!P7+gaphor/plugins/liveobjectbrowser/browser.py#!/usr/bin/env python # vim:sw=4:et: """ Title: Live Object Browser Submitter: Simon Burton (other recipes) Last Updated: 2004/08/18 Version no: 1.0 Category: Debugging Description: Given an object, this tool throws up a gtk tree widget that maps all the references found. It dynamically builds the tree, which means it can handle large amounts of data and circular references. """ from gi.repository import Gtk class Browser(object): def make_row(self, piter, name, value): info = repr(value) if not hasattr(value, "__dict__"): if len(info) > 80: # it's a big list, or dict etc. info = info[:80] + "..." _piter = self.treestore.append(piter, [name, type(value).__name__, info]) return _piter def make_instance(self, value, piter): if hasattr(value, "__dict__"): for _name, _value in list(value.__dict__.items()): _piter = self.make_row(piter, "." + _name, _value) _path = self.treestore.get_path(_piter) self.otank[_path] = (_name, _value) def make_mapping(self, value, piter): keys = [] if hasattr(value, "keys"): keys = list(value.keys()) elif hasattr(value, "__len__"): keys = list(range(len(value))) for key in keys: _name = "[%s]" % str(key) _piter = self.make_row(piter, _name, value[key]) _path = self.treestore.get_path(_piter) self.otank[_path] = (_name, value[key]) def make(self, name=None, value=None, path=None, depth=1): if path is None: # make root node piter = self.make_row(None, name, value) path = self.treestore.get_path(piter) self.otank[path] = (name, value) else: name, value = self.otank[path] piter = self.treestore.get_iter(path) if not self.treestore.iter_has_child(piter): self.make_mapping(value, piter) self.make_instance(value, piter) if depth: for i in range(self.treestore.iter_n_children(piter)): self.make(path=path + (i,), depth=depth - 1) def row_expanded(self, treeview, piter, path): self.make(path=path) def delete_event(self, widget, event, data=None): self.window.destroy() # Gtk.main_quit() return False def __init__(self, name, value): self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) self.window.set_title("Browser") self.window.set_size_request(512, 320) self.window.connect("delete_event", self.delete_event) # we will store the name, the type name, and the repr columns = [str, str, str] self.treestore = Gtk.TreeStore(*columns) # the otank tells us what object we put at each node in the tree self.otank = {} # map path -> (name,value) self.make(name, value) self.treeview = Gtk.TreeView(self.treestore) self.treeview.connect("row-expanded", self.row_expanded) self.tvcolumns = [Gtk.TreeViewColumn() for _type in columns] i = 0 for tvcolumn in self.tvcolumns: self.treeview.append_column(tvcolumn) cell = Gtk.CellRendererText() tvcolumn.pack_start(cell, True) tvcolumn.add_attribute(cell, "text", i) i = i + 1 scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_shadow_type(Gtk.ShadowType.ETCHED_IN) scrolled_window.add(self.treeview) self.window.add(scrolled_window) self.window.show_all() def dump(name, value): browser = Browser(name, value) Gtk.main() def test(): class Nil(object): pass a = Nil() b = Nil() c = Nil() d = Nil() a.b = b b.c = c c.d = d d.a = a # circular chain dump("a", a) if __name__ == "__main__": test() PK!@9Koo#gaphor/plugins/pynsource/Readme.txtPyNSource http://www.atug.com/andypatterns/pynsource.htm (c) 2003, 2004, 2005, 2006 Andy Bulka License: Free to use as long as acknowledgement is made in source code. abulka@netspace.net.au http://www.atug.com/andypatterns Version 1.4c - Fixed some parsing bugs. - Parsing now more correct under python 2.4 (python changed token.py !!) - Unit tests now all pass Version 1.4b - Added wxpython 2.5 compatibility (thanks Thomas Margraf!) Version 1.4a GUI changes: - Right Click on a node to delete it. - Run Layout anytime from menu. - Left click on background will deselect any selected shapes Version 1.4 - Fixed indentation error causing more output than normal in text ouput - Module level functions not treated as classes any more - Smarter detection of composition relationships, as long as classname and variable name are the same (ignoring case) then PyNSource will detect e.g. class Cat: pass class A: def __init__(self, cat): self.cats.append(Cat()) # always has worked, composition detected. self.cats.append(cat) # new 1.4 feature, composition detected here too. Version 1.3a A reverse engineering tool for Python source code - UML diagram models that you can layout, arrange and print out. - UML text diagrams, which you can paste into your source code for documentation purposes. - Java or Delphi code (which can be subsequently imported into more sophisticated UML modeling tools, like Enterprise Architect or ESS-Model (free).) Features - Resilient: doesn't import the python files, thus will never get "stuck" when syntax is wrong.� - Fast - Recognises inheritance and composition relationships - Detects the cardinality of associations e.g. one to one or 1..* etc - Optionally treat modules as classes - creating a pseudo class for each module - module variables and functions are treated as attributes and methods of a class - Has been developed using unit tests (supplied) so that you can trust it just that little bit more ;-) GUI FRONT END ------------- The PyNSource Gui is started in two ways: * By running the standalone pynsourceGui.exe via the shortcut created by the standalone installer. or * By running pynsourceGui.py ( you need wxpython installed. See http://www.wxpython.org ) e.g. \Python22\python.exe \Python22\Lib\site-packages\pynsource\pyNsourceGui.py The PyNSource command line tool is pynsource.py Command line Usage ------------------ �pynsource -v -m [-j outdir] sourceDirOrListOfPythonFiles...��� no options - generate Ascii UML -j generate java files, specify output folder for java files -d generate pascal files, specify output folder for pascal files -v verbose -m create psuedo class for each module, module attrs/defs etc treated as class attrs/defs Examples e.g. \python22\python.exe \Python22\Lib\site-packages\pynsource\pynsource.py -d c:\delphiouputdir c:\pythoninputdir\*.py The above line will scan all the files in c:\pythoninputdir and generate a bunch of delphi files in the folder c:\delphiouputdir BASIC ASCII UML OUTPUT from PYTHON - EXAMPLES e.g. pynsource Test/testmodule01.py e.g. pynsource -m Test/testmodule03.py GENERATE JAVA FILES from PYTHON - EXAMPLES e.g. pynsource -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try/s*.py e.g. pynsource -j c:/try c:/try/s*.py Tests/u*.py e.g. pynsource -v -m -j c:/try c:/try/s*.py Tests/u*.py c:\cc\Devel\Client\w*.py GENERATE DELPHI FILES from PYTHON - EXAMPLE e.g. pynsource -d c:/delphiouputdir c:/pythoninputdir/*.py see http://www.atug.com/andypatterns/pynsource.htm for more documentation. Bugs to abulka@netspace.net.au PK!!$gaphor/plugins/pynsource/__init__.py""" Code reverse engineer plugin for Python source code. This plugin uses PyNSource, written by Andy Bulka [http://www.atug.com/andypatterns/pynsource.htm]. Depends on the Diagram Layout plugin. """ from gi.repository import GObject from gi.repository import Gtk from zope.interface import implementer from gaphor.core import inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider from gaphor.plugins.pynsource.engineer import Engineer NAME_COLUMN = 0 @implementer(IService, IActionProvider) class PyNSource(object): main_window = inject("main_window") menu_xml = """ """ def __init__(self): self.win = None self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass @action( name="file-import-pynsource", label="Python source code", tooltip="Import Python source code", ) def execute(self): dialog = self.create_dialog() response = dialog.run() if response != Gtk.ResponseType.OK: dialog.destroy() self.reset() return files = [] for row in self.filelist: files.append(row[0]) dialog.destroy() self.process(files) self.reset() def process(self, files): """Create a diagram based on a list of files. """ engineer = Engineer() engineer.process(files) main_window = self.main_window # Open and select the new diagram in the main window: main_window.select_element(engineer.diagram) main_window.show_diagram(engineer.diagram) def create_dialog(self): dialog = Gtk.Dialog( "Import Python files", self.main_window.window, Gtk.DialogFlags.MODAL, ( Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, ), ) dialog.set_default_response(Gtk.ResponseType.OK) filelist = Gtk.ListStore(GObject.TYPE_STRING) filelist.connect("row-inserted", self.on_view_rows_changed) filelist.connect("row-deleted", self.on_view_rows_changed) hbox = Gtk.HBox() frame = Gtk.Frame.new("Files to reverse-engineer") frame.set_border_width(8) frame.set_size_request(500, 300) frame.show() hbox.pack_start(frame, True, True, 0) treeview = Gtk.TreeView(filelist) treeview.set_property("headers-visible", False) selection = treeview.get_selection() selection.set_mode(Gtk.SelectionMode.SINGLE) treeview.set_size_request(200, -1) treeview.connect_after("cursor_changed", self.on_view_cursor_changed) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.set_border_width(4) scrolled_window.add(treeview) frame.add(scrolled_window) scrolled_window.show() cell = Gtk.CellRendererText() column = Gtk.TreeViewColumn("Filename", cell, text=NAME_COLUMN) treeview.append_column(column) bbox = Gtk.VButtonBox() bbox.set_layout(Gtk.ButtonBoxStyle.SPREAD) bbox.set_border_width(10) button = Gtk.Button(stock="gtk-add") button.connect("clicked", self.on_add_dir_clicked) bbox.add(button) self.add_button = button # button = Gtk.Button('Add dir...') # button.connect('clicked', self.on_add_dir_clicked) # bbox.add(button) # self.add_dir_button = button button = Gtk.Button(stock="gtk-remove") button.connect("clicked", self.on_remove_clicked) button.set_property("sensitive", False) bbox.add(button) self.remove_button = button # button = Gtk.Button(stock='gtk-execute') # button.connect('clicked', self.on_execute_clicked) # button.set_property('sensitive', False) # bbox.add(button) # self.execute_button = button hbox.pack_start(bbox, False, True, 0) hbox.show_all() dialog.vbox.pack_start(hbox, True, True, 0) self.filelist = filelist self.treeview = treeview return dialog def reset(self): self.add_button = None self.add_dir_button = None self.remove_button = None self.treeview = None self.filelist = None def Walk(self, root, recurse=0, pattern="*", return_folders=0): import fnmatch import os import string # initialize result = [] # must have at least root folder try: names = os.listdir(root) except os.error: return result # expand pattern pattern = pattern or "*" pat_list = string.splitfields(pattern, ";") # check each file for name in names: fullname = os.path.normpath(os.path.join(root, name)) # grab if it matches our pattern and entry type for pat in pat_list: if fnmatch.fnmatch(name, pat): if os.path.isfile(fullname) or ( return_folders and os.path.isdir(fullname) ): result.append(fullname) continue # recursively scan other folders, appending results if recurse: if os.path.isdir(fullname) and not os.path.islink(fullname): result = result + self.Walk( fullname, recurse, pattern, return_folders ) return result def on_view_cursor_changed(self, view): selection = view.get_selection() filelist, iter = selection.get_selected() if not iter: self.remove_button.set_property("sensitive", False) return # element = filelist.get_value(iter, NAME_COLUMN) self.remove_button.set_property("sensitive", True) # self.update_detail(element) def on_view_rows_changed(self, view, *args): iter = None try: iter = view.get_iter("0") except ValueError: pass # self.execute_button.set_property('sensitive', bool(iter)) def on_add_dir_clicked(self, button): import os filesel = Gtk.FileChooserNative( title="Add Source Code", action=Gtk.FileChooserAction.OPEN ) filesel.set_select_multiple(True) filesel.set_filename("~/") response = filesel.run() selection = filesel.get_filenames() filesel.destroy() if response == Gtk.ResponseType.ACCEPT: for filename in selection: if os.path.isdir(filename): list = self.Walk(filename, 1, "*.py", 1) for file in list: iter = self.filelist.append() self.filelist.set_value(iter, NAME_COLUMN, file) else: list = filename iter = self.filelist.append() self.filelist.set_value(iter, NAME_COLUMN, list) def on_remove_clicked(self, button): selection = self.treeview.get_selection() filelist, iter = selection.get_selected() if not iter: return element = filelist.remove(iter) self.remove_button.set_property("sensitive", False) PK!~X&&$gaphor/plugins/pynsource/engineer.py"""The code reverse engineer. """ from zope import component from gaphas.aspect import ConnectionSink, Connector from gaphor import UML from gaphor.core import inject from gaphor.diagram import items from gaphor.diagram.interfaces import IConnect from gaphor.plugins.pynsource.pynsource import PySourceAsText BASE_CLASSES = ("object", "type", "dict", "list", "tuple", "int", "float") class Engineer(object): """ The Engineer class will create a Gaphor model based on a list of Python files. """ element_factory = inject("element_factory") diagram_layout = inject("diagram_layout") def process(self, files=None): # these are tuples between class names. # self.associations_generalisation = [] # self.associations_composition = [] p = PySourceAsText() self.parser = p if files: # u = PythonToJava(None, treatmoduleasclass=0, verbose=0) for f in files: # Build a shape with all attrs and methods, and prepare association dict p.Parse(f) print(p) try: self._root_package = self.element_factory.lselect( lambda e: isinstance(e, UML.Package) and not e.namespace )[0] except IndexError: pass # running as test? for m in p.modulemethods: print("ModuleMethod:", m) # Step 0: create a diagram to put the newly created elements on self.diagram = self.element_factory.create(UML.Diagram) self.diagram.name = "New classes" self.diagram.package = self._root_package # Step 1: create the classes for name, clazz in list(p.classlist.items()): print(type(clazz), dir(clazz)) self._create_class(clazz, name) # Create generalization relationships: for name, clazz in list(p.classlist.items()): self._create_generalization(clazz) # Create attributes (and associations) on the classes for name, clazz in list(p.classlist.items()): self._create_attributes(clazz) # Create operations for name, clazz in list(p.classlist.items()): self._create_methods(clazz) self.diagram_layout.layout_diagram(self.diagram) def _create_class(self, clazz, name): c = self.element_factory.create(UML.Class) c.name = name c.package = self.diagram.namespace ci = self.diagram.create(items.ClassItem) ci.subject = c clazz.gaphor_class = c clazz.gaphor_class_item = ci def _create_generalization(self, clazz): if not clazz.ismodulenotrealclass: for superclassname in clazz.classesinheritsfrom: if superclassname in BASE_CLASSES: continue try: superclass = self.parser.classlist[superclassname].gaphor_class superclass_item = self.parser.classlist[ superclassname ].gaphor_class_item except KeyError as e: print("No class found named", superclassname) others = self.element_factory.lselect( lambda e: isinstance(e, UML.Class) and e.name == superclassname ) if others: superclass = others[0] print("Found class in factory: %s" % superclass.name) superclass_item = self.diagram.create(items.ClassItem) superclass_item.subject = superclass else: continue # Finally, create the generalization relationship print("Creating Generalization for %s" % clazz, superclass) # gen = self.element_factory.create(UML.Generalization) # gen.general = superclass # gen.specific = clazz.gaphor_class geni = self.diagram.create(items.GeneralizationItem) # geni.subject = gen self.connect(geni, geni.tail, clazz.gaphor_class_item) self.connect(geni, geni.head, superclass_item) # adapter = component.queryMultiAdapter((superclass_item, geni), IConnect) # assert adapter # handle = geni.handles()[0] # adapter.connect(handle) # clazz.gaphor_class_item.connect_handle(geni.handles[-1]) # adapter = component.queryMultiAdapter((clazz.gaphor_class_item, geni), IConnect) # assert adapter # handle = geni.handles()[-1] # adapter.connect(handle) def connect(self, line, handle, item, port=None): """ Connect line's handle to an item. If port is not provided, then first port is used. """ canvas = line.canvas if port is None and len(item.ports()) > 0: port = item.ports()[0] sink = ConnectionSink(item, port) connector = Connector(line, handle) connector.connect(sink) def _create_attributes(self, clazz): for attrobj in clazz.attrs: # TODO: Check object type and figure out if it should be an # attribute or an association. self._create_attribute(clazz, attrobj) def _create_methods(self, clazz): for adef in clazz.defs: op = self.element_factory.create(UML.Operation) op.name = adef clazz.gaphor_class.ownedOperation = op def _find_class_by_name(self, classname): try: superclass = self.parser.classlist[classname].gaphor_class superclass_item = self.parser.classlist[classname].gaphor_class_item except KeyError as e: print("No class found named", classname) others = self.element_factory.lselect( lambda e: isinstance(e, UML.Class) and e.name == classname ) if others: superclass = others[0] print("Found class in factory: %s" % superclass.name) superclass_item = self.diagram.create(items.ClassItem) superclass_item.subject = superclass else: return None, None return superclass, superclass_item def _visibility(self, attrname): if attrname.startswith("__"): return "private" elif attrname.startswith("_"): return "protected" return "public" def _create_attribute(self, clazz, attr): static = False many = False if "static" in attr.attrtype: static = True if "many" in attr.attrtype: many = True compositescreated = self.parser.GetCompositeClassesForAttr(attr.attrname, clazz) tail_type = None if compositescreated: tail_type, tail_type_item = self._find_class_by_name(compositescreated[0]) if tail_type: # Create an association: # print "%s %s <@>----> %s" % (attr.attrname, static, str(compositescreated)) # The property on the tail of the association (tail_end) is owned # by the class connected on the head_end (head_type) head_type = clazz.gaphor_class head_type_item = clazz.gaphor_class_item # relation = self.element_factory.create(UML.Association) # head_end = self.element_factory.create(UML.Property) # head_end.lowerValue = self.element_factory.create(UML.LiteralSpecification) # tail_end = self.element_factory.create(UML.Property) # tail_end.name = attr.attrname # tail_end.visibility = self._visibility(attr.attrname) # tail_end.aggregation = 'composite' # tail_end.lowerValue = self.element_factory.create(UML.LiteralSpecification) # relation.package = self.diagram.namespace # relation.memberEnd = head_end # relation.memberEnd = tail_end # head_end.type = head_type # tail_end.type = tail_type # head_type.ownedAttribute = tail_end # tail_type.ownedAttribute = head_end # Now the subject # association.subject = relation # association.head_end.subject = head_end # association.tail_end.subject = tail_end # Create the diagram item: association = self.diagram.create(items.AssociationItem) adapter = component.queryMultiAdapter( (head_type_item, association), IConnect ) assert adapter handle = association.handles()[0] adapter.connect(handle) adapter = component.queryMultiAdapter( (tail_type_item, association), IConnect ) assert adapter handle = association.handles()[-1] adapter.connect(handle) # Apply attribute information to the association (ends) association.head_end.navigability = False tail_prop = association.tail_end.subject tail_prop.name = attr.attrname tail_prop.visibility = self._visibility(attr.attrname) tail_prop.aggregation = "composite" else: # Create a simple attribute: # print "%s %s" % (attr.attrname, static) prop = self.element_factory.create(UML.Property) prop.name = attr.attrname prop.visibility = self._visibility(attr.attrname) prop.isStatic = static clazz.gaphor_class.ownedAttribute = prop # print many import pprint pprint.pprint(attr) # print dir(attr) PK!X$gaphor/plugins/pynsource/keywords.py"""Definitions of Python, Java and Delphi keywords. The definitions are used so that pynsource can skip these and not treat them like you are creating an instance of a locally defined class. """ pythonbuiltinfunctions_txt = """ ArithmeticError AssertionError AttributeError DeprecationWarning EOFError EnvironmentError Exception FloatingPointError FutureWarning IOError ImportError IndentationError IndexError KeyError KeyboardInterrupt LookupError MemoryError NameError NotImplementedError OSError OverflowError OverflowWarning PendingDeprecationWarning ReferenceError RuntimeError RuntimeWarning StandardError StopIteration SyntaxError SyntaxWarning SystemError SystemExit TabError TypeError UnboundLocalError UnicodeError UserWarning ValueError Warning WindowsError ZeroDivisionError Ellipsis False None NotImplemented True UnicodeDecodeError UnicodeEncodeError UnicodeTranslateError __debug__ __import__ abs apply basestring bool buffer callable chr classmethod cmp coerce compile complex copyright credits delattr dict dir divmod enumerate eval execfile exit file filter float getattr globals hasattr hash help hex id input int intern isinstance issubclass iter len license list locals long map max min object oct open ord pow property quit range raw_input reduce reload repr round setattr slice staticmethod str sum super tuple type unichr unicode vars xrange zip __base__ __bases__ __basicsize__ __class__ __dict__ __dictoffset__ __doc__ __flags__ __itemsize__ __module__ __mro__ __name__ __self__ __weakrefoffset__ __abs__ __add__ __and__ __call__ __cmp__ __coerce__ __complex__ __contains__ __del__ __delattr__ __delitem__ __delslice__ __div__ __divmod__ __eq__ __float__ __floordiv__ __ge__ __get__ __getattribute__ __getitem__ __getnewargs__ __getslice__ __gt__ __hash__ __hex__ __iadd__ __iand__ __idiv__ __ifloordiv__ __ilshift__ __imod__ __imul__ __init__ __int__ __invert__ __ior__ __ipow__ __irshift__ __isub__ __iter__ __itruediv__ __ixor__ __le__ __len__ __long__ __lshift__ __lt__ __mod__ __mul__ __ne__ __neg__ __new__ __nonzero__ __oct__ __or__ __pos__ __pow__ __radd__ __rand__ __rdiv__ __rdivmod__ __reduce__ __reduce_ex__ __repr__ __rfloordiv__ __rlshift__ __rmod__ __rmul__ __ror__ __rpow__ __rrshift__ __rshift__ __rsub__ __rtruediv__ __rxor__ __setattr__ __setitem__ __setslice__ __str__ __sub__ __subclasses__ __truediv__ __xor__ append capitalize center clear close conjugate copy count decode encode endswith expandtabs extend fileno find flush fromkeys get has_key index indices insert isalnum isalpha isatty isdecimal isdigit islower isnumeric isspace istitle isupper items iteritems iterkeys itervalues join keys ljust lower lstrip mro next pop popitem read readinto readline readlines remove replace reverse rfind rindex rjust rstrip seek setdefault sort split splitlines startswith strip swapcase tell title translate truncate update upper values write writelines xreadlines zfill closed co_argcount co_cellvars co_code co_consts co_filename co_firstlineno co_flags co_freevars co_lnotab co_name co_names co_nlocals co_stacksize co_varnames f_back f_builtins f_code f_exc_traceback f_exc_type f_exc_value f_globals f_lasti f_lineno f_locals f_restricted f_trace func_closure func_code func_defaults func_dict func_doc func_globals func_name gi_frame gi_running im_class im_func im_self imag mode name newlines real softspace start step stop BooleanType BufferType BuiltinFunctionType BuiltinMethodType ClassType CodeType ComplexType DictProxyType DictType DictionaryType EllipsisType FileType FloatType FrameType FunctionType GeneratorType InstanceType IntType LambdaType ListType LongType MethodType ModuleType NoneType NotImplementedType ObjectType SliceType StringType StringTypes TracebackType TupleType TypeType UnboundMethodType UnicodeType XRangeType __builtins__ __file__ """ pythonbuiltinfunctions = pythonbuiltinfunctions_txt.split() javakeywords_txt = """ abstract boolean break byte case catch char class continue default delegate do double else extends false final finally float for if implements import instanceof int interface long native new null package private protected public return short static super switch synchronized this throw throws transient true try void volatile while goto const strictfp """ javakeywords = javakeywords_txt.split() delphikeywords_txt = """ And Array As Begin Case Class Const Constructor Destructor Div Do DownTo Else End Except File Finally For System Goto If Implementation In Inherited Interface System Is Mod Not System Of On Or Packed System System System Raise Record Repeat Set Shl Shr Then ThreadVar To Try Type Unit Until Uses Var While With Xor """ delphikeywords = delphikeywords_txt.split() delphikeywords = [ x.lower() for x in delphikeywords ] # delphi is case insensitive, so convert everything to lowercase for comparisons # See Token.py in \python2x\Lib TOKEN_MEANINGS_FORDOCO_ONLY = """ AMPER = 19 AMPEREQUAL = 42 AT = 50 BACKQUOTE = 25 CIRCUMFLEX = 33 CIRCUMFLEXEQUAL = 44 COLON = 11 COMMA = 12 COMMENT = 53 DEDENT = 6 DOT = 23 DOUBLESLASH = 48 DOUBLESLASHEQUAL = 49 DOUBLESTAR = 36 DOUBLESTAREQUAL = 47 ENDMARKER = 0 EQEQUAL = 28 EQUAL = 22 ERRORTOKEN = 52 GREATER = 21 GREATEREQUAL = 31 INDENT = 5 LBRACE = 26 LEFTSHIFT = 34 LEFTSHIFTEQUAL = 45 LESS = 20 LESSEQUAL = 30 LPAR = 7 LSQB = 9 MINEQUAL = 38 MINUS = 15 NAME = 1 NEWLINE = 4 NL = 54 NOTEQUAL = 29 NT_OFFSET = 256 NUMBER = 2 N_TOKENS = 55 OP = 51 PERCENT = 24 PERCENTEQUAL = 41 PLUS = 14 PLUSEQUAL = 37 RBRACE = 27 RIGHTSHIFT = 35 RIGHTSHIFTEQUAL = 46 RPAR = 8 RSQB = 10 SEMI = 13 SLASH = 17 SLASHEQUAL = 40 STAR = 16 STAREQUAL = 39 STRING = 3 TILDE = 32 VBAR = 18 VBAREQUAL = 43 """ PK!) %gaphor/plugins/pynsource/pynsource.py""" PyNSource Version 1.4c (c) Andy Bulka 2004-2006 abulka@netspace.net.au http://www.atug.com/andypatterns/pynsource.htm A python source code scanner that generates - UML pictures (as text) - Java code (which can be imported into UML modeling tools.) - UML diagrams in wxpython (see associated module pyNsourceGui.py) GUI FRONT END ------------- Simply run C:\Python22\Lib\site-packages\pynsource\pyNsourceGui.py you need wxpython installed. See http://www.wxpython.org SOURCE GENERATOR ---------------- Example Usage: C:\Python22\Lib\site-packages\pynsource\pynsource -v -m -j outdir sourcedirorpythonfiles... -j generate java files, specify output folder for java files -v verbose -m create psuedo class for each module, module attrs/defs etc treated as class attrs/defs BASIC EXAMPLES e.g. pynsource Test/testmodule01.py e.g. pynsource -m Test/testmodule03.py JAVA EXAMPLES e.g. pynsource -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try/s*.py e.g. pynsource -j c:/try c:/try/s*.py Tests/u*.py e.g. pynsource -v -m -j c:/try c:/try/s*.py Tests/u*.py c:\cc\Devel\Client\w*.py DELPHI EXAMPLE e.g. pynsource -d c:/delphiouputdir c:/pythoninputdir/*.py INSTALLATION ------------- python setup.py install or run the windows .exe installer. JBUILDER TIPS ------------- Consider some folder e.g. .../Tests/ and create a jbuilder project based off there called PythonToJavaTest01 which will actually create a folder called PythonToJavaTest01 plus subfolders called src and classes etc. The Borland project file will also live in .../Tests/PythonToJavaTest01/ with a name PythonToJavaTest01.jpx Run pynsource so that it dumps the output into the src folder e.g. assuming the batch file is in the PythonToJavaTest01 folder and the python source is in .../Tests/PythonToJavaTest01/pythoninput01 then the command is pynsource -j src pythoninput01\*.py """ from functools import cmp_to_key import os import pprint import token import tokenize from gaphor.plugins.pynsource.keywords import ( pythonbuiltinfunctions, javakeywords, delphikeywords, ) DEBUG_DUMPTOKENS = False class AndyBasicParseEngine(object): def __init__(self): self.meat = 0 self.tokens = None self.isfreshline = 1 self.indentlevel = 0 def _ReadAllTokensFromFile(self, file): fp = open(file, "r") try: self.tokens = [x[0:2] for x in tokenize.generate_tokens(fp.readline)] finally: fp.close() if DEBUG_DUMPTOKENS: pprint.pprint(self.tokens) def Parse(self, file): self._ReadAllTokensFromFile(file) self.meat = 0 self._ParseLoop() def _ParseLoop(self): maxtokens = len(self.tokens) for i in range(0, maxtokens): tokentype, token = self.tokens[i] if tokentype == 5: self.indentlevel += 1 continue elif tokentype == 6: self.indentlevel -= 1 self.On_deindent() continue if tokentype == 0: # End Marker. break assert token, ( "Not expecting blank token, once have detected in & out dents. tokentype=%d, token=%s" % (tokentype, token) ) self.tokentype, self.token = tokentype, token if i + 1 < maxtokens: self.nexttokentype, self.nexttoken = self.tokens[i + 1] else: self.nexttokentype, self.nexttoken = (0, None) if self._Isblank(): continue else: # print 'MEAT', self.token self._Gotmeat() def On_deindent(self): pass def On_newline(self): pass def On_meat(self): pass def _Gotmeat(self): self.meat = 1 self.On_meat() self.isfreshline = 0 # must be here, at the end. def _Isblank(self): if self._Isnewline(): return 1 if self._Ispadding(): return 1 return 0 def _Isnewline(self): if self.token == "\n" or self.tokentype == token.N_TOKENS: if self.tokentype == token.N_TOKENS: assert "#" in self.token self.meat = 0 self.isfreshline = 1 self.On_newline() return 1 else: return 0 def _Ispadding(self): if not self.token.strip(): self.meat = 0 return 1 else: return 0 class ClassEntry(object): def __init__(self): self.defs = [] self.attrs = [] self.classdependencytuples = [] self.classesinheritsfrom = [] self.ismodulenotrealclass = 0 def FindAttribute(self, attrname): """ Return boolean hit, index pos """ for attrobj in self.attrs: if attrname == attrobj.attrname: return 1, attrobj return 0, None def AddAttribute(self, attrname, attrtype): """ If the new info is different to the old, and there is more info in it, then replace the old entry. e.g. oldattrtype may be ['normal' and new may be ['normal', 'many'] """ haveEncounteredAttrBefore, attrobj = self.FindAttribute(attrname) if not haveEncounteredAttrBefore: self.attrs.append(Attribute(attrname, attrtype)) else: # See if there is more info to add re this attr. if len(attrobj.attrtype) < len(attrtype): attrobj.attrtype = attrtype # Update it. # OLD CODE # if not self.FindAttribute(attrname): # self.attrs.append(Attribute(attrname, attrtype)) class Attribute(object): def __init__(self, attrname, attrtype="normal"): self.attrname = attrname self.attrtype = attrtype class HandleClasses(AndyBasicParseEngine): def __init__(self): AndyBasicParseEngine.__init__(self) self.currclasslist = [] self._currclass = None self.nexttokenisclass = 0 self.classlist = {} self.modulemethods = [] self.optionModuleAsClass = 0 self.inbetweenClassAndFirstDef = 0 def On_deindent(self): if self.currclassindentlevel and self.indentlevel <= self.currclassindentlevel: ## print 'popping class', self.currclass, 'from', self.currclasslist self.PopCurrClass() ## print ## print 'deindent!!', self.indentlevel, 'class indentlevel =', self.currclassindentlevel def _DeriveNestedClassName(self, currclass): if not self.currclasslist: return currclass else: classname, indentlevel = self.currclasslist[-1] return ( classname + "_" + currclass ) # Cannot use :: since java doesn't like this name, nor does the file system. def PushCurrClass(self, currclass): # print 'pushing currclass', currclass, 'self.currclasslist', self.currclasslist currclass = self._DeriveNestedClassName(currclass) self.currclasslist.append((currclass, self.indentlevel)) # print 'result of pushing = ', self.currclasslist def PopCurrClass(self): # __import__("traceback").print_stack(limit=6) self.currclasslist.pop() def GetCurrClassIndentLevel(self): if not self.currclasslist: return None currclassandindentlevel = self.currclasslist[-1] return currclassandindentlevel[1] def GetCurrClass(self): if not self.currclasslist: return None currclassandindentlevel = self.currclasslist[-1] return currclassandindentlevel[0] currclass = property(GetCurrClass) currclassindentlevel = property(GetCurrClassIndentLevel) def _JustThenGotclass(self): self.PushCurrClass(self.token) self.nexttokenisclass = 0 if self.currclass not in self.classlist: self.classlist[self.currclass] = ClassEntry() # print 'class', self.currclass self.inbetweenClassAndFirstDef = 1 def On_newline(self): pass def On_meat(self): if self.token == "class": ## print 'meat found class', self.token self.nexttokenisclass = 1 elif self.nexttokenisclass: ## print 'meat found class name ', self.token self._JustThenGotclass() class HandleInheritedClasses(HandleClasses): def __init__(self): HandleClasses.__init__(self) self._ClearwaitingInheriteClasses() def _JustThenGotclass(self): HandleClasses._JustThenGotclass(self) self.currsuperclass = "" self.nexttokenisBracketOpenOrColon = 1 def _ClearwaitingInheriteClasses(self): self.nexttokenisBracketOpenOrColon = 0 self.nexttokenisSuperclass = 0 self.nexttokenisComma = 0 def On_newline(self): self._ClearwaitingInheriteClasses() def On_meat(self): HandleClasses.On_meat(self) if self.nexttokenisBracketOpenOrColon and self.token == "(": assert ( self.tokentype == token.OP ) # unecessary, just practicing refering to tokens via names not numbers self.nexttokenisBracketOpen = 0 self.nexttokenisSuperclass = 1 elif self.nexttokenisBracketOpenOrColon and self.token == ":": self._ClearwaitingInheriteClasses() elif self.nexttokenisSuperclass and self.token == ")": self._ClearwaitingInheriteClasses() elif self.nexttokenisSuperclass: self.currsuperclass += self.token if self.token == "." or self.nexttoken == ".": # print 'processing multi part superclass detected!', self.token, self.nexttoken self.nexttokenisSuperclass = 1 else: self.nexttokenisSuperclass = 0 self.nexttokenisComma = 1 self.classlist[self.currclass].classesinheritsfrom.append( self.currsuperclass ) elif self.nexttokenisComma and self.token == ",": self.nexttokenisSuperclass = 1 self.nexttokenisComma = 0 class HandleDefs(HandleInheritedClasses): def __init__(self): HandleInheritedClasses.__init__(self) self.currdef = None self.nexttokenisdef = 0 def _Gotdef(self): self.currdef = self.token self.nexttokenisdef = 0 # print 'ADDING def', self.currdef, 'to', self.currclass ## if self.currclass and self.indentlevel == 1: if self.currclass: self.classlist[self.currclass].defs.append(self.currdef) elif self.optionModuleAsClass and self.indentlevel == 0: assert self.moduleasclass assert self.classlist[self.moduleasclass] self.classlist[self.moduleasclass].defs.append(self.currdef) else: self.modulemethods.append(self.currdef) self.inbetweenClassAndFirstDef = 0 def On_meat(self): HandleInheritedClasses.On_meat(self) ## if self.token == 'def' and self.indentlevel == 1: if self.token == "def": ## print 'DEF FOUND AT LEVEL', self.indentlevel self.nexttokenisdef = 1 elif self.nexttokenisdef: self._Gotdef() ## self.meat = 1 class HandleClassAttributes(HandleDefs): def __init__(self): HandleDefs.__init__(self) self.attrslist = [] self._Clearwaiting() def On_newline(self): HandleInheritedClasses.On_newline(self) self._Clearwaiting() def _Clearwaiting(self): self.waitingfordot = 0 self.waitingforsubsequentdot = 0 self.waitingforvarname = 0 self.waitingforequalsymbol = 0 self.currvarname = None self.lastcurrvarname = None self.waitforappendopenbracket = 0 self.nextvarnameisstatic = 0 self.nextvarnameismany = 0 def JustGotASelfAttr(self, selfattrname): pass def On_meat(self): HandleDefs.On_meat(self) if self.isfreshline and self.token == "self" and self.nexttoken == ".": self.waitingfordot = 1 elif self.waitingfordot and self.token == ".": self.waitingfordot = 0 self.waitingforvarname = 1 elif self.waitingforvarname: # We now have the possible class attribute name. :-) self.waitingforvarname = 0 self.currvarname = self.token """ At this point we have the x in the expression self.x A. We could find self.x = in which case we have a valid class attribute. B. We could find self.x.append( in which case we have a valid class attribute list/vector. C. We could find self.__class__.x = in which case we have a valid STATIC class attribute. D. We could find self.x.y = in which case we skip. E. We could find self.x.y.append( in which case we skip. F. We could find self.x.y.Blah( in which case we skip. G. We could find self.numberOfFlags = read16(fp) - skip cos read16 is a module function. """ if self.nexttoken == "=": self.waitingforequalsymbol = 1 # Case A elif self.nexttoken == ".": self.waitingforsubsequentdot = 1 # Cases B,C, D,E,F pending elif self.waitingforsubsequentdot and self.token == ".": self.waitingfordot = 0 self.waitingforsubsequentdot = 0 self.waitingforequalsymbol = 0 if self.nexttoken.lower() in ("append", "add", "insert"): # Case B # keep the class attribute name we have, wait till bracket self.waitforappendopenbracket = 1 elif self.currvarname in ("__class__",): # Case C self.currvarname = None self.waitingforvarname = 1 self.nextvarnameisstatic = 1 else: # Skip cases D, E, F self._Clearwaiting() elif self.waitforappendopenbracket and self.token == "(": self.waitforappendopenbracket = 0 self.nextvarnameismany = 1 self._AddAttribute() self._Clearwaiting() elif self.waitingforequalsymbol and self.token == "=": self.waitingforequalsymbol = 0 self._AddAttribute() self._Clearwaiting() def _AddAttribute(self): classentry = self.classlist[self.currclass] if self.nextvarnameisstatic: attrtype = ["static"] else: attrtype = ["normal"] if self.nextvarnameismany: attrtype.append("many") classentry.AddAttribute(self.currvarname, attrtype) # print ' ATTR ', self.currvarname self.JustGotASelfAttr(self.currvarname) class HandleComposites(HandleClassAttributes): def __init__(self): HandleClassAttributes.__init__(self) self._ClearwaitingOnComposites() self.dummy = ClassEntry() self.dummy2 = [()] def JustGotASelfAttr(self, selfattrname): assert selfattrname != "self" self.lastselfattrname = selfattrname self.waitingforclassname = 1 self.waitingforOpenBracket = 0 self.possibleclassname = None self.dontdoanythingnow = 1 def _ClearwaitingOnComposites(self): self.lastselfattrname = None self.waitingforclassname = 0 self.possibleclassname = None self.waitingforOpenBracket = 0 self.dontdoanythingnow = 0 def On_newline(self): HandleClassAttributes.On_newline(self) self._ClearwaitingOnComposites() def On_meat(self): self.dontdoanythingnow = 0 HandleClassAttributes.On_meat(self) # At this point we may have had a "self.blah = " encountered, and blah is saved in self.lastselfattrname if self.dontdoanythingnow: pass elif ( self.waitingforclassname and self.token not in ("(", "[") and self.token not in pythonbuiltinfunctions and self.tokentype not in (token.NUMBER, token.STRING) and self.token not in self.modulemethods ): self.possibleclassname = self.token self.waitingforclassname = 0 self.waitingforOpenBracket = 1 elif self.waitingforOpenBracket and self.token == "(": self.waitingforclassname = 0 self.waitingforOpenBracket = 0 dependency = (self.lastselfattrname, self.possibleclassname) self.classlist[self.currclass].classdependencytuples.append(dependency) # print '*** dependency - created instance of', self.possibleclassname, 'assigned to', self.lastselfattrname elif self.waitingforOpenBracket and self.token == ")": """ New - we haven't got a class being created but instead have a variable. Note that the above code detects self.flag.append(Flag()) # notice instance creation inside append but the following code detects self.flag.append(flag) # and assumes flag variable is an instance of Flag class """ # we don't have class being created but have a variable name instead variablename = self.possibleclassname # try to find a class with the same name. correspondingClassName = variablename[0].upper() + variablename[1:] # HACK # print 'correspondingClassName', correspondingClassName dependency = (self.lastselfattrname, correspondingClassName) self.classlist[self.currclass].classdependencytuples.append(dependency) else: self._ClearwaitingOnComposites() class HandleClassStaticAttrs(HandleComposites): def __init__(self): HandleComposites.__init__(self) self.__Clearwaiting() def __Clearwaiting(self): self.__waitingforequalsymbol = 0 self.__staticattrname = "" def On_meat(self): HandleComposites.On_meat(self) if ( self.isfreshline and self.currclass and self.inbetweenClassAndFirstDef and self.tokentype == 1 and self.indentlevel != 0 and self.nexttoken == "=" ): self.__waitingforequalsymbol = 1 self.__staticattrname = self.token elif self.__waitingforequalsymbol and self.token == "=": self.__waitingforequalsymbol = 0 # print 'have static level attr', self.__staticattrname self.__AddAttrModuleLevel() self.__Clearwaiting() def __AddAttrModuleLevel(self): # Should re-use the logic in HandleClassAttributes for both parsing # (getting more info on multiplicity but not static - cos static not relevant?) and # also should be able to reuse most of _AddAttr() # classentry = self.classlist[self.currclass] attrtype = ["static"] classentry.AddAttribute(self.__staticattrname, attrtype) # print ' STATIC ATTR ', self.__staticattrname class HandleModuleLevelDefsAndAttrs(HandleClassStaticAttrs): def __init__(self): HandleClassStaticAttrs.__init__(self) self.moduleasclass = "" self.__Clearwaiting() def __Clearwaiting(self): self.waitingforequalsymbolformoduleattr = 0 self.modulelevelattrname = "" def Parse(self, file): self.moduleasclass = "Module_" + os.path.splitext(os.path.basename(file))[0] if self.optionModuleAsClass: self.classlist[self.moduleasclass] = ClassEntry() self.classlist[self.moduleasclass].ismodulenotrealclass = 1 HandleComposites.Parse(self, file) def On_meat(self): HandleClassStaticAttrs.On_meat(self) if ( self.isfreshline and self.tokentype == 1 and self.indentlevel == 0 and self.nexttoken == "=" ): self.waitingforequalsymbolformoduleattr = 1 self.modulelevelattrname = self.token elif self.waitingforequalsymbolformoduleattr and self.token == "=": self.waitingforequalsymbolformoduleattr = 0 # print 'have module level attr', self.modulelevelattrname self._AddAttrModuleLevel() self.__Clearwaiting() def On_newline(self): HandleClassStaticAttrs.On_newline(self) self.__Clearwaiting() def _AddAttrModuleLevel(self): if not self.optionModuleAsClass: return # Should re-use the logic in HandleClassAttributes for both parsing # (getting more info on multiplicity but not static - cos static not relevant?) and # also should be able to reuse most of _AddAttr() # classentry = self.classlist[self.moduleasclass] attrtype = ["normal"] ## if self.nextvarnameisstatic: ## attrtype = ['static'] ## else: ## attrtype = ['normal'] ## ## if self.nextvarnameismany: ## attrtype.append('many') classentry.AddAttribute(self.modulelevelattrname, attrtype) # print ' ATTR ', self.currvarname # self.JustGotASelfAttr(self.currvarname) class PySourceAsText(HandleModuleLevelDefsAndAttrs): def __init__(self): HandleModuleLevelDefsAndAttrs.__init__(self) self.listcompositesatend = 0 self.embedcompositeswithattributelist = 1 self.result = "" self.aclass = None self.classentry = None self.staticmessage = "" self.manymessage = "" self.verbose = 0 def GetCompositeClassesForAttr(self, classname, classentry): resultlist = [] for dependencytuple in classentry.classdependencytuples: if dependencytuple[0] == classname: resultlist.append(dependencytuple[1]) return resultlist def _GetCompositeCreatedClassesFor(self, classname): return self.GetCompositeClassesForAttr(classname, self.classentry) def _DumpAttribute(self, attrobj): compositescreated = self._GetCompositeCreatedClassesFor(attrobj.attrname) if compositescreated and self.embedcompositeswithattributelist: self.result += "%s %s <@>----> %s" % ( attrobj.attrname, self.staticmessage, str(compositescreated), ) else: self.result += "%s %s" % (attrobj.attrname, self.staticmessage) self.result += self.manymessage self.result += "\n" def _DumpCompositeExtraFooter(self): if self.classentry.classdependencytuples and self.listcompositesatend: for dependencytuple in self.classentry.classdependencytuples: self.result += "%s <*>---> %s\n" % dependencytuple self.result += "-" * 20 + "\n" def _DumpClassNameAndGeneralisations(self): self._Line() if self.classentry.ismodulenotrealclass: self.result += "%s (file)\n" % (self.aclass,) else: self.result += "%s --------|> %s\n" % ( self.aclass, self.classentry.classesinheritsfrom, ) self._Line() def _DumpAttributes(self): for attrobj in self.classentry.attrs: self.staticmessage = "" self.manymessage = "" if "static" in attrobj.attrtype: self.staticmessage = " static" if "many" in attrobj.attrtype: self.manymessage = " 1..*" self._DumpAttribute(attrobj) def _DumpMethods(self): for adef in self.classentry.defs: self.result += adef + "\n" def _Line(self): self.result += "-" * 20 + "\n" def _DumpClassHeader(self): self.result += "\n" def _DumpClassFooter(self): self.result += "\n" self.result += "\n" def _DumpModuleMethods(self): if self.modulemethods: self.result += " ModuleMethods = %s\n" % repr(self.modulemethods) ## self.result += '\n' def __str__(self): self.result = "" self._DumpClassHeader() self._DumpModuleMethods() optionAlphabetic = 0 classnames = list(self.classlist.keys()) if optionAlphabetic: classnames.sort() else: def cmpfunc(a, b): if a.find("Module_") != -1: return -1 else: if a < b: return -1 elif a == b: return 0 else: return 1 classnames.sort(key=cmp_to_key(cmpfunc)) for self.aclass in classnames: self.classentry = self.classlist[self.aclass] ## for self.aclass, self.classentry in self.classlist.items(): self._DumpClassNameAndGeneralisations() self._DumpAttributes() self._Line() self._DumpMethods() self._Line() self._DumpCompositeExtraFooter() self._DumpClassFooter() return self.result class PySourceAsJava(PySourceAsText): def __init__(self, outdir=None): PySourceAsText.__init__(self) self.outdir = outdir self.fp = None def _DumpClassFooter(self): self.result += "}\n" if self.fp: self.fp.write(self.result) self.fp.close() self.fp = None self.result = "" def _DumpModuleMethods(self): self.result += "/*\n" PySourceAsText._DumpModuleMethods(self) self.result += "*/\n" def _OpenNextFile(self): filepath = "%s\\%s.java" % (self.outdir, self.aclass) self.fp = open(filepath, "w") def _NiceNameToPreventCompilerErrors(self, attrname): """ Prevent compiler errors on the java side by checking and modifying attribute name """ # only emit the rhs of a multi part name e.g. undo.UndoItem will appear only as UndoItem if attrname.find(".") != -1: attrname = attrname.split(".")[-1] # take the last # Prevent compiler errors on the java side by avoiding the generating of java keywords as attribute names if attrname in javakeywords: attrname = "_" + attrname return attrname def _DumpAttribute(self, attrobj): compositescreated = self._GetCompositeCreatedClassesFor(attrobj.attrname) if compositescreated: compositecreated = compositescreated[0] else: compositecreated = None # Extra processing on the attribute name, to avoid java compiler errors attrname = self._NiceNameToPreventCompilerErrors(attrobj.attrname) if compositecreated and self.embedcompositeswithattributelist: self.result += " public %s %s %s = new %s();\n" % ( self.staticmessage, compositecreated, attrname, compositecreated, ) else: ## self.result += " public %s void %s;\n" % (self.staticmessage, attrobj.attrname) ## self.result += " public %s int %s;\n" % (self.staticmessage, attrname) self.result += " public %s variant %s;\n" % ( self.staticmessage, attrname, ) """ import java.util.Vector; private java.util.Vector lnkClass4; private Vector lnkClass4; """ def _DumpCompositeExtraFooter(self): pass def _DumpClassNameAndGeneralisations(self): if self.verbose: print(" Generating Java class", self.aclass) self._OpenNextFile() self.result += "// Generated by PyNSource http://www.atug.com/andypatterns/pynsource.htm \n\n" ## self.result += "import javax.swing.Icon; // Not needed, just testing pyNSource's ability to generate import statements.\n\n" # NEW package support! self.result += "public class %s " % self.aclass if self.classentry.classesinheritsfrom: self.result += "extends %s " % self._NiceNameToPreventCompilerErrors( self.classentry.classesinheritsfrom[0] ) self.result += "{\n" def _DumpMethods(self): for adef in self.classentry.defs: self.result += " public void %s() {\n }\n" % adef def _Line(self): pass def unique(s): """ Return a list of the elements in list s in arbitrary order, but without duplicates """ n = len(s) if n == 0: return [] u = {} try: for x in s: u[x] = 1 except TypeError: del u # move onto the next record else: return list(u.keys()) raise KeyError("uniqueness algorithm failed .. type more of it in please") class PySourceAsDelphi(PySourceAsText): """ Example Delphi source file: unit test000123; interface uses SysUtils, Windows, Messages, Classes, Graphics, Controls, Forms, Dialogs; type TDefault1 = class (TObject) private field0012: Variant; public class var field0123434: Variant; procedure Member1; class procedure Member2; end; procedure Register; implementation procedure Register; begin end; { ********************************** TDefault1 *********************************** } procedure TDefault1.Member1; begin end; class procedure TDefault1.Member2; begin end; end. """ def __init__(self, outdir=None): PySourceAsText.__init__(self) self.outdir = outdir self.fp = None def _DumpClassFooter(self): self.result += "\n\n" self.result += "implementation\n\n" self.DumpImplementationMethods() self.result += "\nend.\n\n" if self.fp: self.fp.write(self.result) self.fp.close() self.fp = None self.result = "" def _DumpModuleMethods(self): self.result += "(*\n" PySourceAsText._DumpModuleMethods(self) self.result += "*)\n\n" def _OpenNextFile(self): filepath = "%s\\unit_%s.pas" % (self.outdir, self.aclass) self.fp = open(filepath, "w") def _NiceNameToPreventCompilerErrors(self, attrname): """ Prevent compiler errors on the java side by checking and modifying attribute name """ # only emit the rhs of a multi part name e.g. undo.UndoItem will appear only as UndoItem if attrname.find(".") != -1: attrname = attrname.split(".")[-1] # take the last # Prevent compiler errors on the Delphi side by avoiding the generating of delphi keywords as attribute names if ( attrname.lower() in delphikeywords ): # delphi is case insensitive, so convert everything to lowercase for comparisons attrname = "_" + attrname return attrname def _DumpAttribute(self, attrobj): """ Figure out what type the attribute is only in those cases where we are later going to assign to these variables using .Create() in the constructor. The rest we make Variants. """ compositescreated = self._GetCompositeCreatedClassesFor(attrobj.attrname) if compositescreated: compositecreated = compositescreated[0] else: compositecreated = None # Extra processing on the attribute name, to avoid delphi compiler errors attrname = self._NiceNameToPreventCompilerErrors(attrobj.attrname) self.result += " " if self.staticmessage: self.result += "class var" if compositecreated: vartype = compositecreated else: vartype = "Variant" self.result += "%s : %s;\n" % (attrname, vartype) # generate more complex stuff in the implementation section... ## if compositecreated and self.embedcompositeswithattributelist: ## self.result += " public %s %s %s = new %s();\n" % (self.staticmessage, compositecreated, attrname, compositecreated) ## else: ## self.result += "%s : Variant;\n"%attrname def _DumpCompositeExtraFooter(self): pass def _DumpClassNameAndGeneralisations(self): if self.verbose: print(" Generating Delphi class", self.aclass) self._OpenNextFile() self.result += "// Generated by PyNSource http://www.atug.com/andypatterns/pynsource.htm \n\n" self.result += "unit unit_%s;\n\n" % self.aclass self.result += "interface\n\n" uses = unique(self.GetUses()) if uses: self.result += "uses\n " self.result += ", ".join(uses) self.result += ";\n\n" self.result += "type\n\n" self.result += "%s = class" % self.aclass if self.classentry.classesinheritsfrom: self.result += "(%s)" % self._NiceNameToPreventCompilerErrors( self.classentry.classesinheritsfrom[0] ) self.result += "\n" self.result += "public\n" def _DumpMethods(self): if self.classentry.attrs: # if there were any atributes... self.result += ( "\n" ) # a little bit of a separator between attributes and methods. for adef in self.classentry.defs: if adef == "__init__": self.result += " constructor Create;\n" else: ## self.result += " function %s(): void; virtual;\n" % adef self.result += " procedure %s(); virtual;\n" % adef self.result += "end;\n" # end of class def DumpImplementationMethods(self): for adef in self.classentry.defs: if adef == "__init__": self.result += ( "constructor %s.Create;\n" % self.aclass ) # replace __init__ with the word 'Create' else: ## self.result += "function %s.%s(): void;\n" % (self.aclass, adef) self.result += "procedure %s.%s();\n" % (self.aclass, adef) self.result += "begin\n" if adef == "__init__": self.CreateCompositeAttributeClassCreationAndAssignmentInImplementation() self.result += "end;\n\n" def CreateCompositeAttributeClassCreationAndAssignmentInImplementation(self): # Only do those attributes that are composite and need to create an instance of something for attrobj in self.classentry.attrs: compositescreated = self._GetCompositeCreatedClassesFor(attrobj.attrname) if ( compositescreated and self.embedcompositeswithattributelist ): # latter variable always seems to be true! Never reset!? compositecreated = compositescreated[0] self.result += " %s := %s.Create();\n" % ( attrobj.attrname, compositecreated, ) def GetUses(self): result = [] for attrobj in self.classentry.attrs: compositescreated = self._GetCompositeCreatedClassesFor(attrobj.attrname) if ( compositescreated and self.embedcompositeswithattributelist ): # latter variable always seems to be true! Never reset!? compositecreated = compositescreated[0] result.append(compositecreated) # Also use any inherited calss modules. if self.classentry.classesinheritsfrom: result.append( self._NiceNameToPreventCompilerErrors( self.classentry.classesinheritsfrom[0] ) ) return ["unit_" + u for u in result] def _Line(self): pass class PythonToJava(object): def __init__(self, directories, treatmoduleasclass=0, verbose=0): self.directories = directories self.optionModuleAsClass = treatmoduleasclass self.verbose = verbose def _GenerateAuxilliaryClasses(self): classestocreate = ( "variant", "unittest", "list", "object", "dict", ) # should add more classes and add them to a jar file to avoid namespace pollution. for aclass in classestocreate: fp = open(os.path.join(self.outpath, aclass + ".java"), "w") fp.write(self.GenerateSourceFileForAuxClass(aclass)) fp.close() def GenerateSourceFileForAuxClass(self, aclass): return "\npublic class %s {\n}\n" % aclass def ExportTo(self, outpath): self.outpath = outpath self._GenerateAuxilliaryClasses() for directory in self.directories: if "*" in directory or "." in directory: filepath = directory else: filepath = os.path.join(directory, "*.py") if self.verbose: print("Processing directory", filepath) globbed = glob.glob(filepath) # print 'Java globbed is', globbed for f in globbed: self._Process(f) def _Process(self, filepath): if self.verbose: padding = " " else: padding = "" thefile = os.path.basename(filepath) if thefile[0] == "_": print(" ", "Skipped", thefile, "cos begins with underscore.") return print("%sProcessing %s..." % (padding, thefile)) p = self._CreateParser() p.Parse(filepath) str(p) # triggers the output. def _CreateParser(self): p = PySourceAsJava(self.outpath) p.optionModuleAsClass = self.optionModuleAsClass p.verbose = self.verbose return p class PythonToDelphi(PythonToJava): def _GenerateAuxilliaryJavaClasses(self): pass def _CreateParser(self): p = PySourceAsDelphi(self.outpath) p.optionModuleAsClass = self.optionModuleAsClass p.verbose = self.verbose return p def _GenerateAuxilliaryClasses(self): # Delphi version omits the class 'object' and 'variant' since these already are pre-defined in Delphi. classestocreate = ("unittest", "list", "dict") # should add more classes for aclass in classestocreate: fp = open(os.path.join(self.outpath, "unit_" + aclass + ".pas"), "w") fp.write(self.GenerateSourceFileForAuxClass(aclass)) fp.close() def GenerateSourceFileForAuxClass(self, aclass): template = """ unit unit_%s; interface type %s = class public end; implementation end. """ return template % (aclass, aclass) def run(): # FILE = 'testmodule01.py' # FILE = 'C:\\Documents and Settings\\Administrator\\Desktop\\try\\PyutXmlV6.py' # FILE = 'testmodule02.py' # FILE = 'andyparse9.py' FILE = "c:\\cc\devel\storyline\\battle.py" # FILE = "c:\\cc\devel\storyline\\battleresult.py" # FILE = "c:\\cc\devel\storyline\\battlestabs.py" p = PySourceAsText() # p = JavaDumper("c:\\try") p.Parse(FILE) print("*" * 20, "parsing", FILE, "*" * 20) print(p) print("Done.") if __name__ == "__main__": # run() import sys, glob, getopt SIMPLE = 0 globbed = [] optionVerbose = 0 optionModuleAsClass = 0 optionExportToJava = 0 optionExportToDelphi = 0 optionExportTo_outdir = "" if SIMPLE: params = sys.argv[1] globbed = glob.glob(params) else: listofoptionvaluepairs, params = getopt.getopt(sys.argv[1:], "mvj:d:") print(listofoptionvaluepairs, params) def EnsurePathExists(outdir, outlanguagemsg): assert outdir, "Need to specify output folder for %s output - got %s." % ( outlanguagemsg, outdir, ) if not os.path.exists(outdir): raise RuntimeError( "Output directory %s for %s file output does not exist." % (outdir, outlanguagemsg) ) for optionvaluepair in listofoptionvaluepairs: if "-m" == optionvaluepair[0]: optionModuleAsClass = 1 if "-v" == optionvaluepair[0]: optionVerbose = 1 if optionvaluepair[0] in ("-j", "-d"): if optionvaluepair[0] == "-j": optionExportToJava = 1 language = "Java" else: optionExportToDelphi = 1 language = "Delphi" optionExportTo_outdir = optionvaluepair[1] EnsurePathExists(optionExportTo_outdir, language) for param in params: files = glob.glob(param) globbed += files if globbed: if optionExportToJava or optionExportToDelphi: if optionExportToJava: u = PythonToJava( globbed, treatmoduleasclass=optionModuleAsClass, verbose=optionVerbose, ) else: u = PythonToDelphi( globbed, treatmoduleasclass=optionModuleAsClass, verbose=optionVerbose, ) u.ExportTo(optionExportTo_outdir) else: p = PySourceAsText() p.optionModuleAsClass = optionModuleAsClass p.verbose = optionVerbose for f in globbed: p.Parse(f) print(p) else: print( """Usage: pynsource -v -m -j outdir sourcedirorpythonfiles... -j generate java files, specify output folder for java files -v verbose -m create psuedo class for each module, module attrs/defs etc treated as class attrs/defs BASIC EXAMPLES e.g. pynsource Test/testmodule01.py e.g. pynsource -m Test/testmodule03.py JAVA EXAMPLES e.g. pynsource -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try e.g. pynsource -v -m -j c:/try c:/try/s*.py e.g. pynsource -j c:/try c:/try/s*.py Tests/u*.py e.g. pynsource -v -m -j c:/try c:/try/s*.py Tests/u*.py c:\cc\Devel\Client\w*.py DELPHI EXAMPLE e.g. pynsource -d c:/delphiouputdir c:/pythoninputdir/*.py """ ) PK!G_M`33$gaphor/plugins/xmiexport/__init__.py""" This plugin extends Gaphor with XMI export functionality. """ import logging from zope.interface import implementer from gaphor.core import _, inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider from gaphor.plugins.xmiexport import exportmodel from gaphor.ui.filedialog import FileDialog logger = logging.getLogger(__name__) @implementer(IService, IActionProvider) class XMIExport(object): element_factory = inject("element_factory") main_window = inject("main_window") menu_xml = """ """ def __init__(self): self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass @action( name="file-export-xmi", label=_("Export to XMI"), tooltip=_("Export model to XMI (XML Model Interchange) format"), ) def execute(self): filename = self.main_window.get_filename() if filename: filename = filename.replace(".gaphor", ".xmi") else: filename = "model.xmi" file_dialog = FileDialog( _("Export model to XMI file"), action="save", filename=filename ) filename = file_dialog.selection if filename and len(filename) > 0: logger.debug("Exporting XMI model to: %s" % filename) export = exportmodel.XMIExport(self.element_factory) try: export.export(filename) except Exception as e: logger.error("Error while saving model to file %s: %s" % (filename, e)) # vim:sw=4:et PK!z%%'gaphor/plugins/xmiexport/exportmodel.pyimport logging from gaphor.misc.xmlwriter import XMLWriter logger = logging.getLogger(__name__) class XMIExport(object): XMI_VERSION = "2.1" XMI_NAMESPACE = "http://schema.omg.org/spec/XMI/2.1" UML_NAMESPACE = "http://schema.omg.org/spec/UML/2.1" XMI_PREFIX = "XMI" UML_PREFIX = "UML" def __init__(self, element_factory): self.element_factory = element_factory self.handled_ids = list() def handle(self, xmi, element): logger.debug("Handling %s" % element.__class__.__name__) try: handler_name = "handle%s" % element.__class__.__name__ handler = getattr(self, handler_name) idref = element.id in self.handled_ids handler(xmi, element, idref=idref) if not idref: self.handled_ids.append(element.id) except AttributeError as e: logger.warning( "Missing handler for %s:%s" % (element.__class__.__name__, e) ) except Exception as e: logger.error("Failed to handle %s:%s" % (element.__class__.__name__, e)) def handlePackage(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["name"] = element.name attributes["visibility"] = element.visibility xmi.startElement("%s:Package" % self.UML_PREFIX, attrs=attributes) for ownedMember in element.ownedMember: xmi.startElement("ownedMember", attrs=dict()) self.handle(xmi, ownedMember) xmi.endElement("ownedMember") xmi.endElement("%s:Package" % self.UML_PREFIX) def handleClass(self, xmi, element, idref=False): attributes = dict() if idref: attributes["%s:idref" % self.XMI_PREFIX] = element.id else: attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["name"] = element.name attributes["isAbstract"] = str(element.isAbstract) xmi.startElement("%s:Class" % self.UML_PREFIX, attrs=attributes) if not idref: for ownedAttribute in element.ownedAttribute: xmi.startElement("ownedAttribute", attrs=dict()) self.handle(xmi, ownedAttribute) xmi.endElement("ownedAttribute") for ownedOperation in element.ownedOperation: xmi.startElement("ownedOperation", attrs=dict()) self.handle(xmi, ownedOperation) xmi.endElement("ownedOperation") xmi.endElement("%s:Class" % self.UML_PREFIX) def handleProperty(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["isStatic"] = str(element.isStatic) attributes["isOrdered"] = str(element.isOrdered) attributes["isUnique"] = str(element.isUnique) attributes["isDerived"] = str(element.isDerived) attributes["isDerivedUnion"] = str(element.isDerivedUnion) attributes["isReadOnly"] = str(element.isReadOnly) if element.name is not None: attributes["name"] = element.name xmi.startElement("%s:Property" % self.UML_PREFIX, attrs=attributes) # TODO: This should be type, not typeValue. if element.typeValue is not None: xmi.startElement("type", attrs=dict()) self.handle(xmi, element.typeValue) xmi.endElement("type") xmi.endElement("%s:Property" % self.UML_PREFIX) def handleOperation(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["isStatic"] = str(element.isStatic) attributes["isQuery"] = str(element.isQuery) attributes["name"] = element.name xmi.startElement("%s:Operation" % self.XMI_PREFIX, attrs=attributes) for ownedParameter in element.parameter: xmi.startElement("ownedElement", attrs=dict()) self.handle(xmi, ownedParameter) xmi.endElement("ownedElement") xmi.endElement("%s:Operation" % self.XMI_PREFIX) def handleParameter(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["isOrdered"] = str(element.isOrdered) attributes["isUnique"] = str(element.isUnique) attributes["direction"] = element.direction attributes["name"] = element.name xmi.startElement("%s:Parameter" % self.XMI_PREFIX, attrs=attributes) xmi.endElement("%s:Parameter" % self.XMI_PREFIX) def handleLiteralSpecification(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["value"] = element.value xmi.startElement("%s:LiteralSpecification" % self.UML_PREFIX, attrs=attributes) xmi.endElement("%s:LiteralSpecification" % self.UML_PREFIX) def handleAssociation(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["isDerived"] = str(element.isDerived) xmi.startElement("%s:Association" % self.UML_PREFIX, attrs=attributes) for memberEnd in element.memberEnd: xmi.startElement("memberEnd", attrs=dict()) self.handle(xmi, memberEnd) xmi.endElement("memberEnd") for ownedEnd in element.ownedEnd: xmi.startElement("ownedEnd", attrs=dict()) self.handle(xmi, ownedEnd) xmi.endElement("ownedEnd") xmi.endElement("%s:Association" % self.UML_PREFIX) def handleDependency(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id xmi.startElement("%s:Dependency" % self.UML_PREFIX, attrs=attributes) for client in element.client: xmi.startElement("client", attrs=dict()) self.handle(xmi, client) xmi.endElement("client") for supplier in element.supplier: xmi.startElement("supplier", attrs=dict()) self.handle(xmi, supplier) xmi.endElement("supplier") xmi.endElement("%s:Dependency" % self.UML_PREFIX) def handleGeneralization(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id attributes["isSubstitutable"] = str(element.isSubstitutable) xmi.startElement("%s:Generalization" % self.UML_PREFIX, attrs=attributes) if element.general: xmi.startElement("general", attrs=dict()) self.handle(xmi, element.general) xmi.endElement("general") if element.specific: xmi.startElement("specific", attrs=dict()) self.handle(xmi, element.specific) xmi.endElement("specific") xmi.endElement("%s:Generalization" % self.UML_PREFIX) def handleRealization(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id xmi.startElement("%s:Realization" % self.UML_PREFIX, attrs=attributes) for client in element.client: xmi.startElement("client", attrs=dict()) self.handle(xmi, client) xmi.endElement("client") for supplier in element.supplier: xmi.startElement("supplier", attrs=dict()) self.handle(xmi, supplier) xmi.endElement("supplier") xmi.endElement("%s:Realization" % self.UML_PREFIX) def handleInterface(self, xmi, element, idref=False): attributes = dict() attributes["%s:id" % self.XMI_PREFIX] = element.id xmi.startElement("%s:Interface" % self.UML_PREFIX, attrs=attributes) for ownedAttribute in element.ownedAttribute: xmi.startElement("ownedAttribute", attrs=dict()) self.handle(ownedAttribute) xmi.endElement("ownedAttribute") for ownedOperation in element.ownedOperation: xmi.startElement("ownedOperation", attrs=dict()) self.handle(ownedOperation) xmi.endElement("ownedOperation") xmi.endElement("%s:Interface" % self.UML_PREFIX) def export(self, filename): out = open(filename, "w") xmi = XMLWriter(out) attributes = dict() attributes["xmi.version"] = self.XMI_VERSION attributes["xmlns:xmi"] = self.XMI_NAMESPACE attributes["xmlns:UML"] = self.UML_NAMESPACE xmi.startElement("XMI", attrs=attributes) for package in self.element_factory.select(self.select_package): self.handle(xmi, package) for generalization in self.element_factory.select(self.select_generalization): self.handle(xmi, generalization) for realization in self.element_factory.select(self.select_realization): self.handle(xmi, realization) xmi.endElement("XMI") logger.debug(self.handled_ids) def select_package(self, element): return element.__class__.__name__ == "Package" def select_generalization(self, element): return element.__class__.__name__ == "Generalization" def select_realization(self, element): return element.__class__.name__ == "Implementation" PK!gaphor/services/__init__.pyPK!/Ϛ gaphor/services/actionmanager.py""" """ import logging from zope import component from gi.repository import Gtk from zope.interface import implementer from gaphor.core import inject from gaphor.event import ServiceInitializedEvent, ActionExecuted from gaphor.interfaces import IService, IActionProvider logger = logging.getLogger(__name__) @implementer(IService) class ActionManager(object): """ This service is responsible for maintaining actions. """ component_registry = inject("component_registry") ui_manager = inject("ui_manager") def __init__(self): pass def init(self, app): logger.info("Loading action provider services") for name, service in self.component_registry.get_utilities(IActionProvider): logger.debug("Service is %s" % service) self.register_action_provider(service) self.component_registry.register_handler(self._service_initialized_handler) def shutdown(self): logger.info("Shutting down") self.component_registry.unregister_handler(self._service_initialized_handler) def execute(self, action_id, active=None): logger.debug("Executing action, action_id is %s" % action_id) a = self.get_action(action_id) if a: a.activate() self.component_registry.handle(ActionExecuted(action_id, a)) else: logger.warning("Unknown action %s" % action_id) def update_actions(self): self.ui_manager.ensure_update() def get_action(self, action_id): for g in self.ui_manager.get_action_groups(): a = g.get_action(action_id) if a: return a def register_action_provider(self, action_provider): logger.debug("Registering action provider %s" % action_provider) action_provider = IActionProvider(action_provider) try: # Check if the action provider is not already registered action_provider.__ui_merge_id except AttributeError: assert action_provider.action_group self.ui_manager.insert_action_group(action_provider.action_group, -1) try: menu_xml = action_provider.menu_xml except AttributeError: pass else: action_provider.__ui_merge_id = self.ui_manager.add_ui_from_string( menu_xml ) @component.adapter(ServiceInitializedEvent) def _service_initialized_handler(self, event): logger.debug("Handling ServiceInitializedEvent") logger.debug("Service is %s" % event.service) if IActionProvider.providedBy(event.service): logger.debug("Loading registered service %s" % event.service) self.register_action_provider(event.service) @implementer(IService) class UIManager(Gtk.UIManager): """ Service version of Gtk.UIManager. """ def init(self, app=None): pass def shutdown(self): pass PK!m= gaphor/services/adapterloader.pyfrom zope.interface import implementer from gaphor.interfaces import IService @implementer(IService) class AdapterLoader(object): """ Initiate adapters from the gaphor.adapters module. """ def init(self, app): pass def shutdown(self): pass PK!(g=dd$gaphor/services/componentregistry.py""" TODO: Move component information (event handling, and other stuff done by zope.component) to this service. Maybe we should split the ComponentRegistry in a Dispatcher (register_handler, unregister_handler, handle), a AdapterRegistry and a Subscription registry. """ from zope import component from zope.interface import registry from zope.interface import implementer from gaphor.interfaces import IService, IEventFilter @implementer(IService) class ZopeComponentRegistry(object): """ The ZopeComponentRegistry provides a subset of the ``zope.component.registry.Components`` interface. This part is mainly enough to get the work done and keeps stuff simpler. This service should not be called directly, but through more specific service such as Dispatcher and AdapterRegistry. """ def __init__(self): pass def init(self, app): self._components = registry.Components( name="component_registry", bases=(component.getGlobalSiteManager(),) ) # Make sure component.handle() and query methods works. # TODO: eventually all queries should be done through the Application # instance. # Used in collection.py, transaction.py, diagramtoolbox.py: component.handle = self.handle # component.getMultiAdapter = self._components.getMultiAdapter # Used all over the place: component.queryMultiAdapter = self._components.queryMultiAdapter # component.getAdapter = self._components.getAdapter # component.queryAdapter = self._components.queryAdapter # Used in propertyeditor.py: component.getAdapters = self._components.getAdapters # component.getUtility = self._components.getUtility # Used in test cases (test_application.py) component.queryUtility = self._components.queryUtility # component.getUtilitiesFor = self._components.getUtilitiesFor def shutdown(self): pass def get_service(self, name): """Obtain a service used by Gaphor by name. E.g. service("element_factory") """ return self.get_utility(IService, name) # Wrap zope.component's Components methods def register_utility(self, component=None, provided=None, name=""): """ Register a component (e.g. Service) """ self._components.registerUtility(component, provided, name) def unregister_utility(self, component=None, provided=None, name=""): """ Unregister a component (e.g. Service) """ self._components.unregisterUtility(component, provided, name) def get_utility(self, provided, name=""): """ Get a component from the registry. zope.component.ComponentLookupError is thrown if no such component exists. """ return self._components.getUtility(provided, name) def get_utilities(self, provided): """ Iterate over all components that provide a certain interface. """ for name, utility in self._components.getUtilitiesFor(provided): yield name, utility def register_adapter(self, factory, adapts=None, provides=None, name=""): """ Register an adapter (factory) that adapts objects to a specific interface. A name can be used to distinguish between different adapters that adapt to the same interfaces. """ self._components.registerAdapter(factory, adapts, provides, name, event=False) def unregister_adapter(self, factory=None, required=None, provided=None, name=u""): """ Unregister a previously registered adapter. """ self._components.unregisterAdapter(factory, required, provided, name) def get_adapter(self, objects, interface): """ Obtain an adapter that adheres to a specific interface. Objects can be either a single object or a tuple of objects (multi adapter). If nothing is found `None` is returned. """ if isinstance(objects, (list, tuple)): return self._components.queryMultiAdapter(objects, interface) return self._components.queryAdapter(objects, interface) def register_subscription_adapter(self, factory, adapts=None, provides=None): """ Register a subscription adapter. See registerAdapter(). """ self._components.registerSubscriptionAdapter( factory, adapts, provides, event=False ) def unregister_subscription_adapter( self, factory=None, required=None, provided=None, name=u"" ): """ Unregister a previously registered subscription adapter. """ self._components.unregisterSubscriptionAdapter( factory, required, provided, name ) def subscribers(self, objects, interface): return self._components.subscribers(objects, interface) def register_handler(self, factory, adapts=None): """ Register a handler. Handlers are triggered (executed) when specific events are emitted through the handle() method. """ self._components.registerHandler(factory, adapts, event=False) def unregister_handler(self, factory=None, required=None): """ Unregister a previously registered handler. """ self._components.unregisterHandler(factory, required) def _filter(self, objects): filtered = list(objects) for o in objects: for adapter in self._components.subscribers(objects, IEventFilter): if adapter.filter(): # event is blocked filtered.remove(o) break return filtered def handle(self, *events): """ Send event notifications to registered handlers. """ objects = self._filter(events) if objects: list(map(self._components.handle, events)) PK!oogaphor/services/copyservice.py""" Copy / Paste functionality """ from zope import component import gaphas from zope.interface import implementer from gaphor.UML import Element from gaphor.UML.collection import collection from gaphor.core import inject, action, build_action_group, transactional from gaphor.interfaces import IService, IActionProvider from gaphor.ui.interfaces import IDiagramSelectionChange @implementer(IService, IActionProvider) class CopyService(object): """ Copy/Cut/Paste functionality required a lot of thinking: Store a list of DiagramItems that have to be copied in a global 'copy-buffer'. - in order to make copy/paste work, the load/save functions should be generatlised to allow a subset to be saved/loaded (which is needed anyway for exporting/importing stereotype Profiles). - How many data should be saved? (e.g. we copy a diagram item, remove it (the underlying UML element is removed) and the paste the copied item. The diagram should act as if we have placed a copy of the removed item on the canvas and make the uml element visible again. """ component_registry = inject("component_registry") element_factory = inject("element_factory") main_window = inject("main_window") menu_xml = """ """ def __init__(self): self.copy_buffer = set() self.action_group = build_action_group(self) def init(self, app): self.action_group.get_action("edit-copy").props.sensitive = False self.action_group.get_action("edit-paste").props.sensitive = False self.component_registry.register_handler(self._update) def shutdown(self): self.copy_buffer = set() self.component_registry.unregister_handler(self._update) @component.adapter(IDiagramSelectionChange) def _update(self, event): diagram_view = event.diagram_view self.action_group.get_action("edit-copy").props.sensitive = bool( diagram_view.selected_items ) def copy(self, items): if items: self.copy_buffer = set(items) self.action_group.get_action("edit-paste").props.sensitive = True def copy_func(self, name, value, reference=False): """ Copy an element, preferably from the list of new items, otherwise from the element factory. If it does not exist there, do not copy it! """ def load_element(): item = self._new_items.get(value.id) if item: self._item.load(name, item) else: item = self.element_factory.lookup(value.id) if item: self._item.load(name, item) if reference or isinstance(value, Element): load_element() elif isinstance(value, collection): values = value for value in values: load_element() elif isinstance(value, gaphas.Item): load_element() else: # Plain attribute self._item.load(name, str(value)) @transactional def paste(self, diagram): """ Paste items in the copy-buffer to the diagram """ canvas = diagram.canvas if not canvas: return copy_items = [c for c in self.copy_buffer if c.canvas] # Mapping original id -> new item self._new_items = {} # Create new id's that have to be used to create the items: for ci in copy_items: self._new_items[ci.id] = diagram.create(type(ci)) # Copy attributes and references. References should be # 1. in the ElementFactory (hence they are model elements) # 2. referred to in new_items # 3. canvas property is overridden for ci in copy_items: self._item = self._new_items[ci.id] ci.save(self.copy_func) # move pasted items a bit, so user can see result of his action :) # update items' matrix immediately # TODO: if it is new canvas, then let's not move, how to do it? for item in list(self._new_items.values()): item.matrix.translate(10, 10) canvas.update_matrix(item) # solve internal constraints of items immediately as item.postload # reconnects items and all handles has to be in place canvas.solver.solve() for item in list(self._new_items.values()): item.postload() @action(name="edit-copy", stock_id="gtk-copy") def copy_action(self): view = self.main_window.get_current_diagram_view() if view.is_focus(): items = view.selected_items copy_items = [] for i in items: copy_items.append(i) self.copy(copy_items) @action(name="edit-paste", stock_id="gtk-paste") def paste_action(self): view = self.main_window.get_current_diagram_view() diagram = self.main_window.get_current_diagram() if not view: return self.paste(diagram) view.unselect_all() for item in list(self._new_items.values()): view.select_item(item) PK!F'gaphor/services/diagramexportmanager.py"""Service dedicated to exporting diagrams to a variety of file formats.""" import os import logging import cairo from gaphas.freehand import FreeHandPainter from gaphas.painter import ItemPainter, BoundingBoxPainter from gaphas.view import View from zope.interface import implementer from gaphor.core import _, inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider from gaphor.ui.filedialog import FileDialog from gaphor.ui.interfaces import IUIComponent from gaphor.ui.questiondialog import QuestionDialog logger = logging.getLogger(__name__) @implementer(IService, IActionProvider) class DiagramExportManager(object): """ Service for exporting diagrams as images (SVG, PNG, PDF). """ component_registry = inject("component_registry") main_window = inject("main_window") properties = inject("properties") menu_xml = """ """ def __init__(self): self.action_group = build_action_group(self) def init(self, app): pass def shutdown(self): pass def update(self): pass def get_current_diagram(self): return self.component_registry.get_utility( IUIComponent, "diagrams" ).get_current_diagram() def save_dialog(self, diagram, title, ext): filename = (diagram.name or "export") + ext file_dialog = FileDialog(title, action="save", filename=filename) save = False while True: filename = file_dialog.selection if os.path.exists(filename): question = ( _( "The file %s already exists. Do you want to " "replace it with the file you are exporting " "to?" ) % filename ) question_dialog = QuestionDialog(question) answer = question_dialog.answer question_dialog.destroy() if answer: save = True break else: save = True break file_dialog.destroy() if save and filename: return filename def update_painters(self, view): logger.info("Updating painters") logger.debug("View is %s" % view) sloppiness = self.properties("diagram.sloppiness", 0) logger.debug("Sloppiness is %s" % sloppiness) if sloppiness: view.painter = FreeHandPainter(ItemPainter(), sloppiness) view.bounding_box_painter = FreeHandPainter( BoundingBoxPainter(), sloppiness ) else: view.painter = ItemPainter() def save_svg(self, filename, canvas): logger.info("Exporting to SVG") logger.debug("SVG path is %s" % filename) view = View(canvas) self.update_painters(view) # Update bounding boxes with a temporary CairoContext # (used for stuff like calculating font metrics) tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) view.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = view.bounding_box.width, view.bounding_box.height surface = cairo.SVGSurface(filename, w, h) cr = cairo.Context(surface) view.matrix.translate(-view.bounding_box.x, -view.bounding_box.y) view.paint(cr) cr.show_page() surface.flush() surface.finish() def save_png(self, filename, canvas): logger.info("Exporting to PNG") logger.debug("PNG path is %s" % filename) view = View(canvas) self.update_painters(view) # Update bounding boxes with a temporaly CairoContext # (used for stuff like calculating font metrics) tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) view.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = view.bounding_box.width, view.bounding_box.height surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(w + 1), int(h + 1)) cr = cairo.Context(surface) view.matrix.translate(-view.bounding_box.x, -view.bounding_box.y) view.paint(cr) cr.show_page() surface.write_to_png(filename) def save_pdf(self, filename, canvas): logger.info("Exporting to PDF") logger.debug("PDF path is %s" % filename) view = View(canvas) self.update_painters(view) # Update bounding boxes with a temporaly CairoContext # (used for stuff like calculating font metrics) tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) view.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = view.bounding_box.width, view.bounding_box.height surface = cairo.PDFSurface(filename, w, h) cr = cairo.Context(surface) view.matrix.translate(-view.bounding_box.x, -view.bounding_box.y) view.paint(cr) cr.show_page() surface.flush() surface.finish() @action( name="file-export-svg", label="Export to SVG", tooltip="Export the diagram to SVG", ) def save_svg_action(self): title = "Export diagram to SVG" ext = ".svg" diagram = self.get_current_diagram() filename = self.save_dialog(diagram, title, ext) if filename: self.save_svg(filename, diagram.canvas) @action( name="file-export-png", label="Export to PNG", tooltip="Export the diagram to PNG", ) def save_png_action(self): title = "Export diagram to PNG" ext = ".png" diagram = self.get_current_diagram() filename = self.save_dialog(diagram, title, ext) if filename: self.save_png(filename, diagram.canvas) @action( name="file-export-pdf", label="Export to PDF", tooltip="Export the diagram to PDF", ) def save_pdf_action(self): title = "Export diagram to PDF" ext = ".pdf" diagram = self.get_current_diagram() filename = self.save_dialog(diagram, title, ext) if filename: self.save_pdf(filename, diagram.canvas) # vim:sw=4:et: PK!2L3++$gaphor/services/elementdispatcher.py""" """ from zope.interface import implementer from zope import component from logging import getLogger from gaphor.core import inject from gaphor.interfaces import IService from gaphor.UML.interfaces import IElementChangeEvent, IModelFactoryEvent from gaphor import UML from gaphor.UML.interfaces import ( IAssociationSetEvent, IAssociationAddEvent, IAssociationDeleteEvent, ) class EventWatcher(object): """ A helper for easy registering and unregistering event handlers. """ element_dispatcher = inject("element_dispatcher") def __init__(self, element, default_handler=None): super(EventWatcher, self).__init__() self.element = element self.default_handler = default_handler self._watched_paths = dict() def watch(self, path, handler=None): """ Watch a certain path of elements starting with the DiagramItem. The handler is optional and will default the default provided at construction time. Watches should be set in the constructor, so they can be registered and unregistered in one shot. This interface is fluent(returns self). """ if handler: self._watched_paths[path] = handler elif self.default_handler: self._watched_paths[path] = self.default_handler else: raise ValueError("No handler provided for path " + path) return self def register_handlers(self): dispatcher = self.element_dispatcher element = self.element for path, handler in self._watched_paths.items(): dispatcher.register_handler(handler, element, path) def unregister_handlers(self, *args): """ Unregister handlers. Extra arguments are ignored (makes connecting to destroy signals much easier though). """ dispatcher = self.element_dispatcher for path, handler in self._watched_paths.items(): dispatcher.unregister_handler(handler) @implementer(IService) class ElementDispatcher(object): """ The Element based Dispatcher allows handlers to receive only events related to certain elements. Those elements should be registered to. Also a path should be provided, that is used to find those changes. The handlers are registered on their property attribute. This avoids subclass lookups and is pretty specific. As a result this dispatcher is tailored for dispatching events from the data model (IElementChangeEvent) For example: if you're a TransitionItem (UML.Presentation instance) and you're interested in the value of the guard attribute of the model element that's represented by this item (gaphor.UML.Transition), you can register a handler like this:: dispatcher.register_handler(element, 'guard.specification.value', self._handler) Note the '<' and '>'. This is because guard references ValueSpecification, which does not have a value attribute. Therefore the default reference type is overruled in favour of the LiteralSpecification. This dispatcher keeps track of the kind of events that are dispatched. The dispatcher table is updated accordingly (so the right handlers are fired every time). """ logger = getLogger("ElementDispatcher") component_registry = inject("component_registry") def __init__(self): # Table used to fire events: # (event.element, event.property): { handler: set(path, ..), ..} self._handlers = dict() # Fast resolution when handlers are disconnected # handler: [(element, property), ..] self._reverse = dict() def init(self, app): self.component_registry.register_handler(self.on_model_loaded) self.component_registry.register_handler(self.on_element_change_event) def shutdown(self): self.component_registry.unregister_handler(self.on_element_change_event) self.component_registry.unregister_handler(self.on_model_loaded) def _path_to_properties(self, element, path): """ Given a start element and a path, return a tuple of UML properties (association, attribute, etc.) representing the path. """ c = type(element) tpath = [] for attr in path.split("."): cname = "" if "<" in attr: assert attr.endswith(">"), '"%s" should end with ">"' % attr attr, cname = attr[:-1].split("<") prop = getattr(c, attr) tpath.append(prop) if cname: c = getattr(UML, cname) assert issubclass(c, prop.type), "%s should be a subclass of %s" % ( c, prop.type, ) else: c = prop.type return tuple(tpath) def _add_handlers(self, element, props, handler): """ Provided an element and a path of properties (props), register the handler for each property. """ property, remainder = props[0], props[1:] key = (element, property) # Register key try: handlers = self._handlers[key] except KeyError: handlers = dict() self._handlers[key] = handlers # Register handler and it's remaining paths try: remainders = handlers[handler] except KeyError: remainders = handlers[handler] = set() if remainder: remainders.add(remainder) # Also add them to the reverse table, easing disconnecting try: reverse = self._reverse[handler] except KeyError: reverse = [] self._reverse[handler] = reverse reverse.append(key) # Apply remaining path if remainder: if property.upper is "*" or property.upper > 1: for e in property._get(element): self._add_handlers(e, remainder, handler) else: e = property._get(element) if e and remainder: self._add_handlers(e, remainder, handler) def _remove_handlers(self, element, property, handler): """ Remove the handler of the path of elements. """ key = element, property handlers = self._handlers.get(key) if not handlers: return if property.upper is "*" or property.upper > 1: for remainder in handlers.get(handler, ()): for e in property._get(element): # log.debug(' Remove handler %s for key %s, element %s' % (handler, str(remainder[0].name), e)) self._remove_handlers(e, remainder[0], handler) else: for remainder in handlers.get(handler, ()): e = property._get(element) if e: # log.debug('*Remove handler %s for key %s, element %s' % (handler, str(remainder[0].name), e)) self._remove_handlers(e, remainder[0], handler) try: del handlers[handler] except KeyError: self.logger.warning( "Handler %s is not registered for %s.%s" % (handler, element, property) ) if not handlers: del self._handlers[key] def register_handler(self, handler, element, path): # self.logger.info('Registering handler') # self.logger.debug('Handler is %s' % handler) # self.logger.debug('Element is %s' % element) # self.logger.debug('Path is %s' % path) props = self._path_to_properties(element, path) self._add_handlers(element, props, handler) def unregister_handler(self, handler): """ Unregister a handler from the registy. """ # self.logger.info('Unregistering handler') # self.logger.debug('Handler is %s' % handler) try: reverse = reversed(self._reverse[handler]) except KeyError: return for key in reverse: try: handlers = self._handlers[key] except KeyError: pass else: try: del handlers[handler] except KeyError: pass if not handlers: del self._handlers[key] del self._reverse[handler] @component.adapter(IElementChangeEvent) def on_element_change_event(self, event): # self.logger.info('Handling IElementChangeEvent') # self.logger.debug('Element is %s' % event.element) # self.logger.debug('Property is %s' % event.property) handlers = self._handlers.get((event.element, event.property)) if handlers: # log.debug('') # log.debug('Element change for %s %s [%s]' % (str(type(event)), event.element, event.property)) # if hasattr(event, 'old_value'): # log.debug(' old value: %s' % (event.old_value)) # if hasattr(event, 'new_value'): # log.debug(' new value: %s' % (event.new_value)) for handler in handlers.keys(): try: handler(event) except Exception as e: self.logger.error("Problem executing handler %s" % handler) self.logger.error(e) # Handle add/removal of handlers based on the kind of event # Filter out handlers that have no remaining properties if IAssociationSetEvent.providedBy(event): for handler, remainders in handlers.items(): if remainders and event.old_value: for remainder in remainders: self._remove_handlers( event.old_value, remainder[0], handler ) if remainders and event.new_value: for remainder in remainders: self._add_handlers(event.new_value, remainder, handler) elif IAssociationAddEvent.providedBy(event): for handler, remainders in handlers.items(): for remainder in remainders: self._add_handlers(event.new_value, remainder, handler) elif IAssociationDeleteEvent.providedBy(event): for handler, remainders in handlers.items(): for remainder in remainders: self._remove_handlers(event.old_value, remainder[0], handler) @component.adapter(IModelFactoryEvent) def on_model_loaded(self, event): # self.logger.info('Handling IModelFactoryEvent') # self.logger.debug('Event is %s' % event) for key, value in list(self._handlers.items()): for h, remainders in list(value.items()): for remainder in remainders: self._add_handlers(key[0], (key[1],) + remainder, h) # for h in self._reverse.iterkeys(): # h(None) PK!=>=>gaphor/services/filemanager.py""" The file service is responsible for loading and saving the user data. """ import logging from zope import component from gi.repository import Gtk from zope.interface import implementer from gaphor import UML from gaphor.core import _, inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider, IServiceEvent from gaphor.misc.errorhandler import error_handler from gaphor.misc.gidlethread import GIdleThread, Queue from gaphor.misc.xmlwriter import XMLWriter from gaphor.storage import storage, verify from gaphor.ui.filedialog import FileDialog from gaphor.ui.questiondialog import QuestionDialog from gaphor.ui.statuswindow import StatusWindow DEFAULT_EXT = ".gaphor" MAX_RECENT = 10 log = logging.getLogger(__name__) @implementer(IServiceEvent) class FileManagerStateChanged(object): """ Event class used to send state changes on the ndo Manager. """ def __init__(self, service): self.service = service @implementer(IService, IActionProvider) class FileManager(object): """ The file service, responsible for loading and saving Gaphor models. """ component_registry = inject("component_registry") element_factory = inject("element_factory") main_window = inject("main_window") properties = inject("properties") menu_xml = """ """ def __init__(self): """File manager constructor. There is no current filename yet.""" self._filename = None def init(self, app): """File manager service initialization. The app parameter is the main application object. This method builds the action group in the file menu. The list of recent Gaphor files is then updated in the file menu.""" self.action_group = build_action_group(self) for name, label in (("file-recent-files", "_Recent files"),): action = Gtk.Action.new(name, label, None, None) action.set_property("hide-if-empty", False) self.action_group.add_action(action) for i in range(0, (MAX_RECENT - 1)): action = Gtk.Action.new("file-recent-%d" % i, None, None, None) action.set_property("visible", False) self.action_group.add_action(action) action.connect("activate", self.load_recent, i) self.update_recent_files() def shutdown(self): """Called when shutting down the file manager service.""" log.info("Shutting down") def get_filename(self): """Return the current file name. This method is used by the filename property.""" return self._filename def set_filename(self, filename): """Sets the current file name. This method is used by the filename property. Setting the current filename will update the recent file list.""" log.info("Setting current file") log.debug("Filename is %s" % filename) if filename != self._filename: self._filename = filename self.update_recent_files(filename) filename = property(get_filename, set_filename) def get_recent_files(self): """Returns the recent file list from the properties service. This method is used by the recent_files property.""" try: return self.properties.get("recent-files", []) except component.interfaces.ComponentLookupError: return [] def set_recent_files(self, recent_files): """Updates the properties service with the supplied list of recent files. This method is used by the recent_files property.""" log.info("Storing recent files") log.debug("Recent files are %s" % recent_files) try: self.properties.set("recent-files", recent_files) except component.interfaces.ComponentLookupError: return recent_files = property(get_recent_files, set_recent_files) def update_recent_files(self, new_filename=None): """Updates the list of recent files. If the new_filename parameter is supplied, it is added to the list of recent files. The default recent file placeholder actions are hidden. The real actions are then built using the recent file list.""" log.info("Updating recent files") log.debug("New file is %s" % new_filename) recent_files = self.recent_files if new_filename and new_filename not in recent_files: recent_files.insert(0, new_filename) recent_files = recent_files[0 : (MAX_RECENT - 1)] self.recent_files = recent_files for i in range(0, (MAX_RECENT - 1)): action = self.action_group.get_action("file-recent-%d" % i) action.set_property("visible", False) for i, filename in enumerate(recent_files): id = "file-recent%d" % i action = self.action_group.get_action("file-recent-%d" % i) action.props.label = "_%d. %s" % (i + 1, filename.replace("_", "__")) action.props.tooltip = "Load %s." % filename action.props.visible = True def load_recent(self, action, index): """Load the recent file at the specified index. This will trigger a FileManagerStateChanged event. The recent files are stored in the recent_files property.""" log.info("Loading recent file") log.debug("Action is %s" % action) log.debug("Index is %s" % index) filename = self.recent_files[index] self.load(filename) self.component_registry.handle(FileManagerStateChanged(self)) def load(self, filename): """Load the Gaphor model from the supplied file name. A status window displays the loading progress. The load generator updates the progress queue. The loader is passed to a GIdleThread which executes the load generator. If loading is successful, the filename is set.""" log.info("Loading file") log.debug("Path is %s" % filename) queue = Queue() try: main_window = self.main_window status_window = StatusWindow( _("Loading..."), _("Loading model from %s") % filename, parent=main_window.window, queue=queue, ) except component.interfaces.ComponentLookupError: status_window = None try: loader = storage.load_generator( filename.encode("utf-8"), self.element_factory ) worker = GIdleThread(loader, queue) worker.start() worker.wait() if worker.error: worker.reraise() self.filename = filename except: error_handler( message=_("Error while loading model from file %s") % filename ) raise finally: if status_window is not None: status_window.destroy() def verify_orphans(self): """Verify that no orphaned elements are saved. This method checks of there are any orphan references in the element factory. If orphans are found, a dialog is displayed asking the user if it is OK to unlink them.""" orphans = verify.orphan_references(self.element_factory) if orphans: main_window = self.main_window dialog = QuestionDialog( _( "The model contains some references" " to items that are not maintained." " Do you want to clean this before" " saving the model?" ), parent=main_window.window, ) answer = dialog.answer dialog.destroy() if not answer: for orphan in orphans: orphan.unlink() def verify_filename(self, filename): """Verify that the supplied filename is using the proper default extension. If not, the extension is added to the filename and returned.""" log.debug("Verifying file name") log.debug("File name is %s" % filename) if not filename.endswith(DEFAULT_EXT): filename = filename + DEFAULT_EXT return filename def save(self, filename): """Save the current UML model to the specified file name. Before writing the model file, this will verify that there are no orphan references. It will also verify that the filename has the correct extension. A status window is displayed while the GIdleThread is executed. This thread actually saves the model.""" log.info("Saving file") log.debug("File name is %s" % filename) if not filename or not len(filename): return self.verify_orphans() filename = self.verify_filename(filename) main_window = self.main_window queue = Queue() status_window = StatusWindow( _("Saving..."), _("Saving model to %s") % filename, parent=main_window.window, queue=queue, ) try: with open(filename.encode("utf-8"), "w") as out: saver = storage.save_generator(XMLWriter(out), self.element_factory) worker = GIdleThread(saver, queue) worker.start() worker.wait() if worker.error: worker.reraise() self.filename = filename except: error_handler(message=_("Error while saving model to file %s") % filename) raise finally: status_window.destroy() def _open_dialog(self, title): """Open a file chooser dialog to select a model file to open.""" filesel = Gtk.FileChooserDialog( title=title, action=Gtk.FileChooserAction.OPEN, buttons=( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK, ), ) filesel.set_transient_for(self.main_window.window) filter = Gtk.FileFilter() filter.set_name("Gaphor models") filter.add_pattern("*.gaphor") filesel.add_filter(filter) filter = Gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") filesel.add_filter(filter) if self.filename: filesel.set_current_name(self.filename) response = filesel.run() filename = filesel.get_filename() filesel.destroy() if not filename or response != Gtk.ResponseType.OK: return return filename @action(name="file-new", stock_id="gtk-new") def action_new(self): """The new model menu action. This action will create a new UML model. This will trigger a FileManagerStateChange event.""" element_factory = self.element_factory main_window = self.main_window if element_factory.size(): dialog = QuestionDialog( _( "Opening a new model will flush the" " currently loaded model.\nAny changes" " made will not be saved. Do you want to" " continue?" ), parent=main_window.window, ) answer = dialog.answer dialog.destroy() if not answer: return element_factory.flush() model = element_factory.create(UML.Package) model.name = _("New model") diagram = element_factory.create(UML.Diagram) diagram.package = model diagram.name = _("main") self.filename = None element_factory.notify_model() # main_window.select_element(diagram) # main_window.show_diagram(diagram) self.component_registry.handle(FileManagerStateChanged(self)) @action(name="file-new-template", label=_("New from template")) def action_new_from_template(self): """This menu action opens the new model from template dialog.""" filters = [ {"name": _("Gaphor Models"), "pattern": "*.gaphor"}, {"name": _("All Files"), "pattern": "*"}, ] file_dialog = FileDialog(_("New Gaphor Model From Template"), filters=filters) filename = file_dialog.selection file_dialog.destroy() log.debug(filename) if filename: self.load(filename) self.filename = None self.component_registry.handle(FileManagerStateChanged(self)) @action(name="file-open", stock_id="gtk-open") def action_open(self): """This menu action opens the standard model open dialog.""" filters = [ {"name": _("Gaphor Models"), "pattern": "*.gaphor"}, {"name": _("All Files"), "pattern": "*"}, ] file_dialog = FileDialog(_("Open Gaphor Model"), filters=filters) filename = file_dialog.selection file_dialog.destroy() log.debug(filename) if filename: self.load(filename) self.component_registry.handle(FileManagerStateChanged(self)) @action(name="file-save", stock_id="gtk-save") def action_save(self): """ Save the file. Depending on if there is a file name, either perform the save directly or present the user with a save dialog box. Returns True if the saving actually succeeded. """ filename = self.filename if filename: self.save(filename) self.component_registry.handle(FileManagerStateChanged(self)) return True else: return self.action_save_as() @action(name="file-save-as", stock_id="gtk-save-as") def action_save_as(self): """ Save the model in the element_factory by allowing the user to select a file name. Returns True if the saving actually happened. """ file_dialog = FileDialog( _("Save Gaphor Model As"), action="save", filename=self.filename ) filename = file_dialog.selection file_dialog.destroy() if filename: self.save(filename) self.component_registry.handle(FileManagerStateChanged(self)) return True return False PK!—gggaphor/services/helpservice.py"""About and help services. (help browser anyone?)""" import os import pkg_resources from gi.repository import GdkPixbuf from gi.repository import Gtk from zope.interface import implementer from gaphor.application import Application from gaphor.core import _, inject, action, build_action_group from gaphor.interfaces import IService, IActionProvider @implementer(IService, IActionProvider) class HelpService(object): menu_xml = """ """ main_window = inject("main_window") def __init__(self): pass def init(self, app): self.action_group = build_action_group(self) def shutdown(self): pass @action(name="help-about", stock_id="gtk-about") def about(self): logo_file = os.path.join( pkg_resources.get_distribution("gaphor").location, "gaphor", "ui", "pixmaps", "logo.png", ) logo = GdkPixbuf.Pixbuf.new_from_file(logo_file) version = Application.distribution.version about = Gtk.Dialog.new() about.set_title(_("About Gaphor")) about.set_modal(True) about.set_transient_for(self.main_window.window) about.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK) about.set_default_response(Gtk.ResponseType.OK) vbox = about.vbox image = Gtk.Image() image.set_from_pixbuf(logo) vbox.pack_start(image, True, True, 0) notebook = Gtk.Notebook() notebook.set_scrollable(True) notebook.set_border_width(4) notebook.set_tab_pos(Gtk.PositionType.BOTTOM) vbox.pack_start(notebook, True, True, 0) tab_vbox = Gtk.VBox() def add_label(text, padding_x=0, padding_y=0): label = Gtk.Label(label=text) label.set_property("use-markup", True) label.set_padding(padding_x, padding_y) label.set_justify(Gtk.Justification.CENTER) tab_vbox.pack_start(label, True, True, 0) add_label('version %s' % version) add_label( 'Gaphor is the simple modeling tool written in Python', 8, 8, ) add_label( 'Copyright (c) 2001-2019 Arjan J. Molenaar and Dan Yeaw', 8, 8, ) notebook.append_page(tab_vbox, Gtk.Label(label=_("About"))) tab_vbox = Gtk.VBox() add_label( "This software is published\n" "under the terms of the\n" 'Apache Software License v2.\n' "See the LICENSE.txt file for details.", 0, 8, ) notebook.append_page(tab_vbox, Gtk.Label(label=_("License"))) tab_vbox = Gtk.VBox() add_label( "Thanks to all the wonderful people that have contributed:\n\n" "Arjan Molenaar\n" "Artur Wroblewski\n" "Jeroen Vloothuis\n" "Dan Yeaw\n" "Enno Groeper\n" "Adam Boduch\n" "Jordi Mallach\n" "Ygor Mutti\n" "Alexis Howells\n" "Encolpe Degoute\n" "Melis Dogan" ) add_label("") notebook.append_page(tab_vbox, Gtk.Label(label=_("Contributors"))) vbox.show_all() about.run() about.destroy() PK!MB**gaphor/services/properties.py"""The properties module allows Gaphor properties to be saved to the local file system. These are things like preferences.""" import os import pprint import sys from zope import interface from gaphas.decorators import AsyncIO from gaphor.misc import get_config_dir from zope.interface import implementer from gaphor.core import inject from gaphor.interfaces import IService class IPropertyChangeEvent(interface.Interface): """A property changed event has a name, an old value, and a new value.""" name = interface.Attribute("The property name") old_value = interface.Attribute("The property value before the change") new_value = interface.Attribute("The property value after the change") @implementer(IPropertyChangeEvent) class PropertyChangeEvent(object): """This event is triggered any time a property is changed. This event holds the property name, the current value, and the new value.""" def __init__(self, name, old_value, new_value): self.name = name self.old_value = old_value self.new_value = new_value _no_default = object() @implementer(IService) class Properties(object): """The Properties class holds a collection of application wide properties. Properties are persisted to the local file system.""" component_registry = inject("component_registry") def __init__(self, backend=None): """Constructor. Initialize the Gaphor application object, the dictionary for storing properties in memory, and the storage backend. This defaults to FileBackend""" self._resources = {} self._backend = backend or FileBackend() def init(self, app): """Initialize the properties service. This will load any stored properties from the file system.""" self._backend.load(self._resources) def shutdown(self): """Shutdown the properties service. This will ensure that all properties are saved.""" self._backend.save(self._resources) def __call__(self, key, default=_no_default): """Retrieve the specified property. If the property doesn't exist, the default parameter is returned. This defaults to _no_default.""" return self.get(key, default) def save(self): """Save all properties by calling save() on the properties storage backend.""" self._backend.save(self._resources) def _items(self): """Return an iterator for all stored properties.""" return iter(self._resources.items()) def dump(self, stream=sys.stdout): """ TODO: define resources that are persistent (have to be saved and loaded. """ pprint.pprint(list(self._resources.items()), stream) def get(self, key, default=_no_default): """Locate a property. Resource should be the class of the resource to look for or a string. In case of a string the resource will be looked up in the GConf configuration.""" try: return self._resources[key] except KeyError: if default is _no_default: raise KeyError('No resource with name "%s"' % key) self.set(key, default) return default def set(self, key, value): """Set a property to a specific value. No smart things are done with classes and class names (like the resource() method does).""" resources = self._resources old_value = resources.get(key) if value != old_value: resources[key] = value self.component_registry.handle(PropertyChangeEvent(key, old_value, value)) self._backend.update(resources, key, value) class FileBackend(object): """Resource backend that stores data to a resource file ($HOME/.gaphor/resource).""" RESOURCE_FILE = "resources" def __init__(self, datadir=get_config_dir()): """Constructor. Initialize the directory used for storing properties.""" self.datadir = datadir def get_filename(self, create=False): """Return the current file used to store Gaphor properties. If the created parameter is set to True, the file is created if it doesn't exist. This defaults to False.""" datadir = self.datadir if create and not os.path.exists(datadir): os.mkdir(datadir) return os.path.join(datadir, self.RESOURCE_FILE) def load(self, resource): """Load resources from a file. Resources are saved like you do with a dict().""" filename = self.get_filename() if os.path.exists(filename) and os.path.isfile(filename): with open(filename) as ifile: data = ifile.read() for key, value in eval(data).items(): resource[key] = value def save(self, resource): """Save persist resources from the resources dictionary. @resource is the Resource instance @persistent is a list of persistent resource names. """ filename = self.get_filename(create=True) with open(filename, "w") as ofile: pprint.pprint(resource, ofile) @AsyncIO(single=True, timeout=500) def update(self, resource, key, value): """Update the properties file with any changes in the background.""" self.save(resource) PK!>w%gaphor/services/propertydispatcher.py""" """ from logging import getLogger from zope import component from zope.interface import implementer from gaphor.UML.interfaces import IElementChangeEvent from gaphor.core import inject from gaphor.interfaces import IService @implementer(IService) class PropertyDispatcher(object): """ The Propery Dispatcher allows classes to register on events originated by a specific element property, instead of the event type. This makes it easier for (for example) items to register on events from a specific class, rather than just AssociationChangeEvents, which could originate on a whole lot of classes. """ logger = getLogger("PropertyDispatcher") component_registry = inject("component_registry") def __init__(self): # Handlers is a dict of sets self._handlers = {} def init(self, app): self.logger.info("Starting") self.component_registry.register_handler(self.on_element_change_event) def shutdown(self): self.logger.info("Shutting down") self.component_registry.unregister_handler(self.on_element_change_event) def register_handler(self, property, handler, exact=False): self.logger.info("Registring handler") self.logger.debug("Property is %s" % property) self.logger.debug("Handler is %s" % handler) try: self._handlers[property].add(handler) except KeyError: self._handlers[property] = set([handler]) def unregister_handler(self, property, handler): self.logger.info("Unregistering handler") self.logger.debug("Property is %s" % property) self.logger.debug("Handler is %s" % handler) s = self._handlers.get(property) if s: s.discard(handler) @component.adapter(IElementChangeEvent) def on_element_change_event(self, event): self.logger.info("Handling IElementChangeEvent") property = event.property self.logger.debug("Property is %s" % property) s = self._handlers.get(property) if not s: return for handler in s: try: handler(event) except Exception as e: log.error("problem executing handler %s" % handler, exc_info=True) PK!77#gaphor/services/sanitizerservice.py""" The Sanitize module is dedicated to adapters (stuff) that keeps the model clean and in sync with diagrams. """ from logging import getLogger from zope import component from zope.interface import implementer from gaphor import UML from gaphor.UML.interfaces import IAssociationDeleteEvent, IAssociationSetEvent from gaphor.core import inject from gaphor.interfaces import IService @implementer(IService) class SanitizerService(object): """ Does some background cleanup jobs, such as removing elements from the model that have no presentations (and should have some). """ logger = getLogger("Sanitizer") component_registry = inject("component_registry") element_factory = inject("element_factory") property_dispatcher = inject("property_dispatcher") def __init__(self): pass def init(self, app=None): self.component_registry.register_handler(self._unlink_on_presentation_delete) self.component_registry.register_handler(self._unlink_on_stereotype_delete) self.component_registry.register_handler(self._unlink_on_extension_delete) self.component_registry.register_handler(self._disconnect_extension_end) def shutdown(self): self.component_registry.unregister_handler(self._unlink_on_presentation_delete) self.component_registry.unregister_handler(self._unlink_on_stereotype_delete) self.component_registry.unregister_handler(self._unlink_on_extension_delete) self.component_registry.unregister_handler(self._disconnect_extension_end) @component.adapter(IAssociationDeleteEvent) def _unlink_on_presentation_delete(self, event): """ Unlink the model element if no more presentations link to the `item`'s subject or the deleted item was the only item currently linked. """ self.logger.debug("Handling IAssociationDeleteEvent") # self.logger.debug('Property is %s' % event.property.name) # self.logger.debug('Element is %s' % event.element) # self.logger.debug('Old value is %s' % event.old_value) if event.property is UML.Element.presentation: old_presentation = event.old_value if old_presentation and not event.element.presentation: event.element.unlink() def perform_unlink_for_instances(self, st, meta): self.logger.debug("Performing unlink for instances") # self.logger.debug('Stereotype is %s' % st) # self.logger.debug('Meta is %s' % meta) inst = UML.model.find_instances(self.element_factory, st) for i in list(inst): for e in i.extended: if not meta or isinstance(e, meta): i.unlink() @component.adapter(IAssociationDeleteEvent) def _unlink_on_extension_delete(self, event): """ Remove applied stereotypes when extension is deleted. """ self.logger.debug("Handling IAssociationDeleteEvent") # self.logger.debug('Property is %s' % event.property.name) # self.logger.debug('Element is %s' % event.element) # self.logger.debug('Old value is %s' % event.old_value) if ( isinstance(event.element, UML.Extension) and event.property is UML.Association.memberEnd and event.element.memberEnd ): p = event.element.memberEnd[0] ext = event.old_value if isinstance(p, UML.ExtensionEnd): p, ext = ext, p st = ext.type meta = p.type and getattr(UML, p.type.name) self.perform_unlink_for_instances(st, meta) @component.adapter(IAssociationSetEvent) def _disconnect_extension_end(self, event): self.logger.debug("Handling IAssociationSetEvent") # self.logger.debug('Property is %s' % event.property.name) # self.logger.debug('Element is %s' % event.element) # self.logger.debug('Old value is %s' % event.old_value) if event.property is UML.ExtensionEnd.type and event.old_value: ext = event.element p = ext.opposite if not p: return st = event.old_value meta = getattr(UML, p.type.name) self.perform_unlink_for_instances(st, meta) @component.adapter(IAssociationDeleteEvent) def _unlink_on_stereotype_delete(self, event): """ Remove applied stereotypes when stereotype is deleted. """ self.logger.debug("Handling IAssociationDeleteEvent") # self.logger.debug('Property is %s' % event.property) # self.logger.debug('Element is %s' % event.element) # self.logger.debug('Old value is %s' % event.old_value) if event.property is UML.InstanceSpecification.classifier: if isinstance(event.old_value, UML.Stereotype): event.element.unlink() PK!Ѷ.  "gaphor/services/serviceregistry.py""" The service registry is the place where services can be registered and retrieved. Our good old NameServicer. """ from logging import getLogger from zope import component from gaphor.core import inject from gaphor.interfaces import IService class ServiceRegistry(object): component_registry = inject("component_registry") logger = getLogger("ServiceRegistry") def __init__(self): self._uninitialized_services = {} def init(self, app=None): self.logger.info("Starting") def shutdown(self): self.logger.info("Shutting down") def load_services(self, services=None): """ Load services from resources. Services are registered as utilities in zope.component. Service should provide an interface gaphor.interfaces.IService. """ self.logger.info("Loading services") for ep in pkg_resources.iter_entry_points("gaphor.services"): cls = ep.load() if not IService.implementedBy(cls): raise NameError("Entry point %s doesn" "t provide IService" % ep.name) if services is None or ep.name in services: srv = cls() self._uninitialized_services[ep.name] = srv def init_all_services(self): self.logger.info("Initializing services") while self._uninitialized_services: self.init_service(next(iter(self._uninitialized_services.keys()))) def init_service(self, name): """ Initialize a not yet initialized service. Raises ComponentLookupError if the service has nor been found """ self.logger.info("Initializing service") self.logger.debug("Service name is %s" % name) try: srv = self._uninitialized_services.pop(name) except KeyError: raise component.ComponentLookupError(IService, name) else: srv.init(self) self.component_registry.register_utility(srv, IService, name) self.handle(ServiceInitializedEvent(name, srv)) return srv def get_service(self, name): try: return self.component_registry.get_utility(IService, name) except component.ComponentLookupError: return self.init_service(name) # vim: sw=4:et:ai PK!!gaphor/services/tests/__init__.pyPK!bb+gaphor/services/tests/test_actionmanager.pyimport unittest class ActionManagerTestCase(unittest.TestCase): def setUp(self): pass def tearDown(self): pass def testLoadAll(self): from gaphor.application import Application Application.init() am = Application.get_service("action_manager") ui = am.ui_manager.get_ui() print(ui) PK!療ō )gaphor/services/tests/test_copyservice.pyfrom gaphor import UML from gaphor.diagram import items from gaphor.services.copyservice import CopyService from gaphor.application import Application from gaphor.tests.testcase import TestCase class CopyServiceTestCase(TestCase): services = TestCase.services + [ "main_window", "action_manager", "properties", "undo_manager", "ui_manager", ] def test_init(self): service = CopyService() service.init(Application) # No exception? ok! def test_copy(self): service = CopyService() service.init(Application) ef = self.element_factory diagram = ef.create(UML.Diagram) ci = diagram.create(items.CommentItem, subject=ef.create(UML.Comment)) service.copy([ci]) assert diagram.canvas.get_all_items() == [ci] service.paste(diagram) assert len(diagram.canvas.get_all_items()) == 2, diagram.canvas.get_all_items() def test_copy_named_item(self): service = CopyService() service.init(Application) ef = self.element_factory diagram = ef.create(UML.Diagram) c = diagram.create(items.ClassItem, subject=ef.create(UML.Class)) c.subject.name = "Name" from gi.repository import GLib self.assertEqual(0, GLib.main_depth()) diagram.canvas.update_now() i = list(diagram.canvas.get_all_items()) self.assertEqual(1, len(i), i) self.assertEqual("Name", i[0]._name.text) service.copy([c]) assert diagram.canvas.get_all_items() == [c] service.paste(diagram) i = diagram.canvas.get_all_items() self.assertEqual(2, len(i), i) diagram.canvas.update_now() self.assertEqual("Name", i[0]._name.text) self.assertEqual("Name", i[1]._name.text) def _skip_test_copy_paste_undo(self): """ Test if copied data is undoable. """ from gaphor.storage.verify import orphan_references service = CopyService() service.init(Application) # Setting the stage: ci1 = self.create(items.ClassItem, UML.Class) ci2 = self.create(items.ClassItem, UML.Class) a = self.create(items.AssociationItem) self.connect(a, a.head, ci1) self.connect(a, a.tail, ci2) self.assertTrue(a.subject) self.assertTrue(a.head_end.subject) self.assertTrue(a.tail_end.subject) # The act: copy and paste, perform undo afterwards service.copy([ci1, ci2, a]) service.paste(self.diagram) all_items = list(self.diagram.canvas.get_all_items()) self.assertEqual(6, len(all_items)) self.assertFalse(orphan_references(self.element_factory)) self.assertSame(all_items[0].subject, all_items[3].subject) self.assertSame(all_items[1].subject, all_items[4].subject) self.assertSame(all_items[2].subject, all_items[5].subject) undo_manager = self.get_service("undo_manager") undo_manager.undo_transaction() self.assertEqual(3, len(self.diagram.canvas.get_all_items())) self.assertFalse(orphan_references(self.element_factory)) PK!qU~2gaphor/services/tests/test_diagramexportmanager.pyimport unittest from gaphor.application import Application from gaphor.services.diagramexportmanager import DiagramExportManager class DiagramExportManagerTestCase(unittest.TestCase): def setUp(self): Application.init( services=[ "main_window", "properties", "element_factory", "diagram_export_manager", "action_manager", "ui_manager", ] ) def shutDown(self): Application.shutdown() def test_init(self): des = DiagramExportManager() des.init(None) def test_init_from_application(self): Application.get_service("diagram_export_manager") Application.get_service("main_window") PK!@M0606/gaphor/services/tests/test_elementdispatcher.pyfrom gaphor.tests import TestCase from gaphor import UML from gaphor.application import Application from gaphor.services.elementdispatcher import ElementDispatcher class ElementDispatcherTestCase(TestCase): def setUp(self): super(ElementDispatcherTestCase, self).setUp() self.events = [] self.dispatcher = ElementDispatcher() self.dispatcher.init(Application) def tearDown(self): self.dispatcher.shutdown() super(ElementDispatcherTestCase, self).tearDown() def _handler(self, event): self.events.append(event) def test_register_handler(self): dispatcher = self.dispatcher element = UML.Class() dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) assert len(dispatcher._handlers) == 1 assert list(dispatcher._handlers.keys())[0] == ( element, UML.Class.ownedOperation, ) # Add some properties: # 1: element.ownedOperation = UML.Operation() # 2: p = element.ownedOperation[0].formalParameter = UML.Parameter() # 3: p.name = "func" dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) self.assertEqual(3, len(self.events)) self.assertEqual(3, len(dispatcher._handlers)) def test_register_handler_twice(self): """ Multiple registrations have no effect. """ dispatcher = self.dispatcher element = UML.Class() # Add some properties: element.ownedOperation = UML.Operation() p = element.ownedOperation[0].formalParameter = UML.Parameter() dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) n_handlers = len(dispatcher._handlers) self.assertEqual(0, len(self.events)) dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) self.assertEqual(n_handlers, len(dispatcher._handlers)) dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) self.assertEqual(n_handlers, len(dispatcher._handlers)) dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) self.assertEqual(n_handlers, len(dispatcher._handlers)) p.name = "func" self.assertEqual(1, len(self.events)) def test_unregister_handler(self): # First some setup: dispatcher = self.dispatcher element = UML.Class() o = element.ownedOperation = UML.Operation() p = element.ownedOperation[0].formalParameter = UML.Parameter() p.name = "func" dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) assert len(dispatcher._handlers) == 3 assert dispatcher._handlers[element, UML.Class.ownedOperation] assert dispatcher._handlers[o, UML.Operation.parameter] assert dispatcher._handlers[p, UML.Parameter.name] dispatcher.unregister_handler(self._handler) assert len(dispatcher._handlers) == 0, dispatcher._handlers assert len(dispatcher._reverse) == 0, dispatcher._reverse # assert dispatcher._handlers.keys()[0] == (element, UML.Class.ownedOperation) # Should not fail here too: dispatcher.unregister_handler(self._handler) def test_notification(self): """ Test notifications with Class object. """ dispatcher = self.dispatcher element = UML.Class() o = element.ownedOperation = UML.Operation() p = element.ownedOperation[0].formalParameter = UML.Parameter() p.name = "func" dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) assert len(dispatcher._handlers) == 3 assert not self.events element.ownedOperation = UML.Operation() assert len(self.events) == 1, self.events assert len(dispatcher._handlers) == 4 p.name = "othername" assert len(self.events) == 2, self.events del element.ownedOperation[o] assert len(dispatcher._handlers) == 2 def test_notification_2(self): """ Test notifications with Transition object. """ dispatcher = self.dispatcher element = UML.Transition() g = element.guard = UML.Constraint() dispatcher.register_handler(self._handler, element, "guard.specification") assert len(dispatcher._handlers) == 2 assert not self.events assert (element.guard, UML.Constraint.specification) in list( dispatcher._handlers.keys() ), list(dispatcher._handlers.keys()) g.specification = "x" assert len(self.events) == 1, self.events element.guard = UML.Constraint() assert len(self.events) == 2, self.events assert len(dispatcher._handlers) == 2, len(dispatcher._handlers) assert (element.guard, UML.Constraint.specification) in list( dispatcher._handlers.keys() ) def test_notification_of_change(self): """ Test notifications with Transition object. """ dispatcher = self.dispatcher element = UML.Transition() g = element.guard = UML.Constraint() dispatcher.register_handler(self._handler, element, "guard.specification") assert len(dispatcher._handlers) == 2 assert not self.events g.specification = "x" assert len(self.events) == 1, self.events element.guard = UML.Constraint() assert len(self.events) == 2, self.events def test_notification_with_composition(self): """ Test unregister with composition. Use Class.ownedOperation.precondition. """ dispatcher = self.dispatcher element = UML.Class() o = element.ownedOperation = UML.Operation() p = element.ownedOperation[0].precondition = UML.Constraint() p.name = "func" dispatcher.register_handler( self._handler, element, "ownedOperation.precondition.name" ) assert len(dispatcher._handlers) == 3 assert not self.events del element.ownedOperation[o] assert len(dispatcher._handlers) == 1 def test_notification_with_incompatible_elements(self): """ Test unregister with composition. Use Class.ownedOperation.precondition. """ dispatcher = self.dispatcher element = UML.Transition() g = element.guard = UML.Constraint() dispatcher.register_handler(self._handler, element, "guard.specification") assert len(dispatcher._handlers) == 2 assert not self.events assert (element.guard, UML.Constraint.specification) in list( dispatcher._handlers.keys() ), list(dispatcher._handlers.keys()) g.specification = "x" assert len(self.events) == 1, self.events g.specification = "a" assert len(self.events) == 2, self.events from gaphor.UML import Element from gaphor.UML.properties import association from gaphor.services.elementdispatcher import EventWatcher class A(Element): pass A.one = association("one", A, lower=0, upper=1, composite=True) A.two = association("two", A, lower=0, upper=2, composite=True) class ElementDispatcherAsServiceTestCase(TestCase): services = TestCase.services + ["element_dispatcher"] def setUp(self): super(ElementDispatcherAsServiceTestCase, self).setUp() self.events = [] self.dispatcher = Application.get_service("element_dispatcher") def tearDown(self): super(ElementDispatcherAsServiceTestCase, self).tearDown() def _handler(self, event): self.events.append(event) def test_notification(self): """ Test notifications with Class object. """ dispatcher = self.dispatcher element = UML.Class() o = element.ownedOperation = UML.Operation() p = element.ownedOperation[0].formalParameter = UML.Parameter() p.name = "func" dispatcher.register_handler( self._handler, element, "ownedOperation.parameter.name" ) assert len(dispatcher._handlers) == 3 assert not self.events element.ownedOperation = UML.Operation() assert len(self.events) == 1, self.events assert len(dispatcher._handlers) == 4 p.name = "othername" assert len(self.events) == 2, self.events del element.ownedOperation[o] assert len(dispatcher._handlers) == 2 def test_association_notification(self): """ Test notifications with Class object. Tricky case where no events are fired. """ dispatcher = self.dispatcher element = UML.Association() p1 = element.memberEnd = UML.Property() p2 = element.memberEnd = UML.Property() assert len(element.memberEnd) == 2 print(element.memberEnd) dispatcher.register_handler(self._handler, element, "memberEnd.name") assert len(dispatcher._handlers) == 3, len(dispatcher._handlers) assert not self.events p1.name = "foo" assert len(self.events) == 1, (self.events, dispatcher._handlers) assert len(dispatcher._handlers) == 3 p1.name = "othername" assert len(self.events) == 2, self.events p1.name = "othername" assert len(self.events) == 2, self.events def test_association_notification_complex(self): """ Test notifications with Class object. Tricky case where no events are fired. """ dispatcher = self.dispatcher element = UML.Association() p1 = element.memberEnd = UML.Property() p2 = element.memberEnd = UML.Property() p1.lowerValue = "0" p1.upperValue = "1" p2.lowerValue = "1" p2.upperValue = "*" assert len(element.memberEnd) == 2 print(element.memberEnd) base = "memberEnd." dispatcher.register_handler(self._handler, element, base + "name") dispatcher.register_handler(self._handler, element, base + "aggregation") dispatcher.register_handler(self._handler, element, base + "classifier") dispatcher.register_handler(self._handler, element, base + "lowerValue") dispatcher.register_handler(self._handler, element, base + "upperValue") assert len(dispatcher._handlers) == 11, len(dispatcher._handlers) assert not self.events p1.name = "foo" assert len(self.events) == 1, (self.events, dispatcher._handlers) assert len(dispatcher._handlers) == 11 p1.name = "othername" assert len(self.events) == 2, self.events def test_diamond(self): """ Test diamond shaped dependencies a -> b -> c, a -> b' -> c """ a = A() watcher = EventWatcher(a, self._handler) watcher.watch("one.two.one.two") # watcher.watch('one.one.one.one') watcher.register_handlers() a.one = A() a.one.two = A() a.one.two = A() a.one.two[0].one = A() a.one.two[1].one = a.one.two[0].one a.one.two[0].one.two = A() self.assertEqual(6, len(self.events)) a.unlink() watcher.unregister_handlers() watcher.unregister_handlers() def test_big_diamond(self): """ Test diamond shaped dependencies a -> b -> c -> d, a -> b' -> c' -> d """ a = A() watcher = EventWatcher(a, self._handler) watcher.watch("one.two.one.two") # watcher.watch('one.one.one.one') watcher.register_handlers() a.one = A() a.one.two = A() a.one.two = A() a.one.two[0].one = A() a.one.two[1].one = A() a.one.two[0].one.two = A() a.one.two[1].one.two = a.one.two[0].one.two[0] self.assertEqual(7, len(self.events)) a.unlink() watcher.unregister_handlers() watcher.unregister_handlers() self.assertEqual(0, len(self.dispatcher._handlers)) def test_braking_big_diamond(self): """ Test diamond shaped dependencies a -> b -> c -> d, a -> b' -> c' -> d """ a = A() watcher = EventWatcher(a, self._handler) watcher.watch("one.two.one.two") # watcher.watch('one.one.one.one') watcher.register_handlers() a.one = A() a.one.two = A() a.one.two = A() a.one.two[0].one = A() a.one.two[1].one = A() a.one.two[0].one.two = A() a.one.two[1].one.two = a.one.two[0].one.two[0] self.assertEqual(7, len(self.events)) self.assertEqual(6, len(self.dispatcher._handlers)) del a.one.two[0].one # a.unlink() watcher.unregister_handlers() watcher.unregister_handlers() self.assertEqual(0, len(self.dispatcher._handlers)) def test_cyclic(self): """ Test cyclic dependency a -> b -> c -> a. """ a = A() watcher = EventWatcher(a, self._handler) watcher.watch("one.two.one.two") # watcher.watch('one.one.one.one') watcher.register_handlers() a.one = A() a.one.two = A() a.one.two = A() a.one.two[0].one = a self.assertEqual(4, len(self.events)) # a.one.two[0].one.two = A() # a.one.two[0].one.two = A() a.unlink() self.assertEqual(1, len(self.dispatcher._handlers)) # vim: sw=4:et:ai PK!Gdd)gaphor/services/tests/test_filemanager.pyimport unittest from gaphor.application import Application class FileManagerTestCase(unittest.TestCase): def setUp(self): Application.init( services=[ "file_manager", "element_factory", "properties", "main_window", "action_manager", "ui_manager", ] ) self.recent_files_backup = Application.get_service("properties").get( "recent-files" ) def tearDown(self): Application.get_service("properties").set( "recent-files", self.recent_files_backup ) Application.shutdown() def test_recent_files(self): fileman = Application.get_service("file_manager") properties = Application.get_service("properties") # ensure the recent_files list is empty: properties.set("recent-files", []) fileman.update_recent_files() for i in range(0, 9): a = fileman.action_group.get_action("file-recent-%d" % i) assert a assert a.get_property("visible") == False, "%s, %d" % ( a.get_property("visible"), i, ) fileman.filename = "firstfile" a = fileman.action_group.get_action("file-recent-%d" % 0) assert a assert a.get_property("visible") == True assert a.props.label == "_1. firstfile", a.props.label for i in range(1, 9): a = fileman.action_group.get_action("file-recent-%d" % i) assert a assert a.get_property("visible") == False PK!(gaphor/services/tests/test_properties.pyfrom unittest import TestCase from gaphor.services.properties import Properties, FileBackend import tempfile # class MockApplication(object): # # def __init__(self): # self.events = [] # # def handle(self, event): # self.events.append(event) # class TestProperties(TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() backend = FileBackend(self.tmpdir) self.properties = Properties(backend) # self.app = MockApplication() self.properties.init(self.app) def shutDown(self): self.properties.shutdown() os.remove(os.path.join(self.tmpdir, FileBackend.RESOURCE_FILE)) os.rmdir(self.tmpdir) # def test_properties(self): # prop = self.properties # assert not self.app.events # # prop.set('test1', 2) # assert len(self.app.events) == 1, self.app.events # event = self.app.events[0] # assert 'test1' == event.name # assert None is event.old_value # assert 2 is event.new_value # assert 2 == prop('test1') # # prop.set('test1', 2) # assert len(self.app.events) == 1 # # prop.set('test1', 'foo') # assert len(self.app.events) == 2 # event = self.app.events[1] # assert 'test1' == event.name # assert 2 is event.old_value # assert 'foo' is event.new_value # assert 'foo' == prop('test1') # # assert 3 == prop('test2', 3) # assert 3 == prop('test2', 4) PK!.gaphor/services/tests/test_sanitizerservice.pyfrom gaphor.tests import TestCase from gaphor import UML from gaphor.diagram import items class SanitizerServiceTest(TestCase): services = TestCase.services + ["sanitizer"] def test_presentation_delete(self): """ Remove element if the last instance of an item is deleted. """ ef = self.element_factory klassitem = self.create(items.ClassItem, UML.Class) klass = klassitem.subject assert klassitem.subject.presentation[0] is klassitem assert klassitem.canvas # Delete presentation here: klassitem.unlink() assert not klassitem.canvas assert klass not in self.element_factory.lselect() def test_stereotype_attribute_delete(self): """ This test was applicable to the Sanitizer service, but is now resolved by a tweak in the data model (Instances diagram). """ factory = self.element_factory create = factory.create # Set the stage # metaklass = create(UML.Class) # metaklass.name = 'Class' klass = create(UML.Class) stereotype = create(UML.Stereotype) st_attr = self.element_factory.create(UML.Property) stereotype.ownedAttribute = st_attr # ext = UML.model.create_extension(factory, metaklass, stereotype) # Apply stereotype to class and create slot instspec = UML.model.apply_stereotype(factory, klass, stereotype) slot = UML.model.add_slot(factory, instspec, st_attr) # Now, what happens if the attribute is deleted: self.assertTrue(st_attr in stereotype.ownedMember) self.assertTrue(slot in instspec.slot) st_attr.unlink() self.assertEqual([], list(stereotype.ownedMember)) self.assertEqual([], list(instspec.slot)) def test_extension_disconnect(self): factory = self.element_factory create = factory.create # Set the stage metaklass = create(UML.Class) metaklass.name = "Class" klass = create(UML.Class) stereotype = create(UML.Stereotype) st_attr = self.element_factory.create(UML.Property) stereotype.ownedAttribute = st_attr ext = UML.model.create_extension(factory, metaklass, stereotype) # Apply stereotype to class and create slot instspec = UML.model.apply_stereotype(factory, klass, stereotype) slot = UML.model.add_slot(factory, instspec, st_attr) self.assertTrue(stereotype in klass.appliedStereotype[:].classifier) # Causes set event del ext.ownedEnd.type self.assertEqual([], list(klass.appliedStereotype)) def test_extension_deletion(self): factory = self.element_factory create = factory.create # Set the stage metaklass = create(UML.Class) metaklass.name = "Class" klass = create(UML.Class) stereotype = create(UML.Stereotype) st_attr = self.element_factory.create(UML.Property) stereotype.ownedAttribute = st_attr ext = UML.model.create_extension(factory, metaklass, stereotype) # Apply stereotype to class and create slot instspec = UML.model.apply_stereotype(factory, klass, stereotype) slot = UML.model.add_slot(factory, instspec, st_attr) self.assertTrue(stereotype in klass.appliedStereotype[:].classifier) ext.unlink() self.assertEqual([], list(klass.appliedStereotype)) def test_extension_deletion_with_2_metaclasses(self): factory = self.element_factory create = factory.create # Set the stage metaklass = create(UML.Class) metaklass.name = "Class" metaiface = create(UML.Class) metaiface.name = "Interface" klass = create(UML.Class) iface = create(UML.Interface) stereotype = create(UML.Stereotype) st_attr = self.element_factory.create(UML.Property) stereotype.ownedAttribute = st_attr ext1 = UML.model.create_extension(factory, metaklass, stereotype) ext2 = UML.model.create_extension(factory, metaiface, stereotype) # Apply stereotype to class and create slot instspec1 = UML.model.apply_stereotype(factory, klass, stereotype) instspec2 = UML.model.apply_stereotype(factory, iface, stereotype) slot = UML.model.add_slot(factory, instspec1, st_attr) self.assertTrue(stereotype in klass.appliedStereotype[:].classifier) self.assertTrue(klass in self.element_factory) ext1.unlink() self.assertEqual([], list(klass.appliedStereotype)) self.assertTrue(klass in self.element_factory) self.assertEqual([instspec2], list(iface.appliedStereotype)) def test_stereotype_deletion(self): factory = self.element_factory create = factory.create # Set the stage metaklass = create(UML.Class) metaklass.name = "Class" klass = create(UML.Class) stereotype = create(UML.Stereotype) st_attr = self.element_factory.create(UML.Property) stereotype.ownedAttribute = st_attr ext = UML.model.create_extension(factory, metaklass, stereotype) # Apply stereotype to class and create slot instspec = UML.model.apply_stereotype(factory, klass, stereotype) slot = UML.model.add_slot(factory, instspec, st_attr) self.assertTrue(stereotype in klass.appliedStereotype[:].classifier) stereotype.unlink() self.assertEqual([], list(klass.appliedStereotype)) PK!²--)gaphor/services/tests/test_undomanager.py""" Test the UndoManager. """ from gaphor.tests.testcase import TestCase from gaphor.services.undomanager import UndoManager from gaphor.transaction import Transaction from gaphor.application import Application class TestUndoManager(TestCase): def test_transactions(self): undo_manager = UndoManager() undo_manager.init(None) assert not undo_manager._current_transaction # undo_manager.begin_transaction() tx = Transaction() # assert undo_manager._transaction_depth == 1 assert undo_manager._current_transaction current = undo_manager._current_transaction # undo_manager.begin_transaction() tx2 = Transaction() # assert undo_manager._transaction_depth == 2 # assert undo_manager._transaction_depth == 1 assert undo_manager._current_transaction is current # undo_manager.commit_transaction() tx2.commit() # assert undo_manager._transaction_depth == 1 assert undo_manager._current_transaction is current # undo_manager.commit_transaction() tx.commit() # assert undo_manager._transaction_depth == 0 assert undo_manager._current_transaction is None undo_manager.shutdown() def test_not_in_transaction(self): undo_manager = UndoManager() undo_manager.init(Application) action = object() undo_manager.add_undo_action(action) assert undo_manager._current_transaction is None undo_manager.begin_transaction() undo_manager.add_undo_action(action) assert undo_manager._current_transaction assert undo_manager.can_undo() assert len(undo_manager._current_transaction._actions) == 1 undo_manager.shutdown() def test_actions(self): undone = [0] def undo_action(undone=undone): # print 'undo_action called' undone[0] = 1 undo_manager.add_undo_action(redo_action) def redo_action(undone=undone): # print 'redo_action called' undone[0] = -1 undo_manager.add_undo_action(undo_action) undo_manager = UndoManager() undo_manager.init(Application) # undo_manager.begin_transaction() tx = Transaction() undo_manager.add_undo_action(undo_action) assert undo_manager._current_transaction assert undo_manager.can_undo() assert len(undo_manager._current_transaction._actions) == 1 # undo_manager.commit_transaction() tx.commit() undo_manager.undo_transaction() assert not undo_manager.can_undo(), undo_manager._undo_stack assert undone[0] == 1, undone undone[0] = 0 assert undo_manager.can_redo(), undo_manager._redo_stack undo_manager.redo_transaction() assert not undo_manager.can_redo() assert undo_manager.can_undo() assert undone[0] == -1, undone undo_manager.shutdown() def test_undo_attribute(self): import types from gaphor.UML.properties import attribute from gaphor.UML.element import Element undo_manager = UndoManager() undo_manager.init(Application) class A(Element): attr = attribute("attr", bytes, default="default") a = A() assert a.attr == "default", a.attr undo_manager.begin_transaction() a.attr = "five" undo_manager.commit_transaction() assert a.attr == "five" undo_manager.undo_transaction() assert a.attr == "default", a.attr undo_manager.redo_transaction() assert a.attr == "five" undo_manager.shutdown() def test_undo_association_1_x(self): from gaphor.UML.properties import association from gaphor.UML.element import Element undo_manager = UndoManager() undo_manager.init(Application) class A(Element): pass class B(Element): pass A.one = association("one", B, 0, 1, opposite="two") B.two = association("two", A, 0, 1) a = A() b = B() assert a.one is None assert b.two is None undo_manager.begin_transaction() a.one = b undo_manager.commit_transaction() assert a.one is b assert b.two is a assert len(undo_manager._undo_stack) == 1 assert len(undo_manager._undo_stack[0]._actions) == 2, undo_manager._undo_stack[ 0 ]._actions undo_manager.undo_transaction() assert a.one is None assert b.two is None assert undo_manager.can_redo() assert len(undo_manager._redo_stack) == 1 assert len(undo_manager._redo_stack[0]._actions) == 2, undo_manager._redo_stack[ 0 ]._actions undo_manager.redo_transaction() assert len(undo_manager._undo_stack) == 1 assert len(undo_manager._undo_stack[0]._actions) == 2, undo_manager._undo_stack[ 0 ]._actions assert b.two is a assert a.one is b undo_manager.shutdown() def test_undo_association_1_n(self): from gaphor.UML.properties import association from gaphor.UML.element import Element undo_manager = UndoManager() undo_manager.init(Application) class A(Element): pass class B(Element): pass A.one = association("one", B, lower=0, upper=1, opposite="two") B.two = association("two", A, lower=0, upper="*", opposite="one") a1 = A() a2 = A() b1 = B() b2 = B() undo_manager.begin_transaction() b1.two = a1 undo_manager.commit_transaction() assert a1 in b1.two assert b1 is a1.one assert len(undo_manager._undo_stack) == 1 assert len(undo_manager._undo_stack[0]._actions) == 2, undo_manager._undo_stack[ 0 ]._actions undo_manager.undo_transaction() assert len(b1.two) == 0 assert a1.one is None assert undo_manager.can_redo() assert len(undo_manager._redo_stack) == 1 assert len(undo_manager._redo_stack[0]._actions) == 2, undo_manager._redo_stack[ 0 ]._actions undo_manager.redo_transaction() assert a1 in b1.two assert b1 is a1.one undo_manager.begin_transaction() b1.two = a2 undo_manager.commit_transaction() assert a1 in b1.two assert a2 in b1.two assert b1 is a1.one assert b1 is a2.one undo_manager.shutdown() def test_element_factory_undo(self): from gaphor.UML.element import Element ef = self.element_factory ef.flush() undo_manager = UndoManager() undo_manager.init(Application) undo_manager.begin_transaction() p = ef.create(Element) assert undo_manager._current_transaction assert undo_manager._current_transaction._actions assert undo_manager.can_undo() undo_manager.commit_transaction() assert undo_manager.can_undo() assert ef.size() == 1 undo_manager.undo_transaction() assert not undo_manager.can_undo() assert undo_manager.can_redo() assert ef.size() == 0 undo_manager.redo_transaction() assert undo_manager.can_undo() assert not undo_manager.can_redo() assert ef.size() == 1 assert ef.lselect()[0] is p undo_manager.shutdown() def test_element_factory_rollback(self): from gaphor.UML.element import Element ef = self.element_factory ef.flush() undo_manager = UndoManager() undo_manager.init(Application) undo_manager.begin_transaction() p = ef.create(Element) assert undo_manager._current_transaction assert undo_manager._current_transaction._actions assert undo_manager.can_undo() undo_manager.rollback_transaction() assert not undo_manager.can_undo() assert ef.size() == 0 undo_manager.shutdown() def test_uml_associations(self): from zope import component from gaphor.UML.interfaces import IAssociationChangeEvent from gaphor.UML.properties import association, derivedunion from gaphor.UML import Element class A(Element): is_unlinked = False def unlink(self): self.is_unlinked = True Element.unlink(self) A.a1 = association("a1", A, upper=1) A.a2 = association("a2", A, upper=1) A.b1 = association("b1", A, upper="*") A.b2 = association("b2", A, upper="*") A.b3 = association("b3", A, upper=1) A.derived_a = derivedunion("derived_a", 0, 1, A.a1, A.a2) A.derived_b = derivedunion("derived_b", 0, "*", A.b1, A.b2, A.b3) events = [] @component.adapter(IAssociationChangeEvent) def handler(event, events=events): events.append(event) compreg = Application.get_service("component_registry") compreg.register_handler(handler) try: a = A() undo_manager = UndoManager() undo_manager.init(Application) undo_manager.begin_transaction() a.a1 = A() undo_manager.commit_transaction() assert len(events) == 1, events assert events[0].property is A.a1 assert undo_manager.can_undo() undo_manager.undo_transaction() assert not undo_manager.can_undo() assert undo_manager.can_redo() assert len(events) == 2, events assert events[1].property is A.a1 finally: compreg.unregister_handler(handler) undo_manager.shutdown() def test_redo_stack(self): from gaphor.UML.element import Element undo_manager = UndoManager() undo_manager.init(Application) undo_manager.begin_transaction() ef = self.element_factory ef.flush() p = ef.create(Element) assert undo_manager._current_transaction assert undo_manager._current_transaction._actions assert undo_manager.can_undo() undo_manager.commit_transaction() assert undo_manager.can_undo() assert ef.size() == 1, ef.size() with Transaction(): q = ef.create(Element) assert undo_manager.can_undo() assert not undo_manager.can_redo() assert ef.size() == 2 undo_manager.undo_transaction() assert undo_manager.can_undo() self.assertEqual(1, len(undo_manager._undo_stack)) self.assertEqual(1, len(undo_manager._redo_stack)) assert undo_manager.can_redo() assert ef.size() == 1 undo_manager.undo_transaction() assert not undo_manager.can_undo() assert undo_manager.can_redo() self.assertEqual(0, len(undo_manager._undo_stack)) self.assertEqual(2, len(undo_manager._redo_stack)) # assert ef.size() == 0 undo_manager.redo_transaction() self.assertEqual(1, len(undo_manager._undo_stack)) self.assertEqual(1, len(undo_manager._redo_stack)) assert undo_manager.can_undo() assert undo_manager.can_redo() assert ef.size() == 1 undo_manager.redo_transaction() assert undo_manager.can_undo() assert not undo_manager.can_redo() assert ef.size() == 2 assert p in ef.lselect() undo_manager.shutdown() PK! 66gaphor/services/undomanager.py# vim:sw=4:et: """ Undo management for Gaphor. Undoing and redoing actions is managed through the UndoManager. An undo action should be a callable object (called with no arguments). An undo action should return a callable object that acts as redo function. If None is returned the undo action is considered to be the redo action as well. NOTE: it would be nice to use actions in conjunction with functools.partial. """ from logging import getLogger from zope import component from gaphas import state from zope.interface import implementer from gaphor.UML.event import ( ElementCreateEvent, ElementDeleteEvent, AssociationSetEvent, AssociationAddEvent, AssociationDeleteEvent, ) from gaphor.UML.interfaces import ( IElementDeleteEvent, IAttributeChangeEvent, IModelFactoryEvent, ) from gaphor.action import action, build_action_group from gaphor.core import inject from gaphor.event import ActionExecuted from gaphor.event import TransactionBegin, TransactionCommit, TransactionRollback from gaphor.interfaces import IService, IServiceEvent, IActionProvider from gaphor.transaction import Transaction, transactional class ActionStack(object): """ A transaction. Every action that is added between a begin_transaction() and a commit_transaction() call is recorded in a transaction, so it can be played back when a transaction is executed. This executing a transaction has the effect of performing the actions recorded, which will typically undo actions performed by the user. """ logger = getLogger("UndoManager.ActionStack") def __init__(self): self._actions = [] def add(self, action): self._actions.append(action) def can_execute(self): return self._actions and True or False @transactional def execute(self): self._actions.reverse() for action in self._actions: try: action() except Exception as e: self.logger.error( "Error while undoing action %s" % action, exc_info=True ) @implementer(IServiceEvent) class UndoManagerStateChanged(object): """ Event class used to send state changes on the ndo Manager. """ def __init__(self, service): self.service = service @implementer(IService, IActionProvider) class UndoManager(object): """ Simple transaction manager for Gaphor. This transaction manager supports nested transactions. The Undo manager sports an undo and a redo stack. Each stack contains a set of actions that can be executed, just by calling them (e.i action()) If something is returned by an action, that is considered the callable to be used to undo or redo the last performed action. """ menu_xml = """ """ component_registry = inject("component_registry") logger = getLogger("UndoManager") def __init__(self): self._undo_stack = [] self._redo_stack = [] self._stack_depth = 20 self._current_transaction = None self.action_group = build_action_group(self) def init(self, app): self.logger.info("Starting") self.component_registry.register_handler(self.reset) self.component_registry.register_handler(self.begin_transaction) self.component_registry.register_handler(self.commit_transaction) self.component_registry.register_handler(self.rollback_transaction) self.component_registry.register_handler(self._action_executed) self._register_undo_handlers() self._action_executed() def shutdown(self): self.logger.info("Shutting down") self.component_registry.unregister_handler(self.reset) self.component_registry.unregister_handler(self.begin_transaction) self.component_registry.unregister_handler(self.commit_transaction) self.component_registry.unregister_handler(self.rollback_transaction) self.component_registry.unregister_handler(self._action_executed) self._unregister_undo_handlers() def clear_undo_stack(self): self._undo_stack = [] self._current_transaction = None def clear_redo_stack(self): del self._redo_stack[:] @component.adapter(IModelFactoryEvent) def reset(self, event=None): self.clear_redo_stack() self.clear_undo_stack() self._action_executed() @component.adapter(TransactionBegin) def begin_transaction(self, event=None): """ Add an action to the current transaction """ assert not self._current_transaction self._current_transaction = ActionStack() def add_undo_action(self, action): """ Add an action to undo. An action """ if self._current_transaction: self._current_transaction.add(action) self.component_registry.handle(UndoManagerStateChanged(self)) # TODO: should this be placed here? self._action_executed() @component.adapter(TransactionCommit) def commit_transaction(self, event=None): assert self._current_transaction if self._current_transaction.can_execute(): # Here: self.clear_redo_stack() self._undo_stack.append(self._current_transaction) while len(self._undo_stack) > self._stack_depth: del self._undo_stack[0] self._current_transaction = None self.component_registry.handle(UndoManagerStateChanged(self)) self._action_executed() @component.adapter(TransactionRollback) def rollback_transaction(self, event=None): """ Roll back the transaction we're in. """ assert self._current_transaction # Store stacks undo_stack = list(self._undo_stack) erroneous_tx = self._current_transaction self._current_transaction = None try: with Transaction(): try: erroneous_tx.execute() except Exception as e: self.logger.error("Could not roolback transaction") self.logger.error(e) finally: # Discard all data collected in the rollback "transaction" self._undo_stack = undo_stack self.component_registry.handle(UndoManagerStateChanged(self)) self._action_executed() def discard_transaction(self): self._current_transaction = None self.component_registry.handle(UndoManagerStateChanged(self)) self._action_executed() @action(name="edit-undo", stock_id="gtk-undo", accel="z") def undo_transaction(self): if not self._undo_stack: return if self._current_transaction: log.warning("Trying to undo a transaction, while in a transaction") self.commit_transaction() transaction = self._undo_stack.pop() # Store stacks undo_stack = list(self._undo_stack) redo_stack = list(self._redo_stack) self._undo_stack = [] try: with Transaction(): transaction.execute() finally: # Restore stacks and put latest tx on the redo stack self._redo_stack = redo_stack if self._undo_stack: self._redo_stack.extend(self._undo_stack) self._undo_stack = undo_stack while len(self._redo_stack) > self._stack_depth: del self._redo_stack[0] self.component_registry.handle(UndoManagerStateChanged(self)) self._action_executed() @action(name="edit-redo", stock_id="gtk-redo", accel="y") def redo_transaction(self): if not self._redo_stack: return transaction = self._redo_stack.pop() redo_stack = list(self._redo_stack) try: with Transaction(): transaction.execute() finally: self._redo_stack = redo_stack self.component_registry.handle(UndoManagerStateChanged(self)) self._action_executed() def in_transaction(self): return self._current_transaction is not None def can_undo(self): return bool(self._current_transaction or self._undo_stack) def can_redo(self): return bool(self._redo_stack) @component.adapter(ActionExecuted) def _action_executed(self, event=None): self.action_group.get_action("edit-undo").set_sensitive(self.can_undo()) self.action_group.get_action("edit-redo").set_sensitive(self.can_redo()) ## ## Undo Handlers ## def _gaphas_undo_handler(self, event): self.add_undo_action(lambda: state.saveapply(*event)) def _register_undo_handlers(self): self.logger.debug("Registering undo handlers") self.component_registry.register_handler(self.undo_create_event) self.component_registry.register_handler(self.undo_delete_event) self.component_registry.register_handler(self.undo_attribute_change_event) self.component_registry.register_handler(self.undo_association_set_event) self.component_registry.register_handler(self.undo_association_add_event) self.component_registry.register_handler(self.undo_association_delete_event) # # Direct revert-statements from gaphas to the undomanager state.observers.add(state.revert_handler) state.subscribers.add(self._gaphas_undo_handler) def _unregister_undo_handlers(self): self.logger.debug("Unregistering undo handlers") self.component_registry.unregister_handler(self.undo_create_event) self.component_registry.unregister_handler(self.undo_delete_event) self.component_registry.unregister_handler(self.undo_attribute_change_event) self.component_registry.unregister_handler(self.undo_association_set_event) self.component_registry.unregister_handler(self.undo_association_add_event) self.component_registry.unregister_handler(self.undo_association_delete_event) state.observers.discard(state.revert_handler) state.subscribers.discard(self._gaphas_undo_handler) @component.adapter(ElementCreateEvent) def undo_create_event(self, event): factory = event.service # A factory is not always present, e.g. for DiagramItems if not factory: return element = event.element def _undo_create_event(): try: del factory._elements[element.id] except KeyError: pass # Key was probably already removed in an unlink call self.component_registry.handle(ElementDeleteEvent(factory, element)) self.add_undo_action(_undo_create_event) @component.adapter(IElementDeleteEvent) def undo_delete_event(self, event): factory = event.service # A factory is not always present, e.g. for DiagramItems if not factory: return element = event.element assert factory, "No factory defined for %s (%s)" % (element, factory) def _undo_delete_event(): factory._elements[element.id] = element self.component_registry.handle(ElementCreateEvent(factory, element)) self.add_undo_action(_undo_delete_event) @component.adapter(IAttributeChangeEvent) def undo_attribute_change_event(self, event): attribute = event.property element = event.element value = event.old_value def _undo_attribute_change_event(): attribute._set(element, value) self.add_undo_action(_undo_attribute_change_event) @component.adapter(AssociationSetEvent) def undo_association_set_event(self, event): association = event.property element = event.element value = event.old_value # print 'got new set event', association, element, value def _undo_association_set_event(): # print 'undoing action', element, value # Tell the association it should not need to let the opposite # side connect (it has it's own signal) association._set(element, value, from_opposite=True) self.add_undo_action(_undo_association_set_event) @component.adapter(AssociationAddEvent) def undo_association_add_event(self, event): association = event.property element = event.element value = event.new_value def _undo_association_add_event(): # print 'undoing action', element, value # Tell the association it should not need to let the opposite # side connect (it has it's own signal) association._del(element, value, from_opposite=True) self.add_undo_action(_undo_association_add_event) @component.adapter(AssociationDeleteEvent) def undo_association_delete_event(self, event): association = event.property element = event.element value = event.old_value def _undo_association_delete_event(): # print 'undoing action', element, value # Tell the assoctaion it should not need to let the opposite # side connect (it has it's own signal) association._set(element, value, from_opposite=True) self.add_undo_action(_undo_association_delete_event) PK!LLgaphor/storage/__init__.py"""Storage module, for loading and saving of model files.""" # vim:sw=4:et PK!(#C+C+gaphor/storage/parser.py"""Gaphor file reader. This module contains only one interesting function: parse(filename) which returns a dictionary of ID -> pairs. A parsed_object is one of element, canvas or canvasitem. A parsed_object contains values and references. values is a dictionary of name -> value pairs. A value contains a string with the value read from the model file. references contains a list of name -> reference_list pairs, where reference_list is a list of ID's. element objects can contain a canvas object (which is the case for elements of type Diagram). Each element has a type, which corresponds to a class name in the gaphor.UML module. Elements also have a unique ID, by which they are referered to in the dictionary returned by parse(). canvas does not have an ID, but contains a list of canvasitems (which is a list of real canvasitem objects, not references). canvasitem objects can also contain a list of canvasitems (canvasitems can be nested). They also have a unique ID by which they have been added to the dictionary returned by parse(). Each canvasitem has a type, which maps to a class name in the gaphor.diagram module. The generator parse_generator(filename, loader) may be used if the loading takes a long time. The yielded values are the percentage of the file read. """ __all__ = ["parse", "ParserException"] import io import os from xml.sax import handler from gaphor.misc.odict import odict class base(object): """Simple base class for element, canvas and canvasitem. """ def __init__(self): self.values = {} self.references = {} def __getattr__(self, key): try: return self.__getitem__(key) except KeyError as e: raise AttributeError(e) def __getitem__(self, key): try: return self.values[key] except: return self.references[key] def get(self, key): try: return self.__getitem__(key) except: return None class element(base): def __init__(self, id, type): base.__init__(self) self.id = id self.type = type self.canvas = None class canvas(base): def __init__(self): base.__init__(self) self.canvasitems = [] class canvasitem(base): def __init__(self, id, type): base.__init__(self) self.id = id self.type = type self.canvasitems = [] XMLNS = "http://gaphor.sourceforge.net/model" class ParserException(Exception): pass # Loader state: [ ROOT, # Expect 'gaphor' element GAPHOR, # Expect UML classes (tag name is the UML class name) ELEMENT, # Expect properties of UML object DIAGRAM, # Expect properties of Diagram object + canvas CANVAS, # Expect canvas properties + tags ITEM, # Expect item attributes and nested items ATTR, # Reading contents of an attribute (such as a or ) VAL, # Redaing contents of a tag REFLIST, # In a REF, # Reading contents of a tag ] = range(10) class GaphorLoader(handler.ContentHandler): """Create a list of elements. an element may contain a canvas and a canvas may contain canvas items. Each element can have values and references to other elements. """ def __init__(self): handler.ContentHandler.__init__(self) # make sure all variables are initialized: self.startDocument() def push(self, element, state): """Add an element to the item stack. """ self.__stack.append((element, state)) def pop(self): """Return the last item on the stack. The item is removed from the stack. """ return self.__stack.pop()[0] def peek(self, depth=1): """Return the last item on the stack. The item is not removed. """ return self.__stack[-1 * depth][0] def state(self): """Return the current state of the parser. """ try: return self.__stack[-1][1] except IndexError: return ROOT def endDTD(self): pass def startDocument(self): """Start of document: all our attributes are initialized. """ self.version = None self.gaphor_version = None self.elements = odict() # map id: element/canvasitem self.__stack = [] self.text = "" def endDocument(self): if len(self.__stack) != 0: raise ParserException("Invalid XML document.") def startElement(self, name, attrs): self.text = "" state = self.state() # Read a element class. The name of the tag is the class name: if state == GAPHOR: id = attrs["id"] e = element(id, name) assert id not in list(self.elements.keys()), "%s already defined" % ( id ) # , self.elements[id]) self.elements[id] = e self.push(e, name == "Diagram" and DIAGRAM or ELEMENT) # Special treatment for the tag in a Diagram: elif state == DIAGRAM and name == "canvas": c = canvas() self.peek().canvas = c self.push(c, CANVAS) # Items in a canvas are referenced by the tag: elif state in (CANVAS, ITEM) and name == "item": id = attrs["id"] c = canvasitem(id, attrs["type"]) assert id not in list(self.elements.keys()), "%s already defined" % id self.elements[id] = c self.peek().canvasitems.append(c) self.push(c, ITEM) # Store the attribute name on the stack, so we can use it later # to store the , or content: elif state in (ELEMENT, DIAGRAM, CANVAS, ITEM): # handle 'normal' attributes self.push(name, ATTR) # Reference list: elif state == ATTR and name == "reflist": self.push(self.peek(), REFLIST) # Reference with multiplicity 1: elif state == ATTR and name == "ref": n = self.peek(1) # Fetch the element instance from the stack r = self.peek(2).references[n] = attrs["refid"] self.push(None, REF) # Reference with multiplicity *: elif state == REFLIST and name == "ref": n = self.peek(1) # Fetch the element instance from the stack r = self.peek(3).references refid = attrs["refid"] try: r[n].append(refid) except KeyError: r[n] = [refid] self.push(None, REF) # We need to get the text within the tag: elif state == ATTR and name == "val": self.push(None, VAL) # The tag is the toplevel tag: elif state == ROOT and name == "gaphor": assert attrs["version"] in ("3.0",) self.version = attrs["version"] self.gaphor_version = attrs.get("gaphor-version") if not self.gaphor_version: self.gaphor_version = attrs.get("gaphor_version") self.push(None, GAPHOR) else: raise ParserException( "Invalid XML: tag <%s> not known (state = %s)" % (name, state) ) def endElement(self, name): # Put the text on the value if self.state() == VAL: # Two levels up: the attribute name n = self.peek(2) # Three levels up: the element instance (element or canvasitem) self.peek(3).values[n] = self.text self.pop() def startElementNS(self, name, qname, attrs): if not name[0] or name[0] == XMLNS: a = {} for key, val in list(attrs.items()): a[key[1]] = val self.startElement(name[1], a) def endElementNS(self, name, qname): if not name[0] or name[0] == XMLNS: self.endElement(name[1]) def characters(self, content): """Read characters.""" self.text = self.text + content def parse(filename): """Parse a file and return a dictionary ID:element/canvasitem. """ loader = GaphorLoader() for x in parse_generator(filename, loader): pass return loader.elements def parse_generator(filename, loader): """The generator based version of parse(). parses the file filename and load it with ContentHandler loader. """ assert isinstance(loader, GaphorLoader), "loader should be a GaphorLoader" from xml.sax import make_parser parser = make_parser() parser.setFeature(handler.feature_namespaces, 1) parser.setContentHandler(loader) for percentage in parse_file(filename, parser): yield percentage class ProgressGenerator(object): """A generator that yields the progress of taking from a file input object and feeding it into an output object. The supplied file object is neither opened not closed by this generator. The file object is assumed to already be opened for reading and that it will be closed elsewhere.""" def __init__(self, input, output, block_size=512): """Initialize the progress generator. The input parameter is a file object. The output parameter is usually a SAX parser but can be anything that implements a feed() method. The block size is the size of each block that is read from the input.""" self.input = input self.output = output self.block_size = block_size if isinstance(self.input, io.IOBase): orig_pos = self.input.tell() self.file_size = self.input.seek(0, 2) self.input.seek(orig_pos, os.SEEK_SET) elif isinstance(self.input, str): self.file_size = len(self.input) def __iter__(self): """Return a generator that yields the progress of reading data from the input and feeding it into the output. The progress yielded in each iteration is the percentage of data read, relative to the to input file size.""" block = self.input.read(self.block_size) read_size = len(block) while block: self.output.feed(block) block = self.input.read(self.block_size) read_size += len(block) yield (read_size * 100) / self.file_size def parse_file(filename, parser): """Parse the supplied file using the supplied parser. The parser parameter should be a GaphorLoader instance. The filename parameter can be an open file descriptor instance or the name of a file. The progress percentage of the parser is yielded.""" is_fd = True print(filename) if isinstance(filename, io.IOBase): file_obj = filename else: is_fd = False file_obj = io.open(filename, "r") for progress in ProgressGenerator(file_obj, parser): yield progress parser.close() if not is_fd: file_obj.close() PK!^44gaphor/storage/storage.py""" Load and save Gaphor models to Gaphors own XML format. Three functions are exported: load(filename) load a model from a file save(filename) store the current model in a file """ __all__ = ["load", "save"] import gc import logging import os.path import io import gaphas from gaphor import UML from gaphor import diagram from gaphor.UML.collection import collection from gaphor.UML.elementfactory import ElementChangedEventBlocker from gaphor.application import Application, NotInitializedError from gaphor.diagram import items from gaphor.i18n import _ from gaphor.storage import parser # import gaphor.adapters.connectors package, so diagram items can find # their appropriate connectors (i.e. diagram line requires this); # this allows external scripts to load diagram properly... or should # this be done using services? i.e. request storage service, which should # depend on connectors service? from gaphor.adapters import connectors FILE_FORMAT_VERSION = "3.0" NAMESPACE_MODEL = "http://gaphor.sourceforge.net/model" log = logging.getLogger(__name__) def save(writer=None, factory=None, status_queue=None): for status in save_generator(writer, factory): if status_queue: status_queue(status) def save_generator(writer, factory): """ Save the current model using @writer, which is a gaphor.misc.xmlwriter.XMLWriter instance. """ # Maintain a set of id's, one for elements, one for references. # Write only to file if references is a subset of elements def save_reference(name, value): """ Save a value as a reference to another element in the model. This applies to both UML as well as canvas items. """ # Save a reference to the object: if value.id: writer.startElement(name, {}) writer.startElement("ref", {"refid": value.id}) writer.endElement("ref") writer.endElement(name) def save_collection(name, value): """ Save a list of references. """ if len(value) > 0: writer.startElement(name, {}) writer.startElement("reflist", {}) for v in value: # save_reference(name, v) if v.id: writer.startElement("ref", {"refid": v.id}) writer.endElement("ref") writer.endElement("reflist") writer.endElement(name) def save_value(name, value): """ Save a value (attribute). """ if value is not None: writer.startElement(name, {}) writer.startElement("val", {}) if isinstance(value, str): writer.characters(value) elif isinstance(value, bool): # Write booleans as 0/1. writer.characters(str(int(value))) else: writer.characters(str(value)) writer.endElement("val") writer.endElement(name) def save_element(name, value): """ Save attributes and references from items in the gaphor.UML module. A value may be a primitive (string, int), a gaphor.UML.collection (which contains a list of references to other UML elements) or a gaphas.Canvas (which contains canvas items). """ # log.debug('saving element: %s|%s %s' % (name, value, type(value))) if isinstance(value, (UML.Element, gaphas.Item)): save_reference(name, value) elif isinstance(value, collection): save_collection(name, value) elif isinstance(value, gaphas.Canvas): writer.startElement("canvas", {}) value.save(save_canvasitem) writer.endElement("canvas") else: save_value(name, value) def save_canvasitem(name, value, reference=False): """ Save attributes and references in a gaphor.diagram.* object. The extra attribute reference can be used to force UML """ # log.debug('saving canvasitem: %s|%s %s' % (name, value, type(value))) if isinstance(value, collection) or ( isinstance(value, (list, tuple)) and reference == True ): save_collection(name, value) elif reference: save_reference(name, value) elif isinstance(value, gaphas.Item): writer.startElement( "item", {"id": value.id, "type": value.__class__.__name__} ) value.save(save_canvasitem) # save subitems for child in value.canvas.get_children(value): save_canvasitem(None, child) writer.endElement("item") elif isinstance(value, UML.Element): save_reference(name, value) else: save_value(name, value) writer.startDocument() writer.startPrefixMapping("", NAMESPACE_MODEL) writer.startElementNS( (NAMESPACE_MODEL, "gaphor"), None, { (NAMESPACE_MODEL, "version"): FILE_FORMAT_VERSION, (NAMESPACE_MODEL, "gaphor-version"): Application.distribution.version, }, ) size = factory.size() n = 0 for e in list(factory.values()): clazz = e.__class__.__name__ assert e.id writer.startElement(clazz, {"id": str(e.id)}) e.save(save_element) writer.endElement(clazz) n += 1 if n % 25 == 0: yield (n * 100) / size # writer.endElement('gaphor') writer.endElementNS((NAMESPACE_MODEL, "gaphor"), None) writer.endPrefixMapping("") writer.endDocument() def load_elements(elements, factory, status_queue=None): for status in load_elements_generator(elements, factory): if status_queue: status_queue(status) def load_elements_generator(elements, factory, gaphor_version=None): """ Load a file and create a model if possible. Exceptions: IOError, ValueError. """ # TODO: restructure loading code, first load model, then add canvas items log.debug(_("Loading %d elements...") % len(elements)) # The elements are iterated three times: size = len(elements) * 3 def update_status_queue(_n=[0]): n = _n[0] = _n[0] + 1 if n % 30 == 0: return (n * 100) / size # First create elements and canvas items in the factory # The elements are stored as attribute 'element' on the parser objects: def create_canvasitems(canvas, canvasitems, parent=None): """ Canvas is a read gaphas.Canvas, items is a list of parser.canvasitem's """ for item in canvasitems: cls = getattr(items, item.type) item.element = diagram.create_as(cls, item.id) canvas.add(item.element, parent=parent) assert canvas.get_parent(item.element) is parent create_canvasitems(canvas, item.canvasitems, parent=item.element) for id, elem in list(elements.items()): st = update_status_queue() if st: yield st if isinstance(elem, parser.element): cls = getattr(UML, elem.type) # log.debug('Creating UML element for %s (%s)' % (elem, elem.id)) elem.element = factory.create_as(cls, id) if elem.canvas is not None: elem.element.canvas.block_updates = True create_canvasitems(elem.element.canvas, elem.canvas.canvasitems) elif not isinstance(elem, parser.canvasitem): raise ValueError( 'Item with id "%s" and type %s can not be instantiated' % (id, type(elem)) ) # load attributes and create references: for id, elem in list(elements.items()): st = update_status_queue() if st: yield st # Ensure that all elements have their element instance ready... assert hasattr(elem, "element") # load attributes and references: for name, value in list(elem.values.items()): try: elem.element.load(name, value) except: log.error( "Loading value %s (%s) for element %s failed." % (name, value, elem.element) ) raise for name, refids in list(elem.references.items()): if isinstance(refids, list): for refid in refids: try: ref = elements[refid] except: raise ValueError( "Invalid ID for reference (%s) for element %s.%s" % (refid, elem.type, name) ) else: try: elem.element.load(name, ref.element) except: log.error( "Loading %s.%s with value %s failed" % (type(elem.element).__name__, name, ref.element.id) ) raise else: try: ref = elements[refids] except: raise ValueError("Invalid ID for reference (%s)" % refids) else: try: elem.element.load(name, ref.element) except: log.error( "Loading %s.%s with value %s failed" % (type(elem.element).__name__, name, ref.element.id) ) raise # Before version 0.7.2 there was only decision node (no merge nodes). # This node could have many incoming and outgoing flows (edges). # According to UML specification decision node has no more than one # incoming node. # # Now, we have implemented merge node, which can have many incoming # flows. We also support combining of decision and merge nodes as # described in UML specification. # # Data model, loaded from file, is updated automatically, so there is # no need for special function. for d in factory.select(lambda e: isinstance(e, UML.Diagram)): # update_now() is implicitly called when lock is released d.canvas.block_updates = False # do a postload: for id, elem in list(elements.items()): st = update_status_queue() if st: yield st elem.element.postload() factory.notify_model() def load(filename, factory, status_queue=None): """ Load a file and create a model if possible. Optionally, a status queue function can be given, to which the progress is written (as status_queue(progress)). """ for status in load_generator(filename, factory): if status_queue: status_queue(status) def load_generator(filename, factory): """ Load a file and create a model if possible. This function is a generator. It will yield values from 0 to 100 (%) to indicate its progression. """ if isinstance(filename, io.IOBase): log.info("Loading file from file descriptor") else: log.info("Loading file %s" % os.path.basename(filename)) try: # Use the incremental parser and yield the percentage of the file. loader = parser.GaphorLoader() for percentage in parser.parse_generator(filename, loader): pass if percentage: yield percentage / 2 else: yield percentage elements = loader.elements gaphor_version = loader.gaphor_version except Exception as e: log.error("File could no be parsed", exc_info=True) raise if version_lower_than(gaphor_version, (0, 17, 0)): raise ValueError( "Gaphor model version should be at least 0.17.0 (found {})".format( gaphor_version ) ) try: component_registry = Application.get_service("component_registry") except NotInitializedError: component_registry = None try: factory.flush() gc.collect() log.info("Read %d elements from file" % len(elements)) if component_registry: component_registry.register_subscription_adapter(ElementChangedEventBlocker) try: for percentage in load_elements_generator( elements, factory, gaphor_version ): if percentage: yield percentage / 2 + 50 else: yield percentage except Exception as e: raise finally: if component_registry: component_registry.unregister_subscription_adapter( ElementChangedEventBlocker ) gc.collect() yield 100 except Exception as e: log.info("file %s could not be loaded" % filename) raise def version_lower_than(gaphor_version, version): """ if version_lower_than('0.3.0', (0, 15, 0)): ... """ parts = gaphor_version.split(".") try: return tuple(map(int, parts)) < version except ValueError: # We're having a -dev, -pre, -beta, -alpha or whatever version parts = parts[:-1] return tuple(map(int, parts)) <= version # vim: sw=4:et:ai PK! gaphor/storage/tests/__init__.pyPK!Yx\*\*$gaphor/storage/tests/test_storage.py""" Unittest the storage and parser modules """ import io import os import os.path import re from io import StringIO import pkg_resources from gaphor import UML from gaphor.diagram import items from gaphor.misc.xmlwriter import XMLWriter from gaphor.storage import storage from gaphor.tests.testcase import TestCase class PseudoFile(object): def __init__(self): self.data = "" def write(self, data): self.data += data def close(self): pass class StorageTestCase(TestCase): def test_version_check(self): from gaphor.storage.storage import version_lower_than self.assertTrue(version_lower_than("0.3.0", (0, 15, 0))) self.assertTrue(version_lower_than("0", (0, 15, 0))) self.assertTrue(version_lower_than("0.14", (0, 15, 0))) self.assertTrue(version_lower_than("0.14.1111", (0, 15, 0))) self.assertFalse(version_lower_than("0.15.0", (0, 15, 0))) self.assertFalse(version_lower_than("1.33.0", (0, 15, 0))) self.assertTrue(version_lower_than("0.15.0.b123", (0, 15, 0))) self.assertTrue(version_lower_than("0.14.0.b1", (0, 15, 0))) self.assertTrue(version_lower_than("0.15.b1", (0, 15, 0))) self.assertFalse(version_lower_than("0.16.b1", (0, 15, 0))) self.assertFalse(version_lower_than("0.15.0.b2", (0, 14, 99))) def test_save_uml(self): """Saving gaphor.UML model elements. """ self.element_factory.create(UML.Package) self.element_factory.create(UML.Diagram) self.element_factory.create(UML.Comment) self.element_factory.create(UML.Class) out = PseudoFile() storage.save(XMLWriter(out), factory=self.element_factory) out.close() assert "" in out.data assert ' type="CommentItem"' in out.data, out.data def test_load_uml(self): """ Test loading of a freshly saved model. """ self.element_factory.create(UML.Package) # diagram is created in TestCase.setUp # self.element_factory.create(UML.Diagram) self.element_factory.create(UML.Comment) self.element_factory.create(UML.Class) data = self.save() self.load(data) assert len(self.element_factory.lselect()) == 4 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Package))) == 1 # diagram is created in TestCase.setUp assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Comment))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Class))) == 1 def test_load_uml_2(self): """ Test loading of a freshly saved model. """ self.element_factory.create(UML.Package) self.create(items.CommentItem, UML.Comment) self.create(items.ClassItem, UML.Class) iface = self.create(items.InterfaceItem, UML.Interface) iface.subject.name = "Circus" iface.matrix.translate(10, 10) data = self.save() self.load(data) assert len(self.element_factory.lselect()) == 5 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Package))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))) == 1 d = self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))[0] assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Comment))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Class))) == 1 assert ( len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Interface))) == 1 ) c = self.element_factory.lselect(lambda e: e.isKindOf(UML.Class))[0] assert c.presentation assert c.presentation[0].subject is c # assert c.presentation[0].subject.name.startwith('Class') iface = self.element_factory.lselect(lambda e: e.isKindOf(UML.Interface))[0] assert iface.name == "Circus" assert len(iface.presentation) == 1 assert tuple(iface.presentation[0].matrix) == (1, 0, 0, 1, 10, 10), tuple( iface.presentation[0].matrix ) # Check load/save of other canvas items. assert len(d.canvas.get_all_items()) == 3 for item in d.canvas.get_all_items(): assert item.subject, "No subject for %s" % item d1 = d.canvas.select(lambda e: isinstance(e, items.ClassItem))[0] assert d1 def test_load_with_whitespace_name(self): difficult_name = " with space before and after " diagram = self.element_factory.lselect()[0] diagram.name = difficult_name data = self.save() self.load(data) elements = self.element_factory.lselect() assert len(elements) == 1, elements assert elements[0].name == difficult_name, elements[0].name def test_load_uml_metamodel(self): """ Test if the meta model can be loaded. """ dist = pkg_resources.get_distribution("gaphor") path = os.path.join(dist.location, "gaphor/UML/uml2.gaphor") with io.open(path) as ifile: storage.load(ifile, factory=self.element_factory) def test_load_uml_relationships(self): """ Test loading of a freshly saved model with relationship objects. """ self.element_factory.create(UML.Package) self.create(items.CommentItem, UML.Comment) c1 = self.create(items.ClassItem, UML.Class) a = self.diagram.create(items.AssociationItem) a.handles()[0].pos = (10, 20) a.handles()[1].pos = (50, 60) assert 10 == a.handles()[0].pos.x, a.handles()[0].pos assert a.handles()[0].pos.y == 20, a.handles()[0].pos assert tuple(a.handles()[1].pos) == (50, 60), a.handles()[1].pos data = self.save() self.load(data) assert len(self.element_factory.lselect()) == 4 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Package))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))) == 1 d = self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))[0] assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Comment))) == 1 assert len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Class))) == 1 assert ( len(self.element_factory.lselect(lambda e: e.isKindOf(UML.Association))) == 0 ) # Check load/save of other canvas items. assert len(d.canvas.get_all_items()) == 3 for item in d.canvas.get_all_items(): if isinstance(item, items.AssociationItem): aa = item assert aa assert list(map(float, aa.handles()[0].pos)) == [0, 0], aa.handles()[0].pos assert list(map(float, aa.handles()[1].pos)) == [40, 40], aa.handles()[1].pos d1 = d.canvas.select(lambda e: isinstance(e, items.ClassItem))[0] assert d1 def test_connection(self): """ Test connection loading of an association and two classes. (Should count for all line-like objects alike if this works). """ c1 = self.create(items.ClassItem, UML.Class) c2 = self.create(items.ClassItem, UML.Class) c2.matrix.translate(200, 200) self.diagram.canvas.update_matrix(c2) assert tuple(self.diagram.canvas.get_matrix_i2c(c2)) == (1, 0, 0, 1, 200, 200) a = self.create(items.AssociationItem) self.connect(a, a.head, c1) head_pos = a.head.pos self.connect(a, a.tail, c2) tail_pos = a.tail.pos self.diagram.canvas.update_now() assert a.head.pos.y == 0, a.head.pos assert a.tail.pos.x == 10, a.tail.pos # assert a.tail.y == 200, a.tail.pos assert a.subject fd = StringIO() storage.save(XMLWriter(fd), factory=self.element_factory) data = fd.getvalue() fd.close() old_a_subject_id = a.subject.id self.element_factory.flush() assert not list(self.element_factory.select()) fd = StringIO(data) storage.load(fd, factory=self.element_factory) fd.close() diagrams = list(self.kindof(UML.Diagram)) self.assertEqual(1, len(diagrams)) d = diagrams[0] a = d.canvas.select(lambda e: isinstance(e, items.AssociationItem))[0] self.assertTrue(a.subject is not None) self.assertEqual(old_a_subject_id, a.subject.id) cinfo_head = a.canvas.get_connection(a.head) self.assertTrue(cinfo_head.connected is not None) cinfo_tail = a.canvas.get_connection(a.tail) self.assertTrue(cinfo_tail.connected is not None) self.assertTrue(not cinfo_head.connected is cinfo_tail.connected) # assert a.head_end._name def test_load_save(self): """Test loading and saving models""" dist = pkg_resources.get_distribution("gaphor") path = os.path.join(dist.location, "test-diagrams/simple-items.gaphor") with io.open(path, "r") as ifile: storage.load(ifile, factory=self.element_factory) pf = PseudoFile() storage.save(XMLWriter(pf), factory=self.element_factory) with io.open(path, "r") as ifile: orig = ifile.read() copy = pf.data with io.open("tmp.gaphor", "w") as ofile: ofile.write(copy) expr = re.compile('gaphor-version="[^"]*"') orig = expr.sub("%VER%", orig) copy = expr.sub("%VER%", copy) self.maxDiff = None self.assertEqual(copy, orig, "Saved model does not match copy") def test_loading_an_old_version(self): """Test loading and saving models""" dist = pkg_resources.get_distribution("gaphor") path = os.path.join(dist.location, "test-diagrams/old-gaphor-version.gaphor") def load_old_model(): with io.open(path, "r") as ifile: storage.load(ifile, factory=self.element_factory) self.assertRaises(ValueError, load_old_model) PK!t9JJ#gaphor/storage/tests/test_verify.pyfrom gaphor.tests.testcase import TestCase from gaphor.storage.verify import orphan_references from gaphor import UML class VerifyTestCase(TestCase): def test_verifier(self): factory = self.element_factory c = factory.create(UML.Class) p = factory.create(UML.Property) c.ownedAttribute = p assert not orphan_references(factory) # Now create a separate item, not part of the factory: m = UML.Comment(id="acd123") m.annotatedElement = c assert m in c.ownedComment assert orphan_references(factory) PK!t t gaphor/storage/verify.py""" Verify the content of an element factory before it is saved. """ from gaphor import UML from gaphor.UML.collection import collection import gaphas def orphan_references(factory): """ Verify the contents of the element factory. Only checks are done that ensure the model can be loaded back again. TODO: Okay, now I can predict if a model can be loaded after it's saved, but I have no means to correct or fix the model. """ # Maintain a set of id's, one for elements, one for references. # Write only to file if references is a subset of elements refs = set() elements = set() def verify_reference(name, value): """ Store the reference """ # Save a reference to the object: if value.id: refs.add((value.id, value)) def verify_collection(name, value): """ Store a list of references. """ for v in value: if v.id: refs.add((v.id, v)) def verify_element(name, value): """ Store the element id. """ if isinstance(value, (UML.Element, gaphas.Item)): verify_reference(name, value) elif isinstance(value, collection): verify_collection(name, value) elif isinstance(value, gaphas.Canvas): value.save(verify_canvasitem) def verify_canvasitem(name, value, reference=False): """ Verify attributes and references in a gaphor.diagram.* object. The extra attribute reference can be used to force UML """ # log.debug('saving canvasitem: %s|%s %s' % (name, value, type(value))) if isinstance(value, collection) or ( isinstance(value, (list, tuple)) and reference == True ): verify_collection(name, value) elif reference: verify_reference(name, value) elif isinstance(value, gaphas.Item): elements.add(value.id) value.save(verify_canvasitem) # save subitems for child in value.canvas.get_children(value): verify_canvasitem(None, child) elif isinstance(value, UML.Element): verify_reference(name, value) for e in list(factory.values()): assert e.id elements.add(e.id) e.save(verify_element) return [r[1] for r in refs if not r[0] in elements] PK!o11gaphor/tests/__init__.pyfrom gaphor.tests.testcase import TestCase, main PK!; gaphor/tests/test_action.pyimport doctest from gaphor import action def test_suite(): return doctest.DocTestSuite(action) if __name__ == "__main__": import unittest unittest.main(defaultTest="test_suite") PK!Jkk gaphor/tests/test_application.py"""Application service test cases.""" import unittest from zope import component from gaphor import UML from gaphor.application import Application from gaphor.interfaces import IService class LoadServiceTestCase(unittest.TestCase): """Test case for loading Gaphor services.""" def test_service_load(self): """Test loading services and querying utilities.""" Application.init(["undo_manager", "file_manager", "properties"]) self.assertTrue( Application.get_service("undo_manager") is not None, "Failed to load the undo manager service", ) self.assertTrue( Application.get_service("file_manager") is not None, "Failed to load the file manager service", ) self.assertTrue( component.queryUtility(IService, "undo_manager") is not None, "Failed to query the undo manager utility", ) self.assertTrue( component.queryUtility(IService, "file_manager") is not None, "Failed to query the file manager utility", ) Application.shutdown() PK!SVnn gaphor/tests/test_transaction.py"""Unit tests for transactions in Gaphor.""" from unittest import TestCase from zope.component.globalregistry import base from gaphor.application import Application from gaphor.transaction import Transaction, transactional, TransactionError from gaphor.event import TransactionBegin, TransactionCommit, TransactionRollback begins = [] commits = [] rollbacks = [] def handle_begins(event): """Store TransactionBegin events in begins.""" begins.append(event) def handle_commits(event): """Store TransactionCommit events in commits.""" commits.append(event) def handle_rollback(event): """Store TransactionRollback events in rollbacks.""" rollbacks.append(event) class TransactionTestCase(TestCase): """Test case for transactions with the component registry enabled.""" def setUp(self): """Initialize Gaphor services and register transaction event handlers.""" Application.init(services=["component_registry"]) component_registry = Application.get_service("component_registry") component_registry.register_handler(handle_begins, [TransactionBegin]) component_registry.register_handler(handle_commits, [TransactionCommit]) component_registry.register_handler(handle_rollback, [TransactionRollback]) del begins[:] del commits[:] del rollbacks[:] def tearDown(self): """Finished with the test case. Unregister event handlers that store transaction events.""" component_registry = Application.get_service("component_registry") component_registry.unregister_handler(handle_begins, [TransactionBegin]) component_registry.unregister_handler(handle_commits, [TransactionCommit]) component_registry.unregister_handler(handle_rollback, [TransactionRollback]) def test_transaction_commit(self): """Test committing a transaction.""" tx = Transaction() self.assertTrue(tx._stack, "Transaction has no stack") self.assertEqual(1, len(begins), "Incorrect number of TrasactionBegin events") self.assertEqual( 0, len(commits), "Incorrect number of TransactionCommit events" ) self.assertEqual( 0, len(rollbacks), "Incorrect number of TransactionRollback events" ) tx.commit() self.assertEqual(1, len(begins), "Incorrect number of TrasactionBegin events") self.assertEqual( 1, len(commits), "Incorrect number of TransactionCommit events" ) self.assertEqual( 0, len(rollbacks), "Incorrect number of TransactionRollback events" ) self.assertFalse(tx._stack, "Transaction stack is not empty") try: tx.commit() except TransactionError: pass else: self.fail("Commit should not have succeeded") def test_transaction_rollback(self): """Test rolling back a transaction.""" tx = Transaction() self.assertTrue(tx._stack, "Transaction has no stack") self.assertEqual(1, len(begins), "Incorrect number of TrasactionBegin events") self.assertEqual( 0, len(commits), "Incorrect number of TransactionCommit events" ) self.assertEqual( 0, len(rollbacks), "Incorrect number of TransactionRollback events" ) tx.rollback() self.assertEqual(1, len(begins), "Incorrect number of TrasactionBegin events") self.assertEqual( 0, len(commits), "Incorrect number of TransactionCommit events" ) self.assertEqual( 1, len(rollbacks), "Incorrect number of TransactionRollback events" ) self.assertFalse(tx._stack, "Transaction stack is not empty") def test_transaction_commit_after_rollback(self): """Test committing one transaction after rolling back another transaction.""" tx1 = Transaction() tx2 = Transaction() tx2.rollback() tx1.commit() self.assertEqual(1, len(begins), "Incorrect number of TrasactionBegin events") self.assertEqual( 0, len(commits), "Incorrect number of TransactionCommit events" ) self.assertEqual( 1, len(rollbacks), "Incorrect number of TransactionRollback events" ) def test_transaction_stack(self): """Test the transaction stack.""" tx1 = Transaction() tx2 = Transaction() try: self.assertRaises(TransactionError, tx1.commit) finally: tx2.rollback() tx1.rollback() def test_transaction_context(self): """Test the transaction context manager.""" with Transaction() as tx: self.assertTrue( isinstance(tx, Transaction), "Context is not a Transaction instance" ) self.assertTrue( Transaction._stack, "Transaction instance has no stack inside a context" ) self.assertFalse(Transaction._stack, "Transaction stack should be empty") def test_transaction_context_error(self): """Test the transaction context manager with errors.""" try: with Transaction(): raise TypeError("transaction error") except TypeError as e: self.assertEqual( "transaction error", str(e), "Transaction context manager did no raise correct exception", ) else: self.fail( "Transaction context manager did not raise exception when it should have" ) class TransactionWithoutComponentRegistryTestCase(TestCase): """Test case for transactions with no component registry.""" def test_transaction(self): """Test basic transaction functionality.""" tx = Transaction() tx.rollback() tx = Transaction() tx.commit() PK!K2{xxgaphor/tests/testcase.py""" Basic test case for Gaphor tests. Everything is about services so the TestCase can define it's required services and start off. """ import logging import unittest from io import StringIO from gaphas.aspect import ConnectionSink, Connector from zope import component from gaphor import UML from gaphor.application import Application from gaphor.diagram.interfaces import IConnect from gaphor.diagram.interfaces import IGroup # For DiagramItemConnector aspect: import gaphor.ui.diagramtools log = logging.getLogger("Gaphor") log.setLevel(logging.WARNING) class TestCaseExtras(object): """ Mixin for some extra tests. """ def failIfIdentityEqual(self, first, second, msg=None): """Fail if the two objects are equal as determined by the 'is operator. """ if first is second: raise self.failureException(msg or "%r is not %r" % (first, second)) assertNotSame = failIfIdentityEqual def failUnlessIdentityEqual(self, first, second, msg=None): """Fail if the two objects are not equal as determined by the 'is operator. """ if first is not second: raise self.failureException(msg or "%r is not %r" % (first, second)) assertSame = failUnlessIdentityEqual class TestCase(TestCaseExtras, unittest.TestCase): services = ["element_factory", "adapter_loader", "element_dispatcher", "sanitizer"] def setUp(self): Application.init(services=self.services) self.element_factory = Application.get_service("element_factory") assert len(list(self.element_factory.select())) == 0, list( self.element_factory.select() ) self.diagram = self.element_factory.create(UML.Diagram) assert len(list(self.element_factory.select())) == 1, list( self.element_factory.select() ) def tearDown(self): self.element_factory.shutdown() Application.shutdown() def get_service(self, name): return Application.get_service(name) def create(self, item_cls, subject_cls=None, subject=None): """ Create an item with specified subject. """ if subject_cls is not None: subject = self.element_factory.create(subject_cls) item = self.diagram.create(item_cls, subject=subject) self.diagram.canvas.update() return item def allow(self, line, handle, item, port=None): """ Glue line's handle to an item. If port is not provided, then first port is used. """ if port is None and len(item.ports()) > 0: port = item.ports()[0] query = (item, line) adapter = component.queryMultiAdapter(query, IConnect) return adapter.allow(handle, port) def connect(self, line, handle, item, port=None): """ Connect line's handle to an item. If port is not provided, then first port is used. """ canvas = line.canvas assert line.canvas is item.canvas if port is None and len(item.ports()) > 0: port = item.ports()[0] sink = ConnectionSink(item, port) connector = Connector(line, handle) connector.connect(sink) cinfo = canvas.get_connection(handle) self.assertSame(cinfo.connected, item) self.assertSame(cinfo.port, port) def disconnect(self, line, handle): """ Disconnect line's handle. """ canvas = self.diagram.canvas # disconnection on adapter level is performed due to callback, so # no adapter look up here canvas.disconnect_item(line, handle) assert not canvas.get_connection(handle) def get_connected(self, handle): """ Get item connected to line via handle. """ cinfo = self.diagram.canvas.get_connection(handle) if cinfo: return cinfo.connected return None def get_connection(self, handle): """ Get connection information. """ return self.diagram.canvas.get_connection(handle) def can_group(self, parent, item): """ Check if an item can be grouped by parent. """ query = (parent, item) adapter = component.queryMultiAdapter(query, IGroup) return adapter.can_contain() def group(self, parent, item): """ Group item within a parent. """ self.diagram.canvas.reparent(item, parent) query = (parent, item) adapter = component.queryMultiAdapter(query, IGroup) adapter.group() def ungroup(self, parent, item): """ Remove item from a parent. """ query = (parent, item) adapter = component.queryMultiAdapter(query, IGroup) adapter.ungroup() self.diagram.canvas.reparent(item, None) def kindof(self, cls): """ Find UML metaclass instances using element factory. """ return self.element_factory.lselect(lambda e: e.isKindOf(cls)) def save(self): """ Save diagram into string. """ from gaphor.storage import storage from gaphor.misc.xmlwriter import XMLWriter f = StringIO() storage.save(XMLWriter(f), factory=self.element_factory) data = f.getvalue() f.close() self.element_factory.flush() assert not list(self.element_factory.select()) assert not list(self.element_factory.lselect()) return data def load(self, data): """ Load data from specified string. Update ``TestCase.diagram`` attribute to hold new loaded diagram. """ from gaphor.storage import storage f = StringIO(data) storage.load(f, factory=self.element_factory) f.close() self.diagram = self.element_factory.lselect(lambda e: e.isKindOf(UML.Diagram))[ 0 ] main = unittest.main PK!ᩏgaphor/tools/README.txtTools ===== This package (``gaphor.tools``) is the place to put (command-line) utilities that are part of the Gaphor distribution. PK!gaphor/tools/__init__.pyPK!d gaphor/tools/gaphorconvert.py#!/usr/bin/python import gaphor from gaphor.storage import storage import gaphor.UML as UML from gaphas.painter import ItemPainter from gaphas.view import View import cairo import optparse import os import re import sys def pkg2dir(package): """ Return directory path from UML package class. """ name = [] while package: name.insert(0, package.name) package = package.package return "/".join(name) def message(msg): """ Print message if user set verbose mode. """ global options if options.verbose: print(msg, file=sys.stderr) usage = "usage: %prog [options] file1 file2..." parser = optparse.OptionParser(usage=usage) parser.add_option( "-v", "--verbose", dest="verbose", action="store_true", help="verbose output" ) parser.add_option( "-u", "--use-underscores", dest="underscores", action="store_true", help="use underscores instead of spaces for output filenames", ) parser.add_option( "-d", "--dir", dest="dir", metavar="directory", help="output to directory" ) parser.add_option( "-f", "--format", dest="format", metavar="format", help="output file format, default pdf", default="pdf", choices=["pdf", "svg", "png"], ) parser.add_option( "-r", "--regex", dest="regex", metavar="regex", help="process diagrams which name matches given regular expresion;" " name includes package name; regular expressions are case insensitive", ) (options, args) = parser.parse_args() if not args: parser.print_help() # sys.exit(1) factory = UML.ElementFactory() name_re = None if options.regex: name_re = re.compile(options.regex, re.I) # we should have some gaphor files to be processed at this point for model in args: message("loading model %s" % model) storage.load(model, factory) message("\nready for rendering\n") for diagram in factory.select(lambda e: e.isKindOf(UML.Diagram)): odir = pkg2dir(diagram.package) # just diagram name dname = diagram.name # full diagram name including package path pname = "%s/%s" % (odir, dname) if options.underscores: odir = odir.replace(" ", "_") dname = dname.replace(" ", "_") if name_re and not name_re.search(pname): message("skipping %s" % pname) continue if options.dir: odir = "%s/%s" % (options.dir, odir) outfilename = "%s/%s.%s" % (odir, dname, options.format) if not os.path.exists(odir): message("creating dir %s" % odir) os.makedirs(odir) message("rendering: %s -> %s..." % (pname, outfilename)) view = View(diagram.canvas) view.painter = ItemPainter() tmpsurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0) tmpcr = cairo.Context(tmpsurface) view.update_bounding_box(tmpcr) tmpcr.show_page() tmpsurface.flush() w, h = view.bounding_box.width, view.bounding_box.height if options.format == "pdf": surface = cairo.PDFSurface(outfilename, w, h) elif options.format == "svg": surface = cairo.SVGSurface(outfilename, w, h) elif options.format == "png": surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(w + 1), int(h + 1)) else: assert False, "unknown format %s" % options.format cr = cairo.Context(surface) view.matrix.translate(-view.bounding_box.x, -view.bounding_box.y) view.paint(cr) cr.show_page() if options.format == "png": surface.write_to_png(outfilename) surface.flush() surface.finish() def main(): pass PK!j@411gaphor/transaction.py""" Transaction support for Gaphor """ import logging from zope.interface import implementer from zope import component from gaphor import application from gaphor.event import TransactionBegin, TransactionCommit, TransactionRollback from gaphor.interfaces import ITransaction log = logging.getLogger(__name__) def transactional(func): """The transactional decorator makes a function transactional. A Transaction instance is created before the decorated function is called. If calling the function leads to an exception being raised, the transaction is rolled-back. Otherwise, it is committed.""" def _transactional(*args, **kwargs): r = None tx = Transaction() try: r = func(*args, **kwargs) except Exception as e: log.error( "Transaction terminated due to an exception, performing a rollback", exc_info=True, ) try: tx.rollback() except Exception as e: log.error("Rollback failed", exc_info=True) raise else: tx.commit() return r return _transactional class TransactionError(Exception): """ Errors related to the transaction module. """ pass @implementer(ITransaction) class Transaction(object): """ The transaction. On start and end of a transaction an event is emitted. Transactions can be nested. If the outermost transaction is committed or rolled back, an event is emitted. Events can be handled programmatically: >>> tx = Transaction() >>> tx.commit() It can be assigned as decorator: >>> @transactional ... def foo(): ... pass Or with the ``with`` statement: >>> with Transaction(): ... pass """ component_registry = application.inject("component_registry") _stack = [] def __init__(self): """Initialize the transaction. If this is the first transaction in the stack, a TransactionBegin event is emitted.""" self._need_rollback = False if not self._stack: self._handle(TransactionBegin()) self._stack.append(self) def commit(self): """Commit the transaction. First, the transaction is closed. If it needs to be rolled-back, a TransactionRollback event is emitted. Otherwise, a TransactionCommit event is emitted.""" self._close() if not self._stack: if self._need_rollback: self._handle(TransactionRollback()) else: self._handle(TransactionCommit()) def rollback(self): """Roll-back the transaction. First, the transaction is closed. Every transaction on the stack is then marked for roll-back. If the stack is empty, a TransactionRollback event is emitted.""" self._close() for tx in self._stack: tx._need_rollback = True else: if not self._stack: self._handle(TransactionRollback()) def _close(self): """Close the transaction. If the stack is empty, a TransactionError is raised. If the last transaction on the stack isn't this transaction, a Transaction error is raised.""" try: last = self._stack.pop() except IndexError: raise TransactionError("No Transaction on stack.") if last is not self: self._stack.append(last) raise TransactionError( "Transaction on stack is not the transaction being closed." ) def _handle(self, event): try: component_registry = self.component_registry except (application.NotInitializedError, component.ComponentLookupError): log.warning("Could not lookup component_registry. Not emitting events.") else: component_registry.handle(event) def __enter__(self): """Provide with-statement transaction support.""" return self def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): """Provide with-statement transaction support. If an error occurred, the transaction is rolled back. Otherwise, it is committed.""" if exc_type: self.rollback() else: self.commit() # vim: sw=4:et:ai PK!^f@'  gaphor/transaction.txtTransaction support for Gaphor ============================== Transaction support is located in module gaphor.transaction: >>> from gaphor import transaction >>> from gaphor.application import Application Do some basic initialization, so event emission will work: >>> Application.init(services=['component_registry']) The Transaction class is used mainly to signal the begin and end of a transaction. This is done by the TransactionBegin, TransactionCommit and TransactionRollback events: >>> from zope import component >>> @component.adapter(transaction.TransactionBegin) ... def transaction_begin_handler(event): ... print 'tx begin' >>> component.provideHandler(transaction_begin_handler) Same goes for commit and rollback events: >>> @component.adapter(transaction.TransactionCommit) ... def transaction_commit_handler(event): ... print 'tx commit' >>> component.provideHandler(transaction_commit_handler) >>> @component.adapter(transaction.TransactionRollback) ... def transaction_rollback_handler(event): ... print 'tx rollback' >>> component.provideHandler(transaction_rollback_handler) A Transaction is started by initiating a Transaction instance: >>> tx = transaction.Transaction() tx begin On success, a transaction can be committed: >>> tx.commit() tx commit After a commit, a rollback is no longer allowed (the transaction is closed): >>> tx.rollback() ... # doctest: +ELLIPSIS Traceback (most recent call last): ... TransactionError: No Transaction on stack. Transactions may be nested: >>> tx = transaction.Transaction() tx begin >>> tx2 = transaction.Transaction() >>> tx2.commit() >>> tx.commit() tx commit Transactions should be closed in the right order (subtransactions first): >>> tx = transaction.Transaction() tx begin >>> tx2 = transaction.Transaction() >>> tx.commit() ... # doctest: +ELLIPSIS Traceback (most recent call last): ... TransactionError: Transaction on stack is not the transaction being closed. >>> tx2.commit() >>> tx.commit() tx commit The transactional decorator can be used to mark functions as transactional: >>> @transaction.transactional ... def a(): ... print 'do something' >>> a() tx begin do something tx commit If an exception is raised from within the decorated function a rollback is performed: >>> @transaction.transactional ... def a(): ... raise IndexError, 'bla' >>> a() ... # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: bla >>> transaction.Transaction._stack [] All transactions are marked for rollback once an exception is raised: >>> tx = transaction.Transaction() tx begin >>> a() ... # doctest: +ELLIPSIS Traceback (most recent call last): ... IndexError: bla >>> tx._need_rollback True >>> tx.commit() tx rollback Cleanup: >>> Application.shutdown() PK!Affgaphor/ui/__init__.py""" This module contains user interface related code, such as the main screen and diagram windows. """ from gi.repository import Gtk, Gdk import pkg_resources import os.path icon_theme = Gtk.IconTheme.get_default() icon_theme.append_search_path( os.path.abspath(pkg_resources.resource_filename("gaphor.ui", "pixmaps")) ) import re def _repl(m): v = m.group(1).lower() return len(v) == 1 and v or "%c-%c" % tuple(v) _repl.expr = "(.?[A-Z])" def icon_for_element(element): return re.sub(_repl.expr, _repl, type(element).__name__) # Set style for model canvas css_provider = Gtk.CssProvider.new() screen = Gdk.Display.get_default().get_default_screen() Gtk.StyleContext.add_provider_for_screen( screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) css_provider.load_from_data("#diagram-tab { background: white }".encode("utf-8")) PK!5A0!!gaphor/ui/accelmap.py""" This module contains user interface related code, such as the main screen and diagram windows. """ import os from gi.repository import Gtk from gaphor.misc import get_config_dir def _get_accel_map_filename(): """ The Gaphor accelMap file ($HOME/.gaphor/accelmap). """ config_dir = get_config_dir() return os.path.join(config_dir, "accelmap") def load_accel_map(): """ Load the user accelerator map from the gaphor user home directory """ filename = _get_accel_map_filename() if os.path.exists(filename) and os.path.isfile(filename): Gtk.AccelMap.load(filename) def save_accel_map(): """ Save the contents of the GtkAccelMap to a file. """ filename = _get_accel_map_filename() Gtk.AccelMap.save(filename) # vim:sw=4:et: PK!CC C gaphor/ui/consolewindow.py#!/usr/bin/env python import logging import os from gi.repository import Gtk from zope.interface import implementer from gaphor.action import action, build_action_group from gaphor.core import inject from gaphor.misc import get_config_dir from gaphor.interfaces import IActionProvider from gaphor.misc.console import GTKInterpreterConsole from gaphor.ui.interfaces import IUIComponent log = logging.getLogger(__name__) @implementer(IUIComponent, IActionProvider) class ConsoleWindow(object): component_registry = inject("component_registry") main_window = inject("main_window") menu_xml = """ """ title = "Gaphor Console" size = (400, 400) placement = "floating" def __init__(self): self.action_group = build_action_group(self) self.window = None def load_console_py(self, console): """Load default script for console. Saves some repetitive typing.""" console_py = os.path.join(get_config_dir(), "console.py") try: with open(console_py) as f: for line in f: console.push(line) except IOError: log.info("No initiation script %s" % console_py) @action(name="ConsoleWindow:open", label="_Console") def open_console(self): if not self.window: self.open() else: self.window.set_property("has-focus", True) def open(self): console = self.construct() self.load_console_py(console) @action(name="ConsoleWindow:close", stock_id="gtk-close", accel="w") def close(self, widget=None): self.window.destroy() self.window = None def construct(self): window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) window.set_transient_for(self.main_window.window) window.set_title(self.title) console = GTKInterpreterConsole( locals={"service": self.component_registry.get_service} ) console.show() window.add(console) window.show() self.window = window window.connect("destroy", self.close) return console # vim:sw=4:et:ai PK!P//gaphor/ui/diagrampage.py#!/usr/bin/env python import logging from zope import component from zope.interface import implementer from gaphas.freehand import FreeHandPainter from gaphas.painter import ( PainterChain, ItemPainter, HandlePainter, FocusedItemPainter, ToolPainter, BoundingBoxPainter, ) from gaphas.view import GtkView from gi.repository import Gdk from gi.repository import Gtk from gaphor import UML from gaphor.interfaces import IActionProvider from gaphor.UML.interfaces import IAttributeChangeEvent, IElementDeleteEvent from gaphor.core import _, inject, transactional, action, build_action_group from gaphor.diagram import get_diagram_item from gaphor.diagram.items import DiagramItem from gaphor.transaction import Transaction from gaphor.ui.diagramtoolbox import DiagramToolbox from gaphor.ui.event import DiagramSelectionChange log = logging.getLogger(__name__) @implementer(IActionProvider) class DiagramPage(object): component_registry = inject("component_registry") element_factory = inject("element_factory") action_manager = inject("action_manager") menu_xml = """ """ VIEW_TARGET_STRING = 0 VIEW_TARGET_ELEMENT_ID = 1 VIEW_TARGET_TOOLBOX_ACTION = 2 VIEW_DND_TARGETS = [ Gtk.TargetEntry.new("gaphor/element-id", 0, VIEW_TARGET_ELEMENT_ID), Gtk.TargetEntry.new("gaphor/toolbox-action", 0, VIEW_TARGET_TOOLBOX_ACTION), ] def __init__(self, diagram): self.diagram = diagram self.view = None self.widget = None self.action_group = build_action_group(self) self.toolbox = None self.component_registry.register_handler(self._on_element_delete) title = property(lambda s: s.diagram and s.diagram.name or _("")) def get_diagram(self): return self.diagram def get_view(self): return self.view def get_canvas(self): return self.diagram.canvas def construct(self): """ Create the widget. Returns: the newly created widget. """ assert self.diagram view = GtkView(canvas=self.diagram.canvas) try: view.set_css_name("diagramview") except AttributeError: pass # Gtk.Widget.set_css_name() is added in 3.20 view.drag_dest_set( Gtk.DestDefaults.ALL, DiagramPage.VIEW_DND_TARGETS, Gdk.DragAction.MOVE | Gdk.DragAction.COPY | Gdk.DragAction.LINK, ) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.add(view) scrolled_window.show_all() self.widget = scrolled_window view.connect("focus-changed", self._on_view_selection_changed) view.connect("selection-changed", self._on_view_selection_changed) view.connect_after("key-press-event", self._on_key_press_event) # view.connect("drag-drop", self._on_drag_drop) view.connect("drag-data-received", self._on_drag_data_received) self.view = view self.toolbox = DiagramToolbox(self.diagram, view) return self.widget @component.adapter(IElementDeleteEvent) def _on_element_delete(self, event): if event.element is self.diagram: self.close() @action(name="diagram-close", stock_id="gtk-close") def close(self): """ Tab is destroyed. Do the same thing that would be done if File->Close was pressed. """ self.widget.destroy() self.component_registry.unregister_handler(self._on_element_delete) self.view = None @action(name="diagram-zoom-in", stock_id="gtk-zoom-in") def zoom_in(self): self.view.zoom(1.2) @action(name="diagram-zoom-out", stock_id="gtk-zoom-out") def zoom_out(self): self.view.zoom(1 / 1.2) @action(name="diagram-zoom-100", stock_id="gtk-zoom-100") def zoom_100(self): zx = self.view.matrix[0] self.view.zoom(1 / zx) @action(name="diagram-select-all", label="_Select all", accel="a") def select_all(self): self.view.select_all() @action( name="diagram-unselect-all", label="Des_elect all", accel="a" ) def unselect_all(self): self.view.unselect_all() @action(name="diagram-delete", stock_id="gtk-delete") @transactional def delete_selected_items(self): items = self.view.selected_items for i in list(items): if isinstance(i, DiagramItem): i.unlink() else: if i.canvas: i.canvas.remove(i) def set_drawing_style(self, sloppiness=0.0): """Set the drawing style for the diagram. 0.0 is straight, 2.0 is very sloppy. If the sloppiness is set to be anything greater than 0.0, the FreeHandPainter instances will be used for both the item painter and the box painter. Otherwise, by default, the ItemPainter is used for the item and BoundingBoxPainter for the box.""" view = self.view if sloppiness: item_painter = FreeHandPainter(ItemPainter(), sloppiness=sloppiness) box_painter = FreeHandPainter(BoundingBoxPainter(), sloppiness=sloppiness) else: item_painter = ItemPainter() box_painter = BoundingBoxPainter() view.painter = ( PainterChain() .append(item_painter) .append(HandlePainter()) .append(FocusedItemPainter()) .append(ToolPainter()) ) view.bounding_box_painter = box_painter view.queue_draw_refresh() def may_remove_from_model(self, view): """ Check if there are items which will be deleted from the model (when their last views are deleted). If so request user confirmation before deletion. """ items = self.view.selected_items last_in_model = [ i for i in items if i.subject and len(i.subject.presentation) == 1 ] log.debug("Last in model: %s" % str(last_in_model)) if last_in_model: return self.confirm_deletion_of_items(last_in_model) return True def confirm_deletion_of_items(self, last_in_model): """ Request user confirmation on deleting the item from the model. """ s = "" for item in last_in_model: s += "%s\n" % str(item) dialog = Gtk.MessageDialog( None, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, "This will remove the following selected items from the model:\n%s\nAre you sure?" % s, ) dialog.set_transient_for(self.get_toplevel()) value = dialog.run() dialog.destroy() if value == Gtk.ResponseType.YES: return True return False def _on_key_press_event(self, view, event): """ Handle the 'Delete' key. This can not be handled directly (through GTK's accelerators) since otherwise this key will confuse the text edit stuff. """ if view.is_focus(): if event.keyval == Gdk.KEY_Delete and ( event.get_state() == 0 or event.get_state() & Gdk.ModifierType.MOD2_MASK ): self.delete_selected_items() elif event.keyval == Gdk.KEY_BackSpace and ( event.get_state() == 0 or event.get_state() & Gdk.ModifierType.MOD2_MASK ): self.delete_selected_items() def _on_view_selection_changed(self, view, selection_or_focus): self.component_registry.handle( DiagramSelectionChange(view, view.focused_item, view.selected_items) ) # def _on_drag_drop(self, view, context, x, y, time): # """The signal handler for the drag-drop signal. # # The drag-drop signal is emitted on the drop site when the user drops # the data onto the widget. # # Args: # view: The view that received the drop. # context (Gdk.DragContext) - The drag context. # x (int): The x coordinate of the current cursor position. # y (int): The y coordinate of the current cursor position. # time (int): The timestamp of the motion event. # # Returns: # bool: Whether the cursor position is in the drop zone. # # """ # targets = context.list_targets() # # print('drag_drop on', targets) # for target in targets: # name = target.name() # if name == "gaphor/element-id": # target = Gdk.atom_intern(name, False) # view.drag_get_data(context, target, time) # return True # elif name == "gaphor/toolbox-action": # target = Gdk.atom_intern(name, False) # view.drag_get_data(context, target, time) # return True # return False def _on_drag_data_received(self, view, context, x, y, data, info, time): """ Handle data dropped on the canvas. """ if ( data and data.get_format() == 8 and info == DiagramPage.VIEW_TARGET_TOOLBOX_ACTION ): tool = self.toolbox.get_tool(data.get_data().decode()) tool.create_item((x, y)) context.finish(True, False, time) elif ( data and data.get_format() == 8 and info == DiagramPage.VIEW_TARGET_ELEMENT_ID ): print("drag_data_received:", data.get_data(), info) n, p = data.get_data().decode().split("#") element = self.element_factory.lookup(n) assert element # TODO: use adapters to execute code below q = type(element) if p: q = q, p item_class = get_diagram_item(q) if isinstance(element, UML.Diagram): self.action_manager.execute("OpenModelElement") elif item_class: tx = Transaction() item = self.diagram.create(item_class) assert item x, y = view.get_matrix_v2i(item).transform_point(x, y) item.matrix.translate(x, y) item.subject = element tx.commit() view.unselect_all() view.focused_item = item else: log.warning( "No graphical representation for UML element %s" % type(element).__name__ ) context.finish(True, False, time) else: context.finish(False, False, time) # vim: sw=4:et:ai PK!;2z:R:Rgaphor/ui/diagramtoolbox.py""" This module contains the actions used in the Toolbox (lower left section of the main window. The Toolbox is bound to a diagram. When a diagram page (tab) is switched, the actions bound to the toolbuttons should change as well. """ from gaphas.item import SE from zope import component from gaphor import UML from gaphor.UML.event import DiagramItemCreateEvent from gaphor.core import _, inject, radio_action, build_action_group from gaphor.diagram import items from gaphor.ui.diagramtools import PlacementTool, GroupPlacementTool, DefaultTool __all__ = ["DiagramToolbox", "TOOLBOX_ACTIONS"] # Actions: ((section (name, label, stock_id, shortcut)), ...) TOOLBOX_ACTIONS = ( ( "", ( ("toolbox-pointer", _("Pointer"), "gaphor-pointer", "Escape"), ("toolbox-line", _("Line"), "gaphor-line", "l"), ("toolbox-box", _("Box"), "gaphor-box", "b"), ("toolbox-ellipse", _("Ellipse"), "gaphor-ellipse", "e"), ("toolbox-comment", _("Comment"), "gaphor-comment", "k"), ("toolbox-comment-line", _("Comment line"), "gaphor-comment-line", "K"), ), ), ( _("Classes"), ( ("toolbox-class", _("Class"), "gaphor-class", "c"), ("toolbox-interface", _("Interface"), "gaphor-interface", "i"), ("toolbox-package", _("Package"), "gaphor-package", "p"), ("toolbox-association", _("Association"), "gaphor-association", "A"), ("toolbox-dependency", _("Dependency"), "gaphor-dependency", "D"), ( "toolbox-generalization", _("Generalization"), "gaphor-generalization", "G", ), ( "toolbox-implementation", _("Implementation"), "gaphor-implementation", "I", ), ), ), ( _("Components"), ( ("toolbox-component", _("Component"), "gaphor-component", "o"), ("toolbox-artifact", _("Artifact"), "gaphor-artifact", "h"), ("toolbox-node", _("Node"), "gaphor-node", "n"), ("toolbox-device", _("Device"), "gaphor-device", "d"), ("toolbox-subsystem", _("Subsystem"), "gaphor-subsystem", "y"), ("toolbox-connector", _("Connector"), "gaphor-connector", "C"), ), ), ( _("Actions"), ( ("toolbox-action", _("Action"), "gaphor-action", "a"), ("toolbox-initial-node", _("Initial node"), "gaphor-initial-node", "j"), ( "toolbox-activity-final-node", _("Activity final node"), "gaphor-activity-final-node", "f", ), ( "toolbox-flow-final-node", _("Flow final node"), "gaphor-flow-final-node", "w", ), ( "toolbox-decision-node", _("Decision/merge node"), "gaphor-decision-node", "g", ), ("toolbox-fork-node", _("Fork/join node"), "gaphor-fork-node", "R"), ("toolbox-object-node", _("Object node"), "gaphor-object-node", "O"), ("toolbox-partition", _("Partition"), "gaphor-partition", "P"), ("toolbox-flow", _("Control/object flow"), "gaphor-control-flow", "F"), ( "toolbox-send-signal-action", _("Send signal action"), "gaphor-send-signal-action", None, ), ( "toolbox-accept-event-action", _("Accept event action"), "gaphor-accept-event-action", None, ), ), ), ( _("Interactions"), ( ("toolbox-lifeline", _("Lifeline"), "gaphor-lifeline", "v"), ("toolbox-message", _("Message"), "gaphor-message", "M"), ("toolbox-interaction", _("Interaction"), "gaphor-interaction", "N"), ), ), ( _("States"), ( ("toolbox-state", _("State"), "gaphor-state", "s"), ( "toolbox-initial-pseudostate", _("Initial Pseudostate"), "gaphor-initial-pseudostate", "S", ), ("toolbox-final-state", _("Final State"), "gaphor-final-state", "x"), ( "toolbox-history-pseudostate", _("History Pseudostate"), "gaphor-history-pseudostate", "q", ), ("toolbox-transition", _("Transition"), "gaphor-transition", "T"), ), ), ( _("Use Cases"), ( ("toolbox-usecase", _("Use case"), "gaphor-usecase", "u"), ("toolbox-actor", _("Actor"), "gaphor-actor", "t"), ( "toolbox-usecase-association", _("Association"), "gaphor-association", "B", ), ("toolbox-include", _("Include"), "gaphor-include", "U"), ("toolbox-extend", _("Extend"), "gaphor-extend", "X"), ), ), ( _("Profiles"), ( ("toolbox-profile", _("Profile"), "gaphor-profile", "r"), ("toolbox-metaclass", _("Metaclass"), "gaphor-metaclass", "m"), ("toolbox-stereotype", _("Stereotype"), "gaphor-stereotype", "z"), ("toolbox-extension", _("Extension"), "gaphor-extension", "E"), ), ), ) def itemiter(toolbox_actions): """ Iterate toolbox items, irregardless section headers """ for name, section in toolbox_actions: for e in section: yield e class DiagramToolbox(object): """ Composite class for DiagramPage (diagrampage.py). """ element_factory = inject("element_factory") properties = inject("properties") def __init__(self, diagram, view): self.view = view self.diagram = diagram self.action_group = build_action_group(self) namespace = property(lambda s: s.diagram.namespace) def get_tool(self, tool_name): """ Return a tool associated with an id (action name). """ return getattr(self, tool_name.replace("-", "_"))() action_list = list(zip(*list(itemiter(TOOLBOX_ACTIONS)))) @radio_action(names=action_list[0], labels=action_list[1], stock_ids=action_list[2]) def _set_toolbox_action(self, id): """ Activate a tool based on its index in the TOOLBOX_ACTIONS list. """ tool_name = list(itemiter(TOOLBOX_ACTIONS))[id][0] self.view.tool = self.get_tool(tool_name) def _item_factory(self, item_class, subject_class=None, config_func=None): """ ``config_func`` may be a function accepting the newly created item. """ def factory_method(parent=None): if subject_class: subject = self.element_factory.create(subject_class) else: subject = None item = self.diagram.create(item_class, subject=subject, parent=parent) if config_func: config_func(item) return item factory_method.item_class = item_class return factory_method def _namespace_item_factory( self, item_class, subject_class, stereotype=None, name=None ): """ Returns a factory method for Namespace classes. To be used by the PlacementTool. """ def factory_method(parent=None): subject = self.element_factory.create(subject_class) item = self.diagram.create(item_class, subject=subject, parent=parent) subject.package = self.namespace if name is not None: subject.name = name elif stereotype: subject.name = "New%s" % stereotype.capitalize() else: subject.name = "New%s" % subject_class.__name__ return item factory_method.item_class = item_class return factory_method def _after_handler(self, new_item): if self.properties("reset-tool-after-create", False): self.action_group.get_action("toolbox-pointer").activate() component.handle(DiagramItemCreateEvent(new_item)) ## ## Toolbox actions ## def toolbox_pointer(self): if self.view: return DefaultTool() def toolbox_line(self): return PlacementTool( self.view, item_factory=self._item_factory(items.Line), after_handler=self._after_handler, ) def toolbox_box(self): return PlacementTool( self.view, item_factory=self._item_factory(items.Box), handle_index=SE, after_handler=self._after_handler, ) def toolbox_ellipse(self): return PlacementTool( self.view, item_factory=self._item_factory(items.Ellipse), handle_index=SE, after_handler=self._after_handler, ) def toolbox_comment(self): return PlacementTool( self.view, item_factory=self._item_factory(items.CommentItem, UML.Comment), handle_index=SE, after_handler=self._after_handler, ) def toolbox_comment_line(self): return PlacementTool( self.view, item_factory=self._item_factory(items.CommentLineItem), after_handler=self._after_handler, ) # Classes: def toolbox_class(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.ClassItem, UML.Class), handle_index=SE, after_handler=self._after_handler, ) def toolbox_interface(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory( items.InterfaceItem, UML.Interface ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_package(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.PackageItem, UML.Package), handle_index=SE, after_handler=self._after_handler, ) def toolbox_association(self): return PlacementTool( self.view, item_factory=self._item_factory(items.AssociationItem), after_handler=self._after_handler, ) def toolbox_dependency(self): return PlacementTool( self.view, item_factory=self._item_factory(items.DependencyItem), after_handler=self._after_handler, ) def toolbox_generalization(self): return PlacementTool( self.view, item_factory=self._item_factory(items.GeneralizationItem), after_handler=self._after_handler, ) def toolbox_implementation(self): return PlacementTool( self.view, item_factory=self._item_factory(items.ImplementationItem), after_handler=self._after_handler, ) # Components: def toolbox_component(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory( items.ComponentItem, UML.Component ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_artifact(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.ArtifactItem, UML.Artifact), handle_index=SE, after_handler=self._after_handler, ) def toolbox_node(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.NodeItem, UML.Node), handle_index=SE, after_handler=self._after_handler, ) def toolbox_device(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.NodeItem, UML.Device), handle_index=SE, after_handler=self._after_handler, ) def toolbox_subsystem(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory( items.SubsystemItem, UML.Component, "subsystem" ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_connector(self): return PlacementTool( self.view, item_factory=self._item_factory(items.ConnectorItem), after_handler=self._after_handler, ) # Actions: def toolbox_action(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.ActionItem, UML.Action), handle_index=SE, after_handler=self._after_handler, ) def toolbox_send_signal_action(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory( items.SendSignalActionItem, UML.SendSignalAction ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_accept_event_action(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory( items.AcceptEventActionItem, UML.AcceptEventAction ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_initial_node(self): return GroupPlacementTool( self.view, item_factory=self._item_factory(items.InitialNodeItem, UML.InitialNode), handle_index=SE, after_handler=self._after_handler, ) def toolbox_activity_final_node(self): return GroupPlacementTool( self.view, item_factory=self._item_factory( items.ActivityFinalNodeItem, UML.ActivityFinalNode ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_flow_final_node(self): return GroupPlacementTool( self.view, item_factory=self._item_factory(items.FlowFinalNodeItem, UML.FlowFinalNode), handle_index=SE, after_handler=self._after_handler, ) def toolbox_decision_node(self): return GroupPlacementTool( self.view, item_factory=self._item_factory(items.DecisionNodeItem, UML.DecisionNode), handle_index=SE, after_handler=self._after_handler, ) def toolbox_fork_node(self): return GroupPlacementTool( self.view, item_factory=self._item_factory(items.ForkNodeItem, UML.JoinNode), handle_index=1, after_handler=self._after_handler, ) def toolbox_object_node(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory( items.ObjectNodeItem, UML.ObjectNode ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_partition(self): # note no subject, which is created by grouping adapter return GroupPlacementTool( self.view, item_factory=self._item_factory(items.PartitionItem), handle_index=SE, after_handler=self._after_handler, ) def toolbox_flow(self): return PlacementTool( self.view, item_factory=self._item_factory(items.FlowItem), after_handler=self._after_handler, ) # Interactions: def toolbox_interaction(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory( items.InteractionItem, UML.Interaction ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_lifeline(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.LifelineItem, UML.Lifeline), handle_index=SE, after_handler=self._after_handler, ) def toolbox_message(self): return PlacementTool( self.view, item_factory=self._item_factory(items.MessageItem), after_handler=self._after_handler, ) # States: def toolbox_state(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.StateItem, UML.State), handle_index=SE, after_handler=self._after_handler, ) def _toolbox_pseudostate(self, kind): def set_state(item): item.subject.kind = kind return PlacementTool( self.view, item_factory=self._item_factory( items.InitialPseudostateItem, UML.Pseudostate, set_state ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_initial_pseudostate(self): def set_state(item): item.subject.kind = "initial" return PlacementTool( self.view, item_factory=self._item_factory( items.InitialPseudostateItem, UML.Pseudostate, set_state ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_history_pseudostate(self): def set_state(item): item.subject.kind = "shallowHistory" return PlacementTool( self.view, item_factory=self._item_factory( items.HistoryPseudostateItem, UML.Pseudostate, set_state ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_final_state(self): return PlacementTool( self.view, item_factory=self._item_factory(items.FinalStateItem, UML.FinalState), handle_index=SE, after_handler=self._after_handler, ) def toolbox_transition(self): return PlacementTool( self.view, item_factory=self._item_factory(items.TransitionItem), after_handler=self._after_handler, ) # Use cases: def toolbox_usecase(self): return GroupPlacementTool( self.view, item_factory=self._namespace_item_factory(items.UseCaseItem, UML.UseCase), handle_index=SE, after_handler=self._after_handler, ) def toolbox_actor(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.ActorItem, UML.Actor), handle_index=SE, after_handler=self._after_handler, ) def toolbox_usecase_association(self): return PlacementTool( self.view, item_factory=self._item_factory(items.AssociationItem), after_handler=self._after_handler, ) def toolbox_include(self): return PlacementTool( self.view, item_factory=self._item_factory(items.IncludeItem), after_handler=self._after_handler, ) def toolbox_extend(self): return PlacementTool( self.view, item_factory=self._item_factory(items.ExtendItem), after_handler=self._after_handler, ) # Profiles: def toolbox_profile(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.PackageItem, UML.Profile), handle_index=SE, after_handler=self._after_handler, ) def toolbox_metaclass(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory( items.MetaclassItem, UML.Class, "metaclass", name="Class" ), handle_index=SE, after_handler=self._after_handler, ) def toolbox_stereotype(self): return PlacementTool( self.view, item_factory=self._namespace_item_factory(items.ClassItem, UML.Stereotype), handle_index=SE, after_handler=self._after_handler, ) def toolbox_extension(self): return PlacementTool( self.view, item_factory=self._item_factory(items.ExtensionItem), after_handler=self._after_handler, ) # vim:sw=4:et:ai PK!gs^==gaphor/ui/diagramtools.py""" Tools for handling items on the canvas. Although Gaphas has quite a few useful tools, some tools need to be extended: - PlacementTool: should perform undo - HandleTool: should support adapter based connection protocol - TextEditTool: should support adapter based edit protocol """ import logging from zope import component from gaphas.aspect import Connector, InMotion from gaphas.guide import GuidedItemInMotion from gaphas.tool import ( Tool, PlacementTool as _PlacementTool, ToolChain, HoverTool, ItemTool, RubberbandTool, ConnectHandleTool, ) from gi.repository import Gdk from gi.repository import Gtk from gaphor.core import Transaction, transactional from gaphor.diagram.diagramline import DiagramLine from gaphor.diagram.elementitem import ElementItem from gaphor.diagram.interfaces import IEditor, IConnect, IGroup # cursor to indicate grouping IN_CURSOR = Gdk.Cursor.new(Gdk.CursorType.DIAMOND_CROSS) # cursor to indicate ungrouping OUT_CURSOR = Gdk.Cursor.new(Gdk.CursorType.SIZING) log = logging.getLogger(__name__) @Connector.when_type(DiagramLine) class DiagramItemConnector(Connector.default): """ Handle Tool (acts on item handles) that uses the IConnect protocol to connect items to one-another. It also adds handles to lines when a line is grabbed on the middle of a line segment (points are drawn by the LineSegmentPainter). """ def allow(self, sink): adapter = component.queryMultiAdapter((sink.item, self.item), IConnect) return adapter and adapter.allow(self.handle, sink.port) @transactional def connect(self, sink): """ Create connection at handle level and at model level. """ handle = self.handle item = self.item cinfo = item.canvas.get_connection(handle) try: callback = DisconnectHandle(self.item, self.handle) if cinfo and cinfo.connected is sink.item: # reconnect only constraint - leave model intact log.debug("performing reconnect constraint") constraint = sink.port.constraint(item.canvas, item, handle, sink.item) item.canvas.reconnect_item(item, handle, constraint=constraint) elif cinfo: # first disconnect but disable disconnection handle as # reconnection is going to happen adapter = component.queryMultiAdapter((sink.item, item), IConnect) try: connect = adapter.reconnect except AttributeError: connect = adapter.connect else: cinfo.callback.disable = True self.disconnect() # new connection self.connect_handle(sink, callback=callback) # adapter requires both ends to be connected. connect(handle, sink.port) else: # new connection adapter = component.queryMultiAdapter((sink.item, item), IConnect) self.connect_handle(sink, callback=callback) adapter.connect(handle, sink.port) except Exception as e: log.error("Error during connect", exc_info=True) @transactional def disconnect(self): super(DiagramItemConnector, self).disconnect() class DisconnectHandle(object): """ Callback for items disconnection using the adapters. This is an object so disconnection data can be serialized/deserialized using pickle. :Variables: item Connecting item. handle Handle of connecting item. disable If set, then disconnection is disabled. """ def __init__(self, item, handle): self.item = item self.handle = handle self.disable = False def __call__(self): handle = self.handle item = self.item canvas = self.item.canvas cinfo = canvas.get_connection(handle) if self.disable: log.debug("Not disconnecting %s.%s (disabled)" % (item, handle)) else: log.debug("Disconnecting %s.%s" % (item, handle)) if cinfo: adapter = component.queryMultiAdapter((cinfo.connected, item), IConnect) adapter.disconnect(handle) class TextEditTool(Tool): """ Text edit tool. Allows for elements that can adapt to the IEditable interface to be edited. """ def create_edit_window(self, x, y, text, editor): """ Create a popup window with some editable text. """ view = self.view window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) window.set_property("decorated", False) window.set_property("skip-taskbar-hint", True) window.set_modal(True) window.set_transient_for(view.get_toplevel()) buffer = Gtk.TextBuffer() if text: buffer.set_text(text) startiter, enditer = buffer.get_bounds() buffer.move_mark_by_name("selection_bound", startiter) buffer.move_mark_by_name("insert", enditer) text_view = Gtk.TextView() text_view.set_buffer(buffer) text_view.set_left_margin(2) text_view.set_right_margin(2) frame = Gtk.Frame() frame.set_shadow_type(Gtk.ShadowType.IN) frame.add(text_view) window.add(frame) r = Gdk.Rectangle() r.x = 0 r.y = 0 r.width = 70 r.height = 50 window.size_allocate(r) window.move(int(x), int(y)) def on_button_press(widget, event): if event.window == view.get_window(): self.submit_text(widget, buffer, editor) def on_key_press_event(widget, event): if event.keyval == Gdk.KEY_Return and not event.get_state() & ( Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK ): self.submit_text(widget, buffer, editor) elif event.keyval == Gdk.KEY_Escape: widget.get_toplevel().destroy() def on_focus_out_event(widget, event): print("Focus out event emitted") self.submit_text(widget, buffer, editor) window.add_events(Gdk.EventMask.FOCUS_CHANGE_MASK) window.connect("focus-out-event", on_focus_out_event) window.connect("button-press-event", on_button_press) text_view.connect("key-press-event", on_key_press_event) window.show_all() @transactional def submit_text(self, widget, buffer, editor): """ Submit the final text to the edited item. """ text = buffer.get_text( buffer.get_start_iter(), buffer.get_end_iter(), include_hidden_chars=True ) editor.update_text(text) widget.get_toplevel().destroy() def on_double_click(self, event): view = self.view item = view.hovered_item if item: try: editor = IEditor(item) except TypeError: # Could not adapt to IEditor return False log.debug("Found editor %r" % editor) x, y = view.get_matrix_v2i(item).transform_point(event.x, event.y) if editor.is_editable(x, y): text = editor.get_text() root_coords = event.get_root_coords() self.create_edit_window( root_coords.x_root, root_coords.y_root, text, editor ) return True class PlacementTool(_PlacementTool): """ PlacementTool is used to place items on the canvas. """ def __init__(self, view, item_factory, after_handler=None, handle_index=-1): """ item_factory is a callable. It is used to create a CanvasItem that is displayed on the diagram. """ _PlacementTool.__init__( self, view, factory=item_factory, handle_tool=ConnectHandleTool(), handle_index=handle_index, ) self.after_handler = after_handler self._tx = None @transactional def create_item(self, pos): return self._create_item(pos) def on_button_press(self, event): assert not self._tx self._tx = Transaction() view = self.view view.unselect_all() if _PlacementTool.on_button_press(self, event): try: opposite = self.new_item.opposite( self.new_item.handles()[self._handle_index] ) except (KeyError, AttributeError): pass else: # Connect opposite handle first, using the HandleTool's # mechanisms # First make sure all matrices are updated: view.canvas.update_matrix(self.new_item) view.update_matrix(self.new_item) vpos = event.x, event.y item = self.handle_tool.glue(self.new_item, opposite, vpos) if item: self.handle_tool.connect(self.new_item, opposite, vpos) return True return False def on_button_release(self, event): try: if self.after_handler: self.after_handler(self.new_item) return _PlacementTool.on_button_release(self, event) finally: self._tx.commit() self._tx = None class GroupPlacementTool(PlacementTool): """ Try to group items when placing them on diagram. """ def __init__(self, view, item_factory, after_handler=None, handle_index=-1): super(GroupPlacementTool, self).__init__( view, item_factory, after_handler, handle_index ) self._parent = None def on_motion_notify(self, event): """ Change parent item to dropzone state if it can accept diagram item object to be created. """ view = self.view if view.focused_item: view.unselect_item(view.focused_item) view.focused_item = None try: parent = view.get_item_at_point((event.x, event.y)) except KeyError: parent = None if parent: # create dummy adapter adapter = component.queryMultiAdapter( (parent, self._factory.item_class()), IGroup ) if adapter and adapter.can_contain(): view.dropzone_item = parent view.window.set_cursor(IN_CURSOR) self._parent = parent else: view.dropzone_item = None view.window.set_cursor(None) self._parent = None parent.request_update(matrix=False) else: if view.dropzone_item: view.dropzone_item.request_update(matrix=False) view.dropzone_item = None view.window.set_cursor(None) def _create_item(self, pos, **kw): """ Create diagram item and place it within parent's boundaries. """ parent = self._parent view = self.view try: adapter = component.queryMultiAdapter( (parent, self._factory.item_class()), IGroup ) if parent and adapter and adapter.can_contain(): kw["parent"] = parent item = super(GroupPlacementTool, self)._create_item(pos, **kw) adapter = component.queryMultiAdapter((parent, item), IGroup) if parent and item and adapter: adapter.group() canvas = view.canvas parent.request_update(matrix=False) finally: self._parent = None view.dropzone_item = None view.window.set_cursor(None) return item @InMotion.when_type(ElementItem) class DropZoneInMotion(GuidedItemInMotion): def move(self, pos): """ Move the item. x and y are in view coordinates. """ super(DropZoneInMotion, self).move(pos) item = self.item view = self.view x, y = pos current_parent = view.canvas.get_parent(item) over_item = view.get_item_at_point((x, y), selected=False) if not over_item: view.dropzone_item = None view.window.set_cursor(None) return if current_parent and not over_item: # are we going to remove from parent? adapter = component.queryMultiAdapter((current_parent, item), IGroup) if adapter: view.window.set_cursor(OUT_CURSOR) view.dropzone_item = current_parent current_parent.request_update(matrix=False) if over_item: # are we going to add to parent? adapter = component.queryMultiAdapter((over_item, item), IGroup) if adapter and adapter.can_contain(): view.dropzone_item = over_item view.window.set_cursor(IN_CURSOR) over_item.request_update(matrix=False) def stop_move(self): """ Motion stops: drop! """ super(DropZoneInMotion, self).stop_move() item = self.item view = self.view canvas = view.canvas old_parent = view.canvas.get_parent(item) new_parent = view.dropzone_item try: if new_parent is old_parent: if old_parent is not None: old_parent.request_update(matrix=False) return if old_parent: adapter = component.queryMultiAdapter((old_parent, item), IGroup) if adapter: adapter.ungroup() canvas.reparent(item, None) m = canvas.get_matrix_i2c(old_parent) item.matrix *= m old_parent.request_update() if new_parent: adapter = component.queryMultiAdapter((new_parent, item), IGroup) if adapter and adapter.can_contain(): adapter.group() canvas.reparent(item, new_parent) m = canvas.get_matrix_c2i(new_parent) item.matrix *= m new_parent.request_update() finally: view.dropzone_item = None view.window.set_cursor(None) class TransactionalToolChain(ToolChain): """ In addition to a normal toolchain, this chain begins an undo-transaction at button-press and commits the transaction at button-release. """ def __init__(self, view=None): super(TransactionalToolChain, self).__init__(view) self._tx = None def handle(self, event): # For double click: button_press, double_click, button_release # print 'event', self.EVENT_HANDLERS.get(event.type) if self.EVENT_HANDLERS.get(event.type) in ("on_button_press",): assert not self._tx self._tx = Transaction() try: super(TransactionalToolChain, self).handle(event) finally: if self._tx and self.EVENT_HANDLERS.get(event.type) in ( "on_button_release", "on_double_click", "on_triple_click", ): self._tx.commit() self._tx = None def DefaultTool(): """ The default tool chain build from HoverTool, ItemTool and HandleTool. """ chain = TransactionalToolChain() chain.append(HoverTool()) chain.append(ConnectHandleTool()) chain.append(ItemTool()) chain.append(TextEditTool()) chain.append(RubberbandTool()) return chain # vim:sw=4:et:ai PK!Jpgaphor/ui/elementeditor.py"""The element editor is a utility window used for editing elements.""" import logging from gi.repository import Gtk from zope.component import adapter, getAdapters from zope.interface import implementer from gaphor.UML import Presentation from gaphor.UML.interfaces import IAssociationChangeEvent from gaphor.core import _, inject, action, build_action_group from gaphor.interfaces import IActionProvider from gaphor.ui.interfaces import IUIComponent, IPropertyPage, IDiagramSelectionChange log = logging.getLogger(__name__) @implementer(IUIComponent, IActionProvider) class ElementEditor(object): """The ElementEditor class is a utility window used to edit UML elements. It will display the properties of the currently selected element in the diagram.""" element_factory = inject("element_factory") main_window = inject("main_window") component_registry = inject("component_registry") title = _("Element Editor") size = (275, -1) resizable = True placement = "floating" menu_xml = """ """ def __init__(self): """Constructor. Build the action group for the element editor window. This will place a button for opening the window in the toolbar. The widget attribute is a PropertyEditor.""" self.action_group = build_action_group(self) self.window = None self.vbox = None self._current_item = None self._expanded_pages = {_("Properties"): True} @action( name="ElementEditor:open", label=_("_Editor"), stock_id="gtk-edit", accel="e", ) def open_elementeditor(self): """Display the element editor when the toolbar button is toggled. If active, the element editor is displayed. Otherwise, it is hidden.""" if not self.window: self.open() def open(self): """Display the ElementEditor window.""" window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) window.set_transient_for(self.main_window.window) window.set_title(self.title) vbox = Gtk.VBox() window.add(vbox) vbox.show() window.show() self.window = window self.vbox = vbox diagrams = self.main_window.get_ui_component("diagrams") current_view = diagrams.get_current_view() if current_view: focused_item = current_view.focused_item if focused_item: self._selection_change(focused_item=focused_item) # Make sure we recieve self.component_registry.register_handler(self._selection_change) self.component_registry.register_handler(self._element_changed) window.connect("destroy", self.close) def close(self, _widget=None): """Hide the element editor window and deactivate the toolbar button. Both the widget and event parameters default to None and are idempotent if set.""" log.debug("ElementEditor.close") self.component_registry.unregister_handler(self._selection_change) self.component_registry.unregister_handler(self._element_changed) self.window = None self.vbox = None self._current_item = None return True def _get_adapters(self, item): """ Return an ordered list of (order, name, adapter). """ adaptermap = {} try: if item.subject: for name, adapter in getAdapters([item.subject], IPropertyPage): adaptermap[name] = (adapter.order, name, adapter) except AttributeError: pass for name, adapter in getAdapters([item], IPropertyPage): adaptermap[name] = (adapter.order, name, adapter) adapters = sorted(adaptermap.values()) return adapters def create_pages(self, item): """ Load all tabs that can operate on the given item. """ adapters = self._get_adapters(item) first = True for _, name, adapter in adapters: try: page = adapter.construct() if page is None: continue elif isinstance(page, Gtk.Container): page.set_border_width(6) if first: self.vbox.pack_start(page, False, True, 0) first = False else: expander = Gtk.Expander() expander.set_use_markup(True) expander.set_label("%s" % name) expander.add(page) expander.show_all() expander.set_expanded(self._expanded_pages.get(name, True)) expander.connect_after("activate", self.on_expand, name) self.vbox.pack_start(expander, False, True, 0) page.show_all() except Exception as e: log.error( "Could not construct property page for " + name, exc_info=True ) def clear_pages(self): """ Remove all tabs from the notebook. """ for page in self.vbox.get_children(): page.destroy() def on_expand(self, widget, name): self._expanded_pages[name] = widget.get_expanded() @adapter(IDiagramSelectionChange) def _selection_change(self, event=None, focused_item=None): """ Called when a diagram item receives focus. This reloads all tabs based on the current selection. """ item = event and event.focused_item or focused_item if item is self._current_item: return self._current_item = item self.clear_pages() if item is None: label = Gtk.Label() label.set_markup("No item selected") self.vbox.pack_start(label, expand=False, padding=10) label.show() return self.create_pages(item) @adapter(IAssociationChangeEvent) def _element_changed(self, event): element = event.element if event.property is Presentation.subject: if element is self._current_item: self.clear_pages() self.create_pages(self._current_item) # vim:sw=4:et:ai PK!Wfgaphor/ui/event.pyfrom zope.interface import implementer from gaphor.ui.interfaces import ( IDiagramSelectionChange, IDiagramPageChange, IDiagramShow, ) @implementer(IDiagramShow) class DiagramShow(object): def __init__(self, diagram): self.diagram = diagram @implementer(IDiagramPageChange) class DiagramPageChange(object): def __init__(self, item): self.item = item self.diagram_page = item.diagram_page @implementer(IDiagramSelectionChange) class DiagramSelectionChange(object): def __init__(self, diagram_view, focused_item, selected_items): self.diagram_view = diagram_view self.focused_item = focused_item self.selected_items = selected_items PK!͌*gaphor/ui/filedialog.py"""This module has a generic FileDialog class that is used to open or save files.""" from gi.repository import Gtk class FileDialog(object): """This is a file dialog that is used to open or save a file.""" def __init__( self, title, filename=None, action="open", parent=None, multiple=False, filters=[], ): """Initialize the file dialog. The title parameter is the title displayed in the dialog. The filename parameter will set the current file name in the dialog. The action is either open or save and changes the buttons displayed. If the parent window parameter is supplied, the file dialog is set to be transient for that window. The multiple parameter should be set to true if multiple files can be opened at once. This means that a list of filenames instead of a single filename string will be returned by the selection property.""" self.multiple = multiple if action == "open": action = Gtk.FileChooserAction.OPEN else: action = Gtk.FileChooserAction.SAVE self.dialog = Gtk.FileChooserNative(title=title, action=action) if parent: self.dialog.set_transient_for(parent) if filename: self.dialog.set_current_name(filename) def get_selection(self): """Return the selected file or files from the dialog. This is used by the selection property.""" response = self.dialog.run() if response == Gtk.ResponseType.ACCEPT: if self.multiple: selection = self.dialog.get_filenames() else: selection = self.dialog.get_filename() else: selection = "" return selection def destroy(self): """Destroy the GTK dialog.""" self.dialog.destroy() selection = property(get_selection) PK!Qkxgaphor/ui/iconoption.py""" Module dealing with options (typing) of icons. """ from gaphor import UML from simplegeneric import generic @generic def get_icon_option(element): """ Default behaviour: no options. """ return @get_icon_option.when_type(UML.Class) def get_option_class(element): if element.extension: return "metaclass" @get_icon_option.when_type(UML.Component) def get_option_component(element): for p in element.presentation: try: if p.__stereotype__ == "subsystem": return "subsystem" except AttributeError: pass @get_icon_option.when_type(UML.Property) def get_option_property(element): if element.association: return "association-end" # vim:sw=4:et:ai PK!tgaphor/ui/icons.xml pointer.png line.png box.png ellipse.png activity-final-node.png ActivityFinalNode action.png Action send-signal-action.png SendSignalAction accept-event-action.png AcceptEventAction actor.png Actor association.png Association artifact.png Artifact class.png Class class.png Class comment.png Comment comment-line.png component.png Component connector.png Connector control-flow.png ControlFlow decision-node.png DecisionNode fork-node.png ForkNode object-node.png ObjectNode flow-final-node.png FlowFinalNode dependency.png Dependency diagram.png Diagram extend.png Extend extension.png Extension generalization.png Generalization implementation.png Implementation include.png Include initial-node.png InitialNode interaction.png Interaction interface.png Interface lifeline.png Lifeline message.png Message node.png Node device.png Device operation.png Operation package.png Package pointer.png Parameter profile.png Profile attribute.png Property association.png Property state.png State final-state.png FinalState pseudostate.png Pseudostate transition.png Transition stereotype.png Stereotype use-case.png UseCase subsystem.png Component activity-partition.png ActivityPartition PK!01]gaphor/ui/interfaces.py""" Interfaces related to the user interface. """ from zope import interface class IDiagramShow(interface.Interface): """ Show a new diagram tab """ diagram = interface.Attribute("The newly selected Diagram") class IDiagramPageChange(interface.Interface): """ The selected diagram changes. """ item = interface.Attribute("The newly selected Notebook pane") diagram_page = interface.Attribute("The newly selected diagram page") class IDiagramSelectionChange(interface.Interface): """ The selection of a diagram changed. """ diagram_view = interface.Attribute("The diagram View that emits the event") focused_item = interface.Attribute("The diagram item that received focus") selected_items = interface.Attribute("All selected items in the diagram") class IUIComponent(interface.Interface): """ A user interface component. """ ui_name = interface.Attribute("The UIComponent name, provided by the loader") title = interface.Attribute("Title of the component") size = interface.Attribute("Size used for floating the component") placement = interface.Attribute('placement. E.g. ("left", "diagrams")') def open(self): """ Create and display the UI components (windows). """ def close(self): """ Close the UI component. The component can decide to hide or destroy the UI components. """ class IPropertyPage(interface.Interface): """ A property page which can display itself in a notebook """ order = interface.Attribute("Order number, used for ordered display") def construct(self): """ Create the page (Gtk.Widget) that belongs to the Property page. Returns the page's toplevel widget (Gtk.Widget). """ def destroy(self): """ Destroy the page and clean up signal handlers and stuff. """ PK!gaphor/ui/layout.py""" Layout code from a simple XML description. """ from simplegeneric import generic from xml.etree.ElementTree import fromstring from gi.repository import Gtk from gaphor.core import _ widget_factory = {} def deserialize(layout, container, layoutstr, itemfactory): """ Return a new layout with it's attached frames. Frames that should be floating already have their Gtk.Window attached (check frame.get_parent()). Transient settings and such should be done by the invoking application. """ def counter(): i = 0 while True: yield i i += 1 def _des(element, index, parent_widget=None): if element.tag == "component": name = element.attrib["name"] widget = itemfactory(name) widget.set_name(name) add(widget, index, parent_widget) else: factory = widget_factory[element.tag] widget = factory(parent=parent_widget, index=index, **element.attrib) assert widget, "No widget (%s)" % widget if len(element): list(map(_des, element, counter(), [widget] * len(element))) return widget tree = fromstring(layoutstr) list(map(layout.append, list(map(_des, tree, counter(), [container] * len(tree))))) # return layout def add(widget, index, parent_widget): if isinstance(parent_widget, Gtk.Paned): if index == 0: parent_widget.pack1(child=widget, resize=False, shrink=False) elif index == 1: parent_widget.pack2(child=widget, resize=True, shrink=False) else: parent_widget.add(widget) def factory(typename): """ Simple decorator for populating the widget_factory dictionary. """ def _factory(func): widget_factory[typename] = func return func return _factory @factory("paned") def paned(parent, index, orientation, position=None): paned = Gtk.Paned.new( Gtk.Orientation.HORIZONTAL if orientation == "horizontal" else Gtk.Orientation.VERTICAL ) add(paned, index, parent) if position: paned.set_position(int(position)) paned.show() return paned PK!m^gaphor/ui/layout.xml PK!+MMgaphor/ui/mainwindow.py""" The main application window. """ import logging import os.path from zope import component from zope.interface import implementer import pkg_resources from gi.repository import GLib from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Gtk from gaphor import UML from gaphor.UML.event import ModelFactoryEvent from gaphor.core import ( _, inject, action, toggle_action, build_action_group, transactional, ) from gaphor.interfaces import IService, IActionProvider from gaphor.UML.interfaces import IAttributeChangeEvent from gaphor.services.filemanager import FileManagerStateChanged from gaphor.services.undomanager import UndoManagerStateChanged from gaphor.ui.accelmap import load_accel_map, save_accel_map from gaphor.ui.diagrampage import DiagramPage from gaphor.ui.event import DiagramPageChange, DiagramShow from gaphor.ui.interfaces import IUIComponent from gaphor.ui.layout import deserialize from gaphor.ui.namespace import Namespace from gaphor.ui.toolbox import Toolbox log = logging.getLogger(__name__) ICONS = ( "gaphor-24x24.png", "gaphor-48x48.png", "gaphor-96x96.png", "gaphor-256x256.png", ) @implementer(IService, IActionProvider) class MainWindow(object): """ The main window for the application. It contains a Namespace-based tree view and a menu and a statusbar. """ component_registry = inject("component_registry") properties = inject("properties") element_factory = inject("element_factory") action_manager = inject("action_manager") file_manager = inject("file_manager") ui_manager = inject("ui_manager") title = "Gaphor" size = property(lambda s: s.properties.get("ui.window-size", (760, 580))) menubar_path = "/mainwindow" toolbar_path = "/mainwindow-toolbar" resizable = True menu_xml = """ """ def __init__(self): self.window = None self.model_changed = False self.layout = None def init(self, app=None): self.init_stock_icons() self.init_action_group() self.init_ui_components() def init_stock_icons(self): # Load stock items import gaphor.ui.stock gaphor.ui.stock.load_stock_icons() def init_ui_components(self): component_registry = self.component_registry for ep in pkg_resources.iter_entry_points("gaphor.uicomponents"): log.debug("found entry point uicomponent.%s" % ep.name) cls = ep.load() if not IUIComponent.implementedBy(cls): raise NameError( "Entry point %s doesn" "t provide IUIComponent" % ep.name ) uicomp = cls() uicomp.ui_name = ep.name component_registry.register_utility(uicomp, IUIComponent, ep.name) if IActionProvider.providedBy(uicomp): self.action_manager.register_action_provider(uicomp) def shutdown(self): if self.window: self.window.destroy() self.window = None save_accel_map() cr = self.component_registry cr.unregister_handler(self._on_file_manager_state_changed) cr.unregister_handler(self._on_undo_manager_state_changed) cr.unregister_handler(self._new_model_content) def init_action_group(self): self.action_group = build_action_group(self) for name, label in ( ("file", "_File"), ("file-export", "_Export"), ("file-import", "_Import"), ("edit", "_Edit"), ("diagram", "_Diagram"), ("tools", "_Tools"), ("help", "_Help"), ): a = Gtk.Action.new(name, label, None, None) a.set_property("hide-if-empty", False) self.action_group.add_action(a) self.action_manager.register_action_provider(self) def get_filename(self): """ Return the file name of the currently opened model. """ return self.file_manager.filename def ask_to_close(self): """ Ask user to close window if the model has changed. The user is asked to either discard the changes, keep the application running or save the model and quit afterwards. """ if self.model_changed: dialog = Gtk.MessageDialog( self.window, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _("Save changed to your model before closing?"), ) dialog.format_secondary_text( _("If you close without saving, your changes will be discarded.") ) dialog.add_buttons( "Close _without saving", Gtk.ResponseType.REJECT, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.YES, ) dialog.set_default_response(Gtk.ResponseType.YES) response = dialog.run() dialog.destroy() if response == Gtk.ResponseType.YES: # On filedialog.cancel, the application should not close. return self.file_manager.action_save() return response == Gtk.ResponseType.REJECT return True def get_ui_component(self, name): return self.component_registry.get_utility(IUIComponent, name) def open(self): load_accel_map() self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) self.window.set_title(self.title) self.window.set_default_size(*self.size) # set default icons of gaphor windows icon_dir = os.path.abspath( pkg_resources.resource_filename("gaphor.ui", "pixmaps") ) icons = ( GdkPixbuf.Pixbuf.new_from_file(os.path.join(icon_dir, f)) for f in ICONS ) self.window.set_icon_list(list(icons)) self.window.add_accel_group(self.ui_manager.get_accel_group()) # Create a full featured window. vbox = Gtk.VBox() self.window.add(vbox) vbox.show() menubar = self.ui_manager.get_widget(self.menubar_path) if menubar: vbox.pack_start(menubar, False, True, 0) toolbar = self.ui_manager.get_widget(self.toolbar_path) if toolbar: vbox.pack_start(toolbar, False, True, 0) def _factory(name): comp = self.get_ui_component(name) log.debug("open component %s" % str(comp)) return comp.open() filename = pkg_resources.resource_filename("gaphor.ui", "layout.xml") self.layout = [] # Gtk.Notebook() with open(filename) as f: deserialize(self.layout, vbox, f.read(), _factory) vbox.show() # TODO: add statusbar self.window.show() self.window.connect("delete-event", self._on_window_delete) # We want to store the window size, so it can be reloaded on startup self.window.set_resizable(True) self.window.connect("size-allocate", self._on_window_size_allocate) self.window.connect("destroy", self._on_window_destroy) cr = self.component_registry cr.register_handler(self._on_file_manager_state_changed) cr.register_handler(self._on_undo_manager_state_changed) cr.register_handler(self._new_model_content) # TODO: register on ElementCreate/Delete event def open_welcome_page(self): """ Create a new tab with a textual welcome page, a sort of 101 for Gaphor. """ pass def set_title(self): """ Sets the window title. """ filename = self.file_manager.filename if self.window: if filename: title = "%s - %s" % (self.title, filename) else: title = self.title if self.model_changed: title += " *" self.window.set_title(title) # Signal callbacks: @component.adapter(ModelFactoryEvent) def _new_model_content(self, event): """ Open the toplevel element and load toplevel diagrams. """ # TODO: Make handlers for ModelFactoryEvent from within the GUI obj for diagram in self.element_factory.select( lambda e: e.isKindOf(UML.Diagram) and not (e.namespace and e.namespace.namespace) ): self.component_registry.handle(DiagramShow(diagram)) @component.adapter(FileManagerStateChanged) def _on_file_manager_state_changed(self, event): # We're only interested in file operations if event.service is self.file_manager: self.model_changed = False self.set_title() @component.adapter(UndoManagerStateChanged) def _on_undo_manager_state_changed(self, event): """ """ undo_manager = event.service if not self.model_changed and undo_manager.can_undo(): self.model_changed = True self.set_title() def _on_window_destroy(self, window): """ Window is destroyed... Quit the application. """ self.window = None if GLib.main_depth() > 0: Gtk.main_quit() cr = self.component_registry cr.unregister_handler(self._on_undo_manager_state_changed) cr.unregister_handler(self._on_file_manager_state_changed) cr.unregister_handler(self._new_model_content) def _on_window_delete(self, window=None, event=None): return not self.ask_to_close() def _on_window_size_allocate(self, window, allocation): """ Store the window size in a property. """ self.properties.set("ui.window-size", (allocation.width, allocation.height)) # Actions: @action(name="file-quit", stock_id="gtk-quit") def quit(self): # TODO: check for changes (e.g. undo manager), fault-save close = self.ask_to_close() if close: Gtk.main_quit() self.shutdown() # TODO: Does not belong here def create_item(self, ui_component): """ Create an item for a ui component. This method can be called from UIComponents. """ window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) window.set_transient_for(self.window) window.set_title(ui_component.title) window.add(ui_component.open()) window.show() window.ui_component = ui_component Gtk.AccelMap.add_filter("gaphor") @implementer(IUIComponent, IActionProvider) class Diagrams(object): title = _("Diagrams") placement = ("left", "diagrams") component_registry = inject("component_registry") properties = inject("properties") ui_manager = inject("ui_manager") menu_xml = """ """ def __init__(self): self._notebook = None self.action_group = build_action_group(self) self.action_group.get_action("diagram-drawing-style").set_active( self.properties("diagram.sloppiness", 0) != 0 ) self._page_ui_settings = None def open(self): """Open the diagrams component. Returns: The Gtk.Notebook. """ self._notebook = Gtk.Notebook() self._notebook.show() self._notebook.connect("switch-page", self._on_switch_page) self.component_registry.register_handler(self._on_show_diagram) self.component_registry.register_handler(self._on_name_change) return self._notebook def close(self): """Close the diagrams component.""" self.component_registry.unregister_handler(self._on_name_change) self.component_registry.unregister_handler(self._on_show_diagram) self._notebook.destroy() self._notebook = None def get_current_diagram(self): """Returns the current page of the notebook. Returns (DiagramPage): The current diagram page. """ page_num = self._notebook.get_current_page() child_widget = self._notebook.get_nth_page(page_num) if child_widget is not None: return child_widget.diagram_page.get_diagram() else: return None def get_current_view(self): """Returns the current view of the diagram page. Returns (GtkView): The current view. """ if not self._notebook: return page_num = self._notebook.get_current_page() child_widget = self._notebook.get_nth_page(page_num) return child_widget.diagram_page.get_view() def cb_close_tab(self, button, widget): """Callback to close the tab and remove the notebook page. Args: button (Gtk.Button): The button the callback is from. widget (Gtk.Widget): The child widget of the tab. """ page_num = self._notebook.page_num(widget) # TODO why does Gtk.Notebook give a GTK-CRITICAL if you remove a page # with set_show_tabs(True)? self._clear_ui_settings() self._notebook.remove_page(page_num) widget.diagram_page.close() widget.destroy() def create_tab(self, title, widget): """Creates a new Notebook tab with a label and close button. Args: title (str): The title of the tab, the diagram name. widget (Gtk.Widget): The child widget of the tab. """ page_num = self._notebook.append_page( child=widget, tab_label=self.tab_label(title, widget) ) self._notebook.set_current_page(page_num) self._notebook.set_tab_reorderable(widget, True) self.component_registry.handle(DiagramPageChange(widget)) self._notebook.set_show_tabs(True) def tab_label(self, title, widget): tab_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) label = Gtk.Label(label=title) tab_box.pack_start(label) close_image = Gtk.Image.new_from_icon_name( icon_name="window-close", size=Gtk.IconSize.MENU ) button = Gtk.Button() button.set_relief(Gtk.ReliefStyle.NONE) button.set_focus_on_click(False) button.add(close_image) button.connect("clicked", self.cb_close_tab, widget) tab_box.pack_start(child=button, expand=False, fill=False, padding=0) tab_box.show_all() return tab_box def get_widgets_on_pages(self): """Gets the widget on each open page Notebook page. The page is the page number in the Notebook (0 indexed) and the widget is the child widget on each page. Returns: List of tuples (page, widget) of the currently open Notebook pages. """ widgets_on_pages = [] if not self._notebook: return widgets_on_pages num_pages = self._notebook.get_n_pages() for page in range(0, num_pages): widget = self._notebook.get_nth_page(page) widgets_on_pages.append((page, widget)) return widgets_on_pages def _on_switch_page(self, notebook, page, page_num): self._clear_ui_settings() self._add_ui_settings(page_num) self.component_registry.handle(DiagramPageChange(page)) def _add_ui_settings(self, page_num): ui_manager = self.ui_manager child_widget = self._notebook.get_nth_page(page_num) action_group = child_widget.diagram_page.action_group menu_xml = child_widget.diagram_page.menu_xml ui_manager.insert_action_group(action_group) ui_id = ui_manager.add_ui_from_string(menu_xml) self._page_ui_settings = (action_group, ui_id) def _clear_ui_settings(self): ui_manager = self.ui_manager if self._page_ui_settings: action_group, ui_id = self._page_ui_settings self.ui_manager.remove_action_group(action_group) self.ui_manager.remove_ui(ui_id) self._page_ui_settings = None @component.adapter(DiagramShow) def _on_show_diagram(self, event): """Show a Diagram element in the Notebook. If a diagram is already open on a Notebook page, show that one, otherwise create a new Notebook page. Args: event: The service event that is calling the method. """ diagram = event.diagram # Try to find an existing diagram page and give it focus for page, widget in self.get_widgets_on_pages(): if widget.diagram_page.get_diagram() is diagram: self._notebook.set_current_page(page) return widget.diagram_page # No existing diagram page found, creating one page = DiagramPage(diagram) widget = page.construct() try: widget.set_css_name("diagram-tab") except AttributeError: pass # Gtk.Widget.set_css_name() is added in 3.20 widget.set_name("diagram-tab") widget.diagram_page = page page.set_drawing_style(self.properties("diagram.sloppiness", 0)) self.create_tab(diagram.name, widget) return page @component.adapter(IAttributeChangeEvent) def _on_name_change(self, event): if event.property is UML.Diagram.name: for page in range(0, self._notebook.get_n_pages()): widget = self._notebook.get_nth_page(page) if event.element is widget.diagram_page.diagram: print("Name change", event.__dict__) self._notebook.set_tab_label( widget, self.tab_label(event.new_value, widget) ) @toggle_action(name="diagram-drawing-style", label="Hand drawn style", active=False) def hand_drawn_style(self, active): """Toggle between straight diagrams and "hand drawn" diagram style.""" if active: sloppiness = 0.5 else: sloppiness = 0.0 for page, widget in self.get_widgets_on_pages(): widget.diagram_page.set_drawing_style(sloppiness) self.properties.set("diagram.sloppiness", sloppiness) PK!}@v@vgaphor/ui/namespace.py""" This is the TreeView that is most common (for example: it is used in Rational Rose). This is a tree based on namespace relationships. As a result only classifiers are shown here. """ import logging import operator # PyGTKCompat used for Gtk.GenericTreeModel Support import pygtkcompat pygtkcompat.enable() pygtkcompat.enable_gtk("3.0") from gi.repository import GObject from gi.repository import Gtk from gi.repository import Gdk from zope import component from zope.interface import implementer from gaphor import UML from gaphor.interfaces import IActionProvider from gaphor.UML.event import ( ElementCreateEvent, ModelFactoryEvent, FlushFactoryEvent, DerivedSetEvent, ) from gaphor.UML.interfaces import IAttributeChangeEvent, IElementDeleteEvent from gaphor.core import _, action, build_action_group, inject, transactional from gaphor.transaction import Transaction from gaphor.ui.event import DiagramPageChange, DiagramShow from gaphor.ui import stock from gaphor.ui.iconoption import get_icon_option from gaphor.ui.interfaces import IUIComponent # The following items will be shown in the treeview, although they # are UML.Namespace elements. _default_filter_list = ( UML.Class, UML.Interface, UML.Package, UML.Component, UML.Device, UML.Node, UML.Artifact, UML.Interaction, UML.UseCase, UML.Actor, UML.Diagram, UML.Profile, UML.Stereotype, UML.Property, UML.Operation, ) # TODO: update tree sorter: # Diagram before Class & Package. # Property before Operation _name_getter = operator.attrgetter("name") _tree_sorter = lambda e: _name_getter(e) or "" log = logging.getLogger(__name__) def catchall(func): def catchall_wrapper(*args, **kwargs): try: func(*args, **kwargs) except Exception as e: log.error( "Exception in %s. Try to refresh the entire model" % (func,), exc_info=True, ) try: args[0].refresh() except Exception as e: log.error("Failed to refresh") return catchall_wrapper class NamespaceModel(Gtk.GenericTreeModel): """ The NamespaceModel holds a view on the data model based on namespace relationships (such as a Package containing a Class). NamedElement.namespace[1] -- Namespace.ownedMember[*] NOTE: when a model is loaded no IAssociation*Event's are emitted. """ component_registry = inject("component_registry") def __init__(self, factory): # Init parent: Gtk.GenericTreeModel.__init__(self) self.stamp = 0 #: Dictionary of (id(user_data): user_data), used when leak-references=False self._held_refs = {} # We own the references to the iterators. self.set_property("leak-references", 0) self.factory = factory self._nodes = {None: []} self.filter = _default_filter_list cr = self.component_registry cr.register_handler(self.flush) cr.register_handler(self.refresh) cr.register_handler(self._on_element_change) cr.register_handler(self._on_element_create) cr.register_handler(self._on_element_delete) cr.register_handler(self._on_association_set) self._build_model() def close(self): """ Close the namespace model, unregister handlers. """ cr = self.component_registry cr.unregister_handler(self.flush) cr.unregister_handler(self.refresh) cr.unregister_handler(self._on_element_change) cr.unregister_handler(self._on_element_create) cr.unregister_handler(self._on_element_delete) cr.unregister_handler(self._on_association_set) def path_from_element(self, e): if e: ns = e.namespace n = self._nodes.get(ns) if n and e in n: return self.path_from_element(ns) + (n.index(e),) else: return () else: return () def element_from_path(self, path): """ Get the node form a path. None is returned if no node is found. """ try: nodes = self._nodes node = None for index in path: node = nodes[node][index] return node except IndexError: return None @component.adapter(IAttributeChangeEvent) @catchall def _on_element_change(self, event): """ Element changed, update appropriate row. """ element = event.element if element not in self._nodes: return if ( event.property is UML.Classifier.isAbstract or event.property is UML.BehavioralFeature.isAbstract ): path = self.path_from_element(element) if path: self.row_changed(path, self.get_iter(path)) if event.property is UML.NamedElement.name: try: path = self.path_from_element(element) except KeyError: # Element not visible in the tree view return if not path: return self.row_changed(path, self.get_iter(path)) parent_nodes = self._nodes[element.namespace] parent_path = self.path_from_element(element.namespace) if not parent_path: return original = list(parent_nodes) parent_nodes.sort(key=_tree_sorter) if parent_nodes != original: # reorder the list: self.rows_reordered( parent_path, self.get_iter(parent_path), list(map(list.index, [original] * len(parent_nodes), parent_nodes)), ) def _add_elements(self, element): """ Add a single element. """ if type(element) not in self.filter: return if element.namespace not in self._nodes: return self._nodes.setdefault(element, []) parent = self._nodes[element.namespace] parent.append(element) parent.sort(key=_tree_sorter) path = self.path_from_element(element) self.row_inserted(path, self.get_iter(path)) # Add children if isinstance(element, UML.Namespace): for e in element.ownedMember: # check if owned member is indeed within parent's namespace # the check is important in case on Node classes if element is e.namespace: self._add_elements(e) def _remove_element(self, element): """ Remove elements from the nodes. No update signal is emitted. """ def remove(n): for c in self._nodes.get(n, []): remove(c) try: del self._nodes[n] except KeyError: pass remove(element) @component.adapter(ElementCreateEvent) @catchall def _on_element_create(self, event): element = event.element if event.service is self.factory: self._add_elements(element) @component.adapter(IElementDeleteEvent) @catchall def _on_element_delete(self, event): element = event.element log.debug("Namespace received deleting element %s" % element) if event.service is self.factory and type(element) in self.filter: path = self.path_from_element(element) # log.debug('Deleting element %s from path %s' % (element, path)) # Remove all sub-elements: if path: self.row_deleted(path) if path[:-1]: self.row_has_child_toggled(path[:-1], self.get_iter(path[:-1])) self._remove_element(element) parent_node = self._nodes.get(element.namespace) if element in parent_node: parent_node.remove(element) # if path and parent_node and len(self._nodes[parent_node]) == 0: # self.row_has_child_toggled(path[:-1], self.get_iter(path[:-1])) @component.adapter(DerivedSetEvent) @catchall def _on_association_set(self, event): element = event.element if type(element) not in self.filter: return if event.property is UML.NamedElement.namespace: # Check if the element is actually in the element factory: if element not in self.factory: return old_value, new_value = event.old_value, event.new_value # Remove entry from old place if old_value in self._nodes: try: path = self.path_from_element(old_value) + ( self._nodes[old_value].index(element), ) except ValueError: log.error( "Unable to create path for element %s and old_value %s" % (element, self._nodes[old_value]) ) else: self._nodes[old_value].remove(element) self.row_deleted(path) path = path[:-1] # self.path_from_element(old_value) if path: self.row_has_child_toggled(path, self.get_iter(path)) # Add to new place. This may fail if the type of the new place is # not in the tree model (because it's filtered) log.debug("Trying to add %s to %s" % (element, new_value)) if new_value in self._nodes: if element in self._nodes: parent = self._nodes[new_value] parent.append(element) parent.sort(key=_tree_sorter) path = self.path_from_element(element) self.row_inserted(path, self.get_iter(path)) else: self._add_elements(element) elif element in self._nodes: # Non-existent: remove entirely self._remove_element(element) @component.adapter(ModelFactoryEvent) def refresh(self, event=None): self.flush() self._build_model() @component.adapter(FlushFactoryEvent) def flush(self, event=None): for n in self._nodes[None]: self.row_deleted((0,)) self._nodes = {None: []} def _build_model(self): toplevel = self.factory.select( lambda e: isinstance(e, UML.Namespace) and not e.namespace ) for element in toplevel: self._add_elements(element) # TreeModel methods: def on_get_flags(self): """ Returns the GtkTreeModelFlags for this particular type of model. """ return 0 def on_get_n_columns(self): """ Returns the number of columns in the model. """ return 1 def on_get_column_type(self, index): """ Returns the type of a column in the model. """ return GObject.TYPE_PYOBJECT def on_get_path(self, node): """ Returns the path for a node as a tuple (0, 1, 1). """ path = self.path_from_element(node) return path def on_get_iter(self, path): """ Returns the node corresponding to the given path. The path is a tuple of values, like (0 1 1). Returns None if no iterator can be created. """ return self.element_from_path(path) def on_get_value(self, node, column): """ Returns the model element that matches 'node'. """ assert column == 0, "column can only be 0" return node def on_iter_next(self, node): """ Returns the next node at this level of the tree. None if no next element. """ try: parent = self._nodes[node.namespace] index = parent.index(node) return parent[index + 1] except (IndexError, ValueError) as e: return None def on_iter_has_child(self, node): """ Returns true if this node has children, or None. """ n = self._nodes.get(node) return n or len(n) > 0 def on_iter_children(self, node): """ Returns the first child of this node, or None. """ try: return self._nodes[node][0] except (IndexError, KeyError) as e: pass def on_iter_n_children(self, node): """ Returns the number of children of this node. """ return len(self._nodes[node]) def on_iter_nth_child(self, node, n): """ Returns the nth child of this node. """ try: nodes = self._nodes[node] return nodes[n] except TypeError as e: return None def on_iter_parent(self, node): """ Returns the parent of this node or None if no parent """ return node.namespace # TreeDragDest def row_drop_possible(self, dest_path, selection_data): return True def drag_data_received(self, dest, selection_data): pass class NamespaceView(Gtk.TreeView): TARGET_STRING = 0 TARGET_ELEMENT_ID = 1 DND_TARGETS = [ Gtk.TargetEntry.new("STRING", 0, TARGET_STRING), Gtk.TargetEntry.new("text/plain", 0, TARGET_STRING), Gtk.TargetEntry.new("gaphor/element-id", 0, TARGET_ELEMENT_ID), ] def __init__(self, model, factory): assert isinstance( model, NamespaceModel ), "model is not a NamespaceModel (%s)" % str(model) GObject.GObject.__init__(self) self.set_model(model) self.factory = factory self.icon_cache = {} self.set_property("headers-visible", False) self.set_property("search-column", 0) def search_func(model, column, key, iter, data=None): assert column == 0 element = model.get_value(iter, column) if element.name: return not element.name.startswith(key) self.set_search_equal_func(search_func) selection = self.get_selection() selection.set_mode(Gtk.SelectionMode.BROWSE) column = Gtk.TreeViewColumn.new() # First cell in the column is for an image... cell = Gtk.CellRendererPixbuf() column.pack_start(cell, 0) column.set_cell_data_func(cell, self._set_pixbuf, None) # Second cell if for the name of the object... cell = Gtk.CellRendererText() # cell.set_property ('editable', 1) cell.connect("edited", self._text_edited) column.pack_start(cell, 0) column.set_cell_data_func(cell, self._set_text, None) self.append_column(column) # drag self.drag_source_set( Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON3_MASK, NamespaceView.DND_TARGETS, Gdk.DragAction.COPY | Gdk.DragAction.MOVE, ) self.connect("drag-begin", NamespaceView.on_drag_begin) self.connect("drag-data-get", NamespaceView.on_drag_data_get) self.connect("drag-data-delete", NamespaceView.on_drag_data_delete) # drop self.drag_dest_set( Gtk.DestDefaults.ALL, [NamespaceView.DND_TARGETS[-1]], Gdk.DragAction.MOVE ) self.connect("drag-motion", NamespaceView.on_drag_motion) self.connect("drag-data-received", NamespaceView.on_drag_data_received) def get_selected_element(self): selection = self.get_selection() model, iter = selection.get_selected() if not iter: return return model.get_value(iter, 0) def expand_root_nodes(self): self.expand_row(path=Gtk.TreePath.new_first(), open_all=False) def _set_pixbuf(self, column, cell, model, iter, data): value = model.get_value(iter, 0) q = t = type(value) p = get_icon_option(value) if p is not None: q = (t, p) try: icon = self.icon_cache[q] except KeyError: stock_id = stock.get_stock_id(t, p) if stock_id: icon = self.render_icon(stock_id, Gtk.IconSize.MENU, "") else: icon = None self.icon_cache[q] = icon cell.set_property("pixbuf", icon) def _set_text(self, column, cell, model, iter, data): """ Set font and of model elements in tree view. """ value = model.get_value(iter, 0) text = value and (value.name or "").replace("\n", " ") or "<None>" if isinstance(value, UML.Diagram): text = "%s" % text elif ( isinstance(value, UML.Classifier) or isinstance(value, UML.Operation) ) and value.isAbstract: text = "%s" % text cell.set_property("markup", text) def _text_edited(self, cell, path_str, new_text): """ The text has been edited. This method updates the data object. Note that 'path_str' is a string where the fields are separated by colons ':', like this: '0:1:1'. We first turn them into a tuple. """ try: model = self.get_property("model") iter = model.get_iter_from_string(path_str) element = model.get_value(iter, 0) tx = Transaction() element.name = new_text tx.commit() except Exception as e: log.error('Could not create path from string "%s"' % path_str) def on_drag_begin(self, context): return True def on_drag_data_get(self, context, selection_data, info, time): """ Get the data to be dropped by on_drag_data_received(). We send the id of the dragged element. """ selection = self.get_selection() model, iter = selection.get_selected() if iter: element = model.get_value(iter, 0) p = get_icon_option(element) p = p if p else "" # 'id#stereotype' is being send if info == NamespaceView.TARGET_ELEMENT_ID: selection_data.set( selection_data.get_target(), 8, ("%s#%s" % (element.id, p)).encode() ) else: selection_data.set( selection_data.get_target(), 8, ("%s#%s" % (element.name, p)).encode(), ) return True def on_drag_data_delete(self, context): """ Delete data from original site, when `ACTION_MOVE` is used. """ self.emit_stop_by_name("drag-data-delete") # Drop def on_drag_motion(self, context, x, y, time): path_pos_or_none = self.get_dest_row_at_pos(x, y) if path_pos_or_none: self.set_drag_dest_row(*path_pos_or_none) else: self.set_drag_dest_row( Gtk.TreePath.new_from_indices([len(self.get_model()) - 1]), Gtk.TreeViewDropPosition.AFTER, ) return True def on_drag_data_received(self, context, x, y, selection, info, time): """ Drop the data send by on_drag_data_get(). """ self.stop_emission_by_name("drag-data-received") if info == NamespaceView.TARGET_ELEMENT_ID: n, _p = selection.get_data().decode().split("#") drop_info = self.get_dest_row_at_pos(x, y) else: drop_info = None if drop_info: model = self.get_model() element = self.factory.lookup(n) path, position = drop_info iter = model.get_iter(path) dest_element = model.get_value(iter, 0) assert dest_element # Add the item to the parent if it is dropped on the same level, # else add it to the item. if position in ( Gtk.TreeViewDropPosition.BEFORE, Gtk.TreeViewDropPosition.AFTER, ): parent_iter = model.iter_parent(iter) if parent_iter is None: dest_element = None else: dest_element = model.get_value(parent_iter, 0) try: # Check if element is part of the namespace of dest_element: ns = dest_element while ns: if ns is element: raise AttributeError ns = ns.namespace # Set package. This only works for classifiers, packages and # diagrams. Properties and operations should not be moved. tx = Transaction() if dest_element is None: del element.package else: element.package = dest_element tx.commit() except AttributeError as e: log.info("Unable to drop data %s" % e) context.finish(False, False, time) else: context.finish(True, True, time) # Finally let's try to select the element again. path = model.path_from_element(element) if len(path) > 1: self.expand_row( path=Gtk.TreePath.new_from_indices(path[:-1]), open_all=False ) selection = self.get_selection() selection.select_path(path) @implementer(IUIComponent, IActionProvider) class Namespace(object): title = _("Namespace") placement = ("left", "diagrams") component_registry = inject("component_registry") element_factory = inject("element_factory") ui_manager = inject("ui_manager") action_manager = inject("action_manager") menu_xml = """ """ def __init__(self): self._namespace = None self.action_group = build_action_group(self) def open(self): widget = self.construct() self.component_registry.register_handler(self.expand_root_nodes) return widget def close(self): if self._namespace: self._namespace.destroy() self._namespace = None self.component_registry.unregister_handler(self.expand_root_nodes) def construct(self): model = NamespaceModel(self.element_factory) view = NamespaceView(model, self.element_factory) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_shadow_type(Gtk.ShadowType.IN) scrolled_window.set_placement(Gtk.CornerType.TOP_RIGHT) scrolled_window.add(view) scrolled_window.show() view.show() view.connect_after("event-after", self._on_view_event) view.connect("row-activated", self._on_view_row_activated) view.connect_after("cursor-changed", self._on_view_cursor_changed) view.connect("destroy", self._on_view_destroyed) self._namespace = view self.expand_root_nodes() return scrolled_window @component.adapter(ModelFactoryEvent) def expand_root_nodes(self, event=None): """ """ # Expand all root elements: self._namespace.expand_root_nodes() self._on_view_cursor_changed(self._namespace) def _on_view_event(self, view, event): """ Show a popup menu if button3 was pressed on the TreeView. """ # handle mouse button 3:" if event.type == Gdk.EventType.BUTTON_PRESS and event.button.button == 3: menu = self.ui_manager.get_widget("/namespace-popup") menu.popup_at_pointer(event) def _on_view_row_activated(self, view, path, column): """ Double click on an element in the tree view. """ self.action_manager.execute("tree-view-open") def _on_view_cursor_changed(self, view): """ Another row is selected, execute a dummy action. """ element = view.get_selected_element() self.action_group.get_action( "tree-view-create-diagram" ).props.sensitive = isinstance(element, UML.Package) self.action_group.get_action( "tree-view-create-package" ).props.sensitive = isinstance(element, UML.Package) self.action_group.get_action( "tree-view-delete-diagram" ).props.visible = isinstance(element, UML.Diagram) self.action_group.get_action("tree-view-delete-package").props.visible = ( isinstance(element, UML.Package) and not element.presentation ) self.action_group.get_action("tree-view-open").props.sensitive = isinstance( element, UML.Diagram ) def _on_view_destroyed(self, widget): self.close() def select_element(self, element): """ Select an element from the Namespace view. The element is selected. After this an action may be executed, such as OpenModelElement, which will try to open the element (if it's a Diagram). """ path = Gtk.TreePath.new_from_indices( self._namespace.get_model().path_from_element(element) ) # Expand the first row: if len(path.get_indices()) > 1: self._namespace.expand_row(path=path, open_all=False) selection = self._namespace.get_selection() selection.select_path(path) self._on_view_cursor_changed(self._namespace) @action(name="tree-view-open", label="_Open") def tree_view_open_selected(self): element = self._namespace.get_selected_element() # TODO: Candidate for adapter? if isinstance(element, UML.Diagram): self.component_registry.handle(DiagramShow(element)) else: log.debug("No action defined for element %s" % type(element).__name__) @action(name="tree-view-rename", label=_("Rename"), accel="F2") def tree_view_rename_selected(self): view = self._namespace element = view.get_selected_element() if element is not None: path = view.get_model().path_from_element(element) column = view.get_column(0) cell = column.get_cells()[1] cell.set_property("editable", 1) cell.set_property("text", element.name) view.set_cursor(path, column, True) cell.set_property("editable", 0) @action( name="tree-view-create-diagram", label=_("_New diagram"), stock_id="gaphor-diagram", ) @transactional def tree_view_create_diagram(self): element = self._namespace.get_selected_element() diagram = self.element_factory.create(UML.Diagram) diagram.package = element if element: diagram.name = "%s diagram" % element.name else: diagram.name = "New diagram" self.select_element(diagram) self.component_registry.handle(DiagramShow(diagram)) self.tree_view_rename_selected() @action( name="tree-view-delete-diagram", label=_("_Delete diagram"), stock_id="gtk-delete", ) @transactional def tree_view_delete_diagram(self): diagram = self._namespace.get_selected_element() m = Gtk.MessageDialog( None, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, "Do you really want to delete diagram %s?\n\n" "This will possibly delete diagram items\n" "that are not shown in other diagrams." % (diagram.name or ""), ) if m.run() == Gtk.ResponseType.YES: for i in reversed(diagram.canvas.get_all_items()): s = i.subject if s and len(s.presentation) == 1: s.unlink() i.unlink diagram.unlink() m.destroy() @action( name="tree-view-create-package", label=_("New _package"), stock_id="gaphor-package", ) @transactional def tree_view_create_package(self): element = self._namespace.get_selected_element() package = self.element_factory.create(UML.Package) package.package = element if element: package.name = "%s package" % element.name else: package.name = "New model" self.select_element(package) self.tree_view_rename_selected() @action( name="tree-view-delete-package", label=_("Delete pac_kage"), stock_id="gtk-delete", ) @transactional def tree_view_delete_package(self): package = self._namespace.get_selected_element() assert isinstance(package, UML.Package) package.unlink() @action(name="tree-view-refresh", label=_("_Refresh")) def tree_view_refresh(self): self._namespace.get_model().refresh() # vim: sw=4:et:ai PK!gaphor/ui/pixmaps/__init__.pyPK!)gaphor/ui/pixmaps/accept-event-action.pngPNG  IHDR}\sRGBbKGD uj! pHYs  tIME;ktEXtCommentCreated with The GIMPd%nIDATHݕkAf80&& ƈ⾝qiM1Rss D"TIY3y.`z\ڵ+OJ=Y} ǃ M(Ο;#bԲ\fv8~l;}dA|eض"a5V=z܌K߻{!UDlG ?UfW/_^0ƼgBuROLZGO-̎BuMMU:XtЖɓQUDJ1xФe Y4zP' vNԲ`%hԋ k0J <~BG elSӦd295v)Zҧ8nۈ< #ZZT&*HD#_Ou++ STfMox+GO$ywo4ņݹ}[- y߯7ŢJOO NƝ,ܿoZ>O b~T@]z9 ` DxWEi۰hio+ vd, Bυ IENDB`PK!Պgaphor/ui/pixmaps/action.pngPNG  IHDRw=bKGDC pHYs 7˭tIME . DIDATxKTQܱ{uprp\h"]DQТmE$QD(̊pMm"EEGgZUљQIMy~bo^ϕ5bc/T9S!{,#~48PIENDB`PK!ߐ  )gaphor/ui/pixmaps/activity-final-node.xcfgimp xcf v002  BB*/ gimp-commentCreated with The GIMPS gimp-commentCreated with The GIMPgimp-image-grid(style intersections) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches) FEmpty Layer#3     @@@qq   qq      qq   vv~ Drop-Shadow     1EU"(()("'rͶs'/ݘ/''rs"#())*)*()"#ss'(/ߚ0(uθv)#())(#Empty Layer#1     @@@N        ##M Empty Layer      @@@6SS M  M;ee;IIe eS SMM    MMS Se eII;ee;I  I \\5Drop-Shadow#1     %    (())(!%]]& 4]5+**+5]5  Pf..fP 5\\5%ff%]. .]!^ ^!(5 5()+ +))**))**))+ +)(5 5(!] ]!^. .^&ff&5\\5 Pf--fQ  5[5+**+5Z5 &``'!(())(!PK!:g>Y(gaphor/ui/pixmaps/activity-partition.pngPNG  IHDR``w8sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<hIDATxI@Q#V!%؁X؁%mWA|ﶰ &u;b  &@feYl>c,[71bĦ׀4n^k  &@L1b?Ynj8G31b_֞_^7 &@L1b  &@L1b  &@L1b  &@L1b  &@L1b  &@L1b~eb}ٟ_`b  &@L2Lyϒ爝1b ۘ1beb}/X;=IENDB`PK!Kgaphor/ui/pixmaps/actor.pngPNG  IHDRw=bKGD uj! pHYs  tIME , vtEXtCommentCreated with The GIMPd%nNIDATxڥjSQ/I'`/WQ:tdTڙg9)>u"J 0mmKk'+rMR };y` yW撤 C#iS, BhӉiuf6̦/'$p2ȵɷ!cdRQ* ;, V*pGN;Bn/ɔ ӵ1ZmT;q-noй((RK)rl Џx7O XC=uN0zڵS| m^¡g}!4Vq ͢k,ؖrGRlZ Q}.jr?Lj=H[eP2B~7b +-_k7&2_9j!I[!ɂO&Ґ#I mx(\ sQFxxy8Qf> 1(p \V_D1vu@g;yIENDB`PK!C00gaphor/ui/pixmaps/artifact.xcfgimp xcf file88BB / gimp-commentCreated with The GIMP o00 New Layer#4      V00j00z   ------'00 New Layer#3     T00h00x b ( h \ V S b00 New Layer#2     900M00]   a<DD:59 )Drop-Shadow#2       ) )         $%%&&%$! ":NX\\]]\YSE1 :ea>! NʶlB" %XÜnB" &\ělA  &\Ùi=  &]_0 &]ܶ~D &]ʓS"  &] ӝZ%  &] ֠\&  &] ס]'  &] ס]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &] ء]'  &\ ֠\&  %Y ΚY%  O ηO! :e f;":NX\\] ]\YN:"!%&&' '&%!    00 New Layer      00 )00 9a++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++a00 Background     0000    88 Drop-Shadow      8888 @ @ @&   &    $%%&%&%$  ":NX\\]%]\YN:":e%f; N%ͶN!  %X%͚Y%  &\%՟\&  &\%ס]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &]%ء]'  &\%֠\&  %Y%ΚY%  O%ηO! :e%f;":NX\\]%]\YN:"!%&&'%'&%!  &  &PK!I!gaphor/ui/pixmaps/association.pngPNG  IHDRw=bKGD pHYs  ~tIME _0tEXtCommentCreated with The GIMPd%nIDATx9hTa/(A Lw\DlD "`VbZةU@%6FDS ;Qb\a84^y9ɍX_Z؋mhp=a[ yV' ĊRo\ʨ(/Ǘ2>lĢ.\K!xQW֧C10uu7A^E*(SRxr$2prz*ϦʴcKcUUao[ ?_DcL|6 ]Dxcգ8hѽj1z!wuuFL}-!j^[]i͚]PiYv=~1DywIENDB`PK!aF(PPgaphor/ui/pixmaps/box.pngPNG  IHDRw=bKGD pHYs  tIME#鑧1tEXtCommentCreated with The GIMPd%nIDATxֱ1/Fק^p Jd4^ep"X́[ܓ2R:9a99^/cO.wJ`78J;0W,/O;z;a 07ezծ'}ux%2tITCcΕ-6ho5w-5Tr"2IENDB`PK!eegaphor/ui/pixmaps/class.pngPNG  IHDRw=bKGD pHYs  tIME  tEXtCommentCreated with The GIMPd%nIDATx= 17 &B <&@/'X(bب,VŸi0D)$'uB]`z'+Q`:s>Dq"]oZ33z1v'Or|5}͌,nesæM5sn+8]eĀwrPIENDB`PK!8rVV"gaphor/ui/pixmaps/comment-line.pngPNG  IHDRw=bKGD pHYs  ~tIME22>tEXtCommentCreated with The GIMPd%nIDATxڭKamޕb+|GAĄQID ? x :43Ǵ3g-5Yѽ ^]}sk9ŲX,͛A 9-jB<;::.ƃUFxUw]ommQŻfQ%cbO@w?Nq`Hv誢( K@@@$YmkkH$3'mIg=.'&&BWd TRg~pxtAMMͥd2itN~q5JP__>N?:zF@1KKP` !nz_,)5%JJJ6N7J-NWS-/>}(U2$LcIENDB`PK!xagaphor/ui/pixmaps/comment.pngPNG  IHDRw=bKGD pHYs  tIME L#tEXtCommentCreated with The GIMPd%nrIDATxO+DQθ3҈!YH d$YYٱS|vvvwPdRcnUs, ʿ2sݽ{sQ`JJKH"Hٶm}7#B:=;eY8>/y4dsRAku m3 optTj;$z4Fg_۶lko_t1>L<`I U g,3Oc=taF [4٫KTC38oIRJ77LĢ&pު !ćƟPӂׄ5@ o.w@o+b|8i )ܻY=EOwPIENDB`PK!VQQgaphor/ui/pixmaps/component.pngPNG  IHDRw=bKGD uj! pHYs  ~tIME  5HeIDATxՖKa?NÄ8*H.v(ATl,ExBD^8(jt햐РluwBL;j^<>,Z 9/* ;˲ڶAJ z' E4519SiuS4 BARJ1:6 *o *mk7FjkWI /pn%aiRIٗϋ&7&4K4 w%䀪=VVL$L)4O6*3\hU |PD9hWdښ )7 #H$ ۴@u{G_wmO/2p$/k-4?S,/-FNu`  q ]X 3 Jmݧf pDIENDB`PK!KKgaphor/ui/pixmaps/component.xcfgimp xcf file000BB"/ gimp-commentCreated with The GIMPI`00 New Layer#2     "m000000 New Layer     0000rrrr,0Pasted Layer#5     v,0,0)))))))))))))))))R)))))))))))))))))R)))))))))))))))))R]################Y00Pasted Layer#4      0000/   ''''''''''''''''''00Selection Mask00 00PK!$$$gaphor/ui/pixmaps/connector.pngPNG  IHDRw=sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATHՔ 0 EG-0 S0F !r伧ıe8ҁs䰤.]/H"I =֪y x$Âԕ K0»OA/WAa6$M"vZ]jB囦@NJE? bIENDB`PK!/@@"gaphor/ui/pixmaps/control-flow.pngPNG  IHDRw=bKGDC pHYs  ~tIME , vtEXtCommentCreated with The GIMPd%nIDATxڽ͋a{11,4eHYyIDRb(+` c#I1b0|o&Γo==SW 1%Hk!؇s 5Й2C8i)b"\ŧ8""0C 0xR/Lb9Wn89fTԨbYȿ6F F؂̯`S'-'q\ $H_1[#3<:rpQ[_m7/!ú?TޑS K+,ֶd>'`'%؋7!?Ȼp,T&vI :r84o9}3n_nb0;f GJn\)Pҙ7hέ? IENDB`PK!7#gaphor/ui/pixmaps/decision-node.pngPNG  IHDRw=bKGDC pHYs  tIME  tEXtCommentCreated with The GIMPd%n|IDATxڵKq_ߏ,="m'#XȤm^!8YPHLX)ؔe(?(ofĹK0e5z<~?$dMZfs2 xQnPE/nhŽ^B655I"kPZp`0x^UU'lhhj`4VU_Z$*MLJEQd[[ p`4/f_ʵHVU-@YAhRELx8Tɺ:p\G6 S;.<~95'nDX\\luW2G{.pv6+i"P*[[[X,#HEa4>&Qe@J)YCYuww_3 ;o[GlWߙY nGyn!kCSYY-EccTR!N)g>ϩ$PRh0T+f,U\JXEssKɹZIE?{Q2~#* ͛4 !nOY*=HABH˵Lr9rzz:(JRbsަg1 P8 sPIENDB`PK!إ}# gaphor/ui/pixmaps/dependency.pngPNG  IHDRw=bKGD pHYs  ~tIME 5tEXtCommentCreated with The GIMPd%nUIDATxڭ]h[e49$i҄f4a(Cm6lh(1EAdVʍav' Aoz9؅l2d:tt nBKs9899M ۠li\}eqv,G?\-\^ϞԺ*rАKz9'VW/UxgtKk}`h B!Dp@+d4ޝ/̟$0˲.Mѫ} 1 38[, L7S3j[aWiǐFrꊢ샇\v2, j%YښY?~2 yM 誃-h>e[!d&(늢̗ջl~0 BUv3]өh]ӧ>F7ι\Dh]}3K|hN6!NJ;0dS;񡉢X Җ&bn?31*Nė+bdY>0wqdqx%LUoh;-yj7bX9H ~0 g˱sDs%߭cefƂ\Smt]W Ӹ)2G3#b8~~!T/4k=޳'[𢩏Kv*kCnJJ? ־}ǎpcؐ_h 4J5N,0f'E, yfux|'(WKb51{!XPdK/"FA ~9ƪpWs1ZhŘ ozԭ_Tteʔ^xoyҲn.]X-Zx-+6ǾDAl*gBHsuؑe˖^3ZL2K<9~WZa5Bh;:pзyn3eDrl?^  W #gkev_8"z\ oN+wɇ5i2jRII #Z #KR G ;p 5U%j8*0 c(41 (p'_>k("B@WxƎu֍RbٙSS>VZ8_YʶPWguS)YUGe{ya(Jng}lJM%3AAA^W^$pBqI)/E%3ZEJKKII^ϺukE !֥K~y =WE^+\ vn 'A/i!IENDB`PK!yngaphor/ui/pixmaps/ellipse.pngPNG  IHDRw=bKGD pHYs  tIMEVA'tEXtCommentCreated with The GIMPd%n2IDATxڭֽoNa9F%US[_"a@HG,j`06I$T ZB}-ב۷/IF͢;p ;1H>zIGE/Gh&ϙ<>j8-"ISSS'{C"83q!wR&l@tdCL?%dYI,ȅP0r{@.\:Lhl"en;fڭk Jr6]P"+oln|br4:t (Ncu:eT*uH4;Hg v]S J0@ 6Ssb ^ ZQu]b\Oӳa|W%@~oO+__nۖeñV.L&Q*j@]AU/ʒy<y%?xB!C'|t;W{|LͫBT ՘#漜@ hyeR8_6˙L&zD3ҠSGǽ^o xhl xwg.kh.>wf%6Sъ \h_=0'VWfr,[4۟2{*>u$9vz"ۭi\e1*gb}/D֚[z.*x<^I6TIftEBS9jE3h2ꐢ(@VՎ wܙvAxucv=!* pSX,v (b Hw/*%*m_7 QÒ(]B~{aw 0 [sLk׮cjL~`\IENDB`PK!xGrΏgaphor/ui/pixmaps/extension.pngPNG  IHDRw=bKGD pHYs  ~tIME !#]\(DtEXtCommentCreated with The GIMPd%nIDATx핿A?n2 M&G!X(B* t&?`_ ׋jzɺؤȝ=7o"l n7[(ʃp']}64@mJ\V,J-oz*ʗ3Mfk`8wZM8T*ct&іeNT5VD"qdAymmWntvsT*C˶mf~w1bjMAba >7cIENDB`PK!-!gaphor/ui/pixmaps/final-state.pngPNG  IHDRw=bKGD pHYs  tIME &x+tEXtCommentCreated with The GIMPd%n5IDATxOhqlfj2vvqpEI)\(ENĔJNV$"r!j?mOYemϳٳ|[?ϗ}{̠3#X|9J\Tɡ-؅؉b І^\(2̼ۀVt= EՋsglE7V$!\%mL;qnRLkp؄E-Gbo^ϕ5bc/T9S!{,#~48PIENDB`PK!@\\%gaphor/ui/pixmaps/flow-final-node.pngPNG  IHDRw=sBIT|dtEXtSoftwarewww.inkscape.org<IDATHՖMHa; ~+!B/=FZDy)ĬSTVH_APbPxW,[ʨ4+涖mOwYҡyxw/럪z(@,̭KG_X0@ i^Acv56L&YňFg90ƀ"_3D)%:N˛ Gy6=.njG`/rCuDPjVVV4 8FT.>_ix3=# j5k$sbtkw#Q :BQ GҺ<69 gwj<{Q<ʽ& En%'VH(nZee E.(-+C)SZnFuNomjjk oue拷8)Hp\'T E*N@v-,,3S8GP>|>?m-9y$O iE'Bu3ﰴ yh`8AFFYAP-vHii)> 6 Y4x08-]4X,Rj E$[iZ u8HUU5%%%EE 300@8t3p'wWDYwA9pXv,47}`f pF<@tLZ^9jͥIf`CR|DdiM7ɅIENDB`PK!¥Lgaphor/ui/pixmaps/fork-node.pngPNG  IHDRw=sBIT|dtEXtSoftwarewww.inkscape.org<jIDATHձ @ p[GrDZtpGxy1GdB}M Xnl#i}| (H T%\nW3o7 IENDB`PK!C "gaphor/ui/pixmaps/gaphor-24x24.pngPNG  IHDRbKGDC pHYsHHFk> vpAgxL $IDATXݗytյ|ߌC `!$ (12X"-Z`{VJpUz J jR!@V$H S ?Zz붫u:s&/ ^O9[dޕ hA;@EtI ' xysti2-62d8nl7u(bi*;Xݤy3ǃ psi΋h"eM! qM. 'E\Dk/.B%1b `DQ@x+PGQqhFmԢ}zs\82G53!x<\}P|iANʕ_/j5&a/vty`n25ՒFJADUY::W/*Ԅ-}w~S;R\wTx!Ay.m^muzBLbFIp(캶=fk0[ 5N2ZjҞ\ xi hOz oݎ̟/T GMyύ zui`q}FoKV#v&LzѢ t#X/zHJjkfɅKU /\ljKw"e9EDA#fD@}:9fkAImԔ˳ gH'p :=.:jI1Xe>G Gc@_6ޫzg%O+k{ܰDu!rzŅc<0` 8 4fh  z#'dϩ8?J<ױʊ5wmR;cJ]wfyUсX ggMJhzxdE\CN{ȵav#4/٨ѴDCϽT)Feāg\]dJHS༎t53(B߻c:ke&k#J)h?ߐ*Oɂ"P2T'd6ɳ癙$d2$g^yngY TrŁ=im˳翸ϯ?_;'VY=>\=X66 >fɺn\ͲW̥n|m⮾//͏#D?`>Tsv\0> 4RGfJQ/{mlx}']-~`[<1ҩPT~JYf?}y}D@F4lc'A:5s?.Nsͬ,?[xF!B4E1r"=wݚ޻#zqW\7ӰpěE(u1#C]@@?G]ww%}{Y{1H!5@іUJ=:RON#ԭ)B?NQ:k7v#'fY\Y`@]I4!Hà[WASOS t tFdwB(Tt, iqH=4 u4 Ј,ub:QjCg;1,p x R0uAZOI<VThy^*tqrȯ?G&BіO`fJ} :5s4Щ0M AΫBqq['OB#7[[C=Ntν:U#аH#.hHC1 i3șΡ!: 3W]4tZ8u0(}i(ԿNFXxx[9ި k)QϽ4> \<~ddXo _z"G<&⇭rҁGψV^٩ toE \)=_V^w[Ы#̩:~v5ԌHq)dQ9o@,]g tMLɌq?zy|*l7o3f@\}ш&cgFWq'!B}J~b-szNw?{ͼwղ5b^t= 0$(D4B7qU;`.XC@ UKX]|jAG)BYgᷟ?W ~UGHXk=ْ hztҙyc,?ZBd ixJҰP)70N-R; |inf,lN JfDdLhoFM&hF;>lB&:[l` :c@wz?H OTcFc9;b縃Z=fĖN@$5ԧ'xpE]F~#0MXw&]ؘ$ӸSQL8.N0uTic=~}=m*N=Go(3J U*(F<+ɶCN^ou#SGR hG?'cVWb=ai,#/ӯo~'s[,u5#p |nR낻oo×WR%TR.+3~ҽ;k6}vhX.7g=_1{OZw۔=,=e})һ{w,ӳdsYXŸ׿55/? Cj>6.Y;X;-t teЕ5P|es(w|޺5EYv>rK""?o$Z} 3( T:VҥJg&t;{.t/0kbwz^_v6cI"ub*rUFP~TAQ4Rf ]@Z9ϛ{uK ~ W ńA` v  5 jPZ N+O Nx ͼViXG :'j̤BMi@QAVPn0 knVuӰ^{@asw Vyk>S^Ջ9an8"0,.9h 0hPz:~02T@(D.F>t)t|o#St]₥gqz+ M7_7 -}i#@#ȻWfZPxWE[PTT -Īw!uߣ`Y\0/Q^4,z s%-hQ٠4s6@/X 2@5K9ӰؼL  P?8(Ϡ K`( 5DN R9h1@y7K6mUh!)K`m!k(!m j2 P(j׮0Ӵ殢aȍ}Y^T~).=5n>&t46kDP g פ2FD=0.\x~0hø~Lq)IFUCɠ^GľD;K8{0^ K něz/o / i uxWFW}Bڱ Δ-cC`ŜoUtJdhI_ۡ{jJ*=ooO=hy㮔%&93صFdgw|^T;vu'zeߋ+YB |m*^|D{c t.Xͻ]<"T}o ~;3ȀƵ\ma@GJd!~`%})C۳vi|] S?SgMjIM㲀Wq.+t[ˠ˪ SK׸;%\>(gy[ykQ Y@+$gYWUѧsY|iּFQrʇ|+ 'R% $ Tܱ Zw,QbbQpJUr{8p=LTf@ Ag,i$ `V":-ty-ѥb5ab;X17}%Fǔ؊sتw8.X\=?0ƃKx Lg[8)$Q͝lĄW$Pe"z.5m)jF _s2"On9.b(Q-؍kbo扻X 0Ȫ$b{J'-VPXxT7!yn/>/ÞnSI"4ޜX*>)#B%8ͻ: H hջYO3f )ؤ0-f)@'J`T B!6X X_4I`~Pے!J=&+ L*XԐ%` M' 醴$3$^}¢1[)dQ@+֛$5r7>0mb =G:N:]xmi*,AJ+(Kd eO4w->sL1?Iz1G$ {nܪ4DiI:4VeV$GNSc5!ZAM(x Ѥ[Ӽx z𫡩2DtoG&x&馄dȒF>#H 9:GԖm0'I*x4{m kwBG1#K gn "gF {P1-l1ʓ#XJ|r[-xԗcFY4loL6H89L(RC1tY LrR\ OҘ8m{\^|Ȇ$1jI0n "  +xԔ]ti}WMq)MSz:>2.Cbl!3|BQ fI䑿*Z.]c~{V2gfļ0ESZ"t`VJ5i%<ŕ!& q򰓸 ռ%kqa0l[cn䅑&Ͽͦv'/fIRFWB h|a&}>gξd<>c0oKII#NSE7r?*S׿J*J*J*J*J*JvȞ|WS˟˚ ^2)&iVwH?m `2<|NϬn2T*l#`GN<{$/U(ˑ/3NӲG{6W0 r> ziSTlܺ5ܘV09=GEBݰdwT 5掠]S%W/>fvT."lc7g[餒eT·'1X>w\x/Yف 1+S+NX/}n\}T4cS z۽ ]h:Hҩmxhe }-c ˧d< /n̓3yfKb`5\v Uq7׊Հ v2Yx{^1?_ҿ_zl>n~H${7̸l.iC"j,z`5uK3GɦsK\ n,A{p78/j` &ԭen᭢Xlnq7Blyi.kdy+%6wM4mx·(SeBL*+iL/ *.~`-fTl^͔o#l=n`νspѧpeKcfOPjMdguw)ßK#R n@bR<4X^Ѷsnst"J~" u/S6g(ݳ+>?zxu'ʗw\ܒ./EQ}YaAҔ$_+-@nC)3R_$GW'~0Jaf [YXaBg¯:˻@s@kbwNW\1U*>r ~!I4%\ȤcCuɱHC0PP@J-r)͚_MF`D~Af;X1WY޽{_>e"hN%qbٟ4_ 6~ #|ٰMj'-(X9AG.Eff>b-P ,<9-5ϛ)5*F܁5_#c7pANJ⤋?[..0*b5Kz"C֋5^%狝/U$qawk+}yB_c-gOv [Y^QJna4ͨIor,XCNbl@s ;XԴ٨ʼ!H*-d'6 . SQ`%%eYuZ.-1~dIJ;xij6Y)sCR`b 00k(yuRҀZ _q/vT% j.ȿוyuGogMJ 2eiƳ<{ i쑱ߡkD)&>WiLҤ2p+u1< ВDm&|yHKCS0SP뾩0u!'@ "%]@1X|̶A힇S = &7c5 y(wՁiIYjeI&"E-2}={fSE){3-H@.@@$ ]|%}7Bdr'5ź枟7) FO@G@=RHj4v+!-M$J7 δ 6B/ !p v s1B+AA忩F*4O$ a|aV++='c\}~_ { li xIbOY.O+I3T+ܞꞛݿ|2{7mR=o9-_+u/9Ц%sơMA8hψ(L6@!S(>7(4QH5%(@#?+$R06[8S(N #"P"!>,=?HЊj֒dJON yfŻߴRK3H;H:nÌlR `0#ex^yk m#BϑƈCɟi5i"gtD*L|9F IDAT|nPn~3Se^ *YҁiC i " H7g0 tjeg jSb4JK A+kQaJD0F 9xIK^Xe)ҮD׷2 \1WIJWzd /D0m& vLs$x,Fb0WWf0GT0c}B<4)<Aڌ-PfO˦1ASl-w"i RKf 'C7pIx n" $|IڑO&%h %caqx" >'Z`ߺ&^?,)y}/Fâx$4U:tr"XU9 )oe*kk5DV  nOS$,חWQn7HMHSKH!ݏH{9wg!4 v{cQI mkMaHk9ZXCŖNSN7||SZSb-jTJ"ˆGkƑG20~,3Mb\5202J?+欹q"M[\oϛ:q~,4tqB:K|9{#mPX7FLy̑ kXD[)` ꘖRǪ.F]do&/Ąksi{]bLKsJ904L8m54e/f*`ۚ: M%}'xjb{{)G?y ]Ö#rx+k&{iʆN+khai/Mg΅YZcv| _@Ga 5d Gs <ͫ<254`-4uGkԠ~0^z-S@b*򇛘/o5L 'aN ./-MMLArz+#je 2o(XC"rqQԱMf %ZumS+ET~a#2kHh6وf&w5]PbvQ9h6Wb5d5Xey}_%_g7DŎ?,'Iؼz|n&1~?FVxqF:2hp7T7y*{;jd*J*J*J*J*J*J*J*J*J*J*]O `^Έ_pƎ^{z`{U곳Me<2Tn{և7mܰ쾊Ǽ`}ϙgeMR. ryMD~-Yܭ qj0Ϭy *_fLoƑ پG*؉rYG6ޙY|]vI>S霢([xe;P}zT>Xߌpemg3{rdQl:5OWrŁ=ӳ!{Ү 6ޱ2(Whw)Qq޿2-H/Vv;Џ3uNS2wk2=A dN7矆BH _{|yn^A%bNQp|Q@y5vKtV!5VjD~5}~f+"?^ܝ% }~Jo/Ywow Ȏ|(&@;l_=SeJ]; 9\ѽE3ٻs>7e+LӿT3 b ܹzJ+矨ȢJK;޸?9C9F";6isA qxѻ_ձ^;x7'_1ېIa1D*5yuk.wS@.| +9Z2 C7WHݭsz>L\SHct ɇD w)KVLAEqJvUlS+= x]' X lX \ 7\7!>0s/SYۖ}xl= M+IPohjP \ç3i<6̓s$0CXGobIg,;$c > kq8uk"Z(4ߴ:߹m_/Hj W:+_OZXֺdD Z c+B&ybF{3dV0*(W}3;޳x1#}|o 2' _\F]Qm5y$3Eb'A}bJ=FE&Z$m; l:PX0+Y81@b~߹f?4F̥kϾEѺG7T?Yڥ];bzIp U7J\㟻TA֍Ad'-*`%opc1FȬz DFG Q2/$áBf-u?P}|s}F?pi{:|tt/d?6ڋxbJ9ZdR(Y3)\+hfL3q2YBfH 3 0by@x>A<'4ò7"kN{KZ=?:+b_g!D/KPS0}9A3Z!(0аh5LJx#ϣB w3UYs:#g/a.<ߝ$Ȓ2& O5Jγy|'ˑs(L2j^rez(G%|9[_BZ<_\=l>uՃUn#Y\./ }v| "T]f(sdy%W@jgASU\ICUC<3 El a:X/8+("s j.Zs5ޱ4h=~>11z;K׭]:vI,t/w ^fSx9nM3T÷qP-0<r8' u/)Z^ܙnlzyw}żOP8&+ Akoy+LŸMH<=[pER@ziy]^z)*2LQ!ñɢ"LOŞ.42dj6F➫jkijbbź"d cXqrϐ5?RBoMa;sۍlF8`O(97js)|)oR/73jlנct2Ӱj6#{edFܫW17 KaB&EW>Zu %{J(? z,sF}[d$0w`]Rac'LM>cVjlB3_ԙ`^`Nc`G1g!.7"-'$B4Zk? }߃\&y8bpxkal7|F|[k4{-g[hZb>[ķ}%|( ƒ7!CSO*罆R'@eT{󐓇 !oh͏_XmsǍol,S_ɲNmڈ&m!CZ(b+9Abk"S6h'$ @DsoHZV 4#AP!ZXL+-1߅i 7|F|=?7E`kEXkl YdnĐ4%pF#;)֊07?V,R1[,a,o B~% sx ?+AVe7A.PA;&S_bo $A)qe]^ EkX@ 6ЖީAɡ#_E^d$ JHV[ 'hv4CЦھ,ӶS*ScDxCL4<S&QɡGla (b_yr$z|bXP>'iȿ9 4]R@!IAT)0>Z—>⓹\48SpNQf vpC֦XY3WZ_q2cJXK¼ 4egUKS d9CKY_#`d¥ |(24ú;BSMA8\+VH'F;"2999!9$c@d9Eb7c!99Tzi FAcT1>͉61 'PlBCs' `$)S q[7AG܁fj8\GBfPKQmkgkD5|rg񥙫j0u`m Y-v^6\Df(P0Eq -Cfכ)x=R pIQ ([=%;96|,5WyzwN6l6]`cl#@,dQޚIk<sj}.'&T w9Dj2 vYfw%b䆀MhR'vF9Bs֦l20 r(o5C,.1rHGN*B C44|r%c>7 ÓO/ [d6Xrai `HKhoNIpApCIyQ!El(d䀍h), g {h'xM!lgLu`հYDOZ( X2=*qM-qK/@z-ݭSe_tq3՛SKіO&3Hyذ"4ٌݫF c;<1TCVrr YdJ(90ɡ;! ~AB \ Y@;cP冠MAi  $i e>QKBu6c/kfMvl&vpZ%99d Y$QY @ģC`FQ²rCFGaK$@cI=OwdG2-$˂M'DCȒؽI+f 4f)YH+")V8d XJ()bż2F70K=7 M XFt5h0FG }(_ ,; 4F8aY6v9:9EDsVB4S?؊)-i.M%]4 ( '`w6Ĵɭ< Xd{0uv#rzB3%"q^H9xn1/U`$]|KKç9N6Ò14*uʯɟaFLx`5R0QM3zJVsfmcm{` 6o11p$0ç9A[;|".,uޟRPP˦y7tesfYB8th#vFWд kW6CYIPgA!'(09$yRCc0B?/L rW=\ߌ,>L=|kE љ%; C_d{t}C0-te0 CV05?yWn;f 2[uD#mRxmiQJf Y 0-˘Sv;?a'Pep` {Gutl%K0qY_#;YFOH1C3zl08_KH;Kû4ԇ ~r9,skDYRcaWx x"L_wϨ-o')\12ۘ?1#ӖtScF|31M n $_1{3'ZlkN`ڸ|[fj--;9c-6,fc73pk\4;PG0f&)fbn Dmyb j5_a>$Ȥ,vl~<39E:0[6BMv4:rBȶ񟨆h%/$ŕ\8)0Jzf~#N-?Ӷ9BEcQ1Jo?;Yb|"4x؆N-esmQmg4?5zxnNjs꜆ *< L-#xm7׸L1 -\H3c9A$j?8[[`l[ %TxZ.)*T=_f}i99PĶe-Sāavk$]E?eKᣃ/_zMم$fC>Gnrx )l A緮wr艬V??K-jj%U3h !ZvaCWvmHfop#+Nz/^y"Qx`QїtS|K~n$t1!\-tmV)cb> ȭ䒟J;^߅3}E%i {f7xwjK (Jȯg/e'\a'葧qiF IDAT\ca ʌ}kQ+i#-]TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TRI%TEAzݺRƞ) Cߧ-}x5kREJF+u?#6L)>;*@+7pF|owc찝"3n| U#^@%;QcQ40籽L-iagmش+TP1/3sHvJN?q^nL+UPvW ɬ6ho6|/2VR@%LOv_G۵EFӎl;k}aR6mάY{ڵk :fo[ :ݍ4ZSH ;:onmTQ@.cԪ6Ϙߞ˚ vLb ӫјDz+B$i&sјJK-}ly &U.=i30éP2mйkWQu,їC:~5jS߫Pq؃҈ΨcWOH5CQ劙=)VۧȦ{=OOHc}܋6Ϩyi sqmu*zDUWE0F}oP.*`^0ՅU_I?߰v U0c6d'"Wn%;a͢r省5,(W}bP4tLiy,_8ĮC'JǨJ\Y_%S6il\Ǣh܇zT)H5]M4S5kV*bn+R5Z+@tTqJїu{p)r`e-2K1'i]箩*T.dlOe;atƕS8i^+\{Z^SL٨XTzE,lVsױ-@3b_/t2q` _~/ %> \%kwKJ8"k0b15dy&mݭuYGIy=Vic̿ܚ7~M=9V05E_Lt׹y8Q Q%A@cHXVy[_}1=JGTg~w@:a,:ۀe3w8*6k3D0 Ex2Qd; 7 oB]t%[ M?{;788%yQ[ϰA.gGVL(Qc(ܛ ?w*W9yI?JTS|;ȼ́5f\p8͈~M>un d8uNn)#V HYՙqKdZ>f(_7ޏΔgrEk#\kf:{-[y2_c[\Ӈ ;~,@=F'8m9=hSSv_VKxm^z#4NPxqu;x({ܱj>un +2嘜ChO)4H[oc ,?hPEѺG_;n3e]@-ig|AIwp@&#"84ukL3]Ʌ-UIK(2u&˅p@iY&D9ZI[p Os W3]b*W2oQnTIdݿG4?9;ԆQ?iG >gl\n ˳;5.uA`ı|@P.8HK/A$`Zno?NcZ}=@l.uV{Mk]qwo~6[x#p"FHzmvtni-Y޽U8_U.<ۤ5Q|ܧ \:HrOhە6Ӗc 3^\n #Һhnc`? (-lM KHƒ#A@0j]i@Qj(ܫk9%/wÜ .`JVw{a+((RPVVԚ&]l56ŔӴXZV6]LӤVljUYւr;s3g;I&fν3yLJJ8M.@Nb 42M`Dj0dQ!<6 ȍ%7Ph,K3W9zq&5Ŋ@D>_ǽ nnkx"?>S*PTorvnͿ*`7CSiX InTp#PeHLM (ՌVyr͗+v\&p>޾yαH:Kʅ 8?*_m|TwBQTF AH`iY3JTLClol!P|_[>7U}5oM $)ܪqVB_'KbhMEB5xYuVktDQz-tUs^hWG$6F P}5 쮬T=((AyH,]fO*>'=6^I \"A̔12*_SWůKgp HlUn0=_ƇùIƅJsATdR) aD!ſIJ7]`O[sj'W\L?`sFɵOڸO|>GZz+H \Yrvw̉*~XlUW^/gm\ I4Tˊ )2DK~> Z5:tXՀ`x0BIB|,}񳓊 eE\8*OyZ oϼ|!ܘ<>;ŻZO1vzpBW8c;ŋ<_+J?WP eB5Iǧs)ۼ؄?o _ :ԭ7~!W&Fxc283?POU!ůйIZLѩ4C<8$` x6 )fê%x g% 2M ;U˅9bG_lx[km=pg\%(B!~r,rU>3?POZAmz}(Mn崭ARQڭ$ '-5hO`Q9&%?X>2ܕݵS rW Q],,J:"y俟(^#?P\k6}ϝh:.|j.&n޳M5_!eV* H<?hS6 8/70wq2?ڡmlጯl!? -hC T,* Y=.q{q "U6r!IFߺ Hz `n.$?כAssLBQ* qF'32Mg?B?h@ROwiy.>V3{<6fů (c}&?S4@j~pTz0~vO*⏅n-j/^Μ[̒ĥAxYǦT~}@zqP_7$J_H+FaT^Wy]UʅI ObJ%%W3"1cS[HML:M 3l bP(䙀vY,yqF ;S8#\ NUi iW+#?kkw_)=ٔ-Ι>IRH d>O4g:*eeE/4g]\l$ `ʋPER<ӏzʋ40Q Qn4lYu 8A"},ܒx{ &@ދS /Qdu *iuBHcTįj0A\JC=zġ8; ׯ߳~GFrA$W/؄51H+2 LRx1%& ڟ"(eDf!#LLY7{Hugl?j]O_cGWynm!WQ3oG*);V=O$ B_ׯ*\JC=ɀU5P6تo(JER;AZJ*5zHdQyMTtM_m!:~Js,ħϺlT+)V3}8V@n*}ҽg_TS 7ߌju+:Nű*ojz/>zOWh ƈs78=C4FV˙v&Hf p70ڋ<leAn P"@eVW?68KFQ2# Hܸȯ3~H/dgEbo ukR"*0ԣ~Fu$亡 P3ןe3f͐)ЃV3k ː$u&P/Bs0G&~^.D+ c $byP/= *l4S[c PzܓZu0`WOgׯ*\D~Y cħ5I!g03S VR̭J\f$EퟯhWG&eF dpJ TD >ki]^!@ ͵jb&@ERc[29C"b !]A~ӏYa̡j~U3/geF`-C=ø~*\Q>Ck ~Ejt^Lq u3 L@i=q_V=`02qA~L tO 5V3 :_ٺ'\17`@tU^×~*\!B{UߏůHs```{`i]hl]]™ATepEtXe?:Ԡ ]맟 nׯ+Pz@_lPT#1-?Wm2pnI1zYq&0ӂH$8!$3E˂Q 2_RgA@3*:S\ T8_> }1*:/U@!H0Wjv*B/:U;C) _M1'z ?ybzlwu"Lܟ!],30 -$  !ԟ3/J R4 jпV.ΪpˍVjPm`?Ipu'" 3@{&ϟ^`g9ƤHk tvad&nYP'5%j#؂(05p?5Y?Upv a&o풙įi>f,"ۗ s"$mdEO@t@g1éE` R_Vד5Ԡo d5aPK%CXP8\AڡPC=T2ӭtW l c%KL6.!KH݃Hs`HJp5A@Acj0oHMқP$I u'N%?'Hlj?\y?&0Kv=R`9QAC#܂`@k@Aнdz&OjgRC=̨Jx,Y#({Oo {7,79䙮3eWI&j/3R !l9j@LA ?qI \,~Mj±X 3P/R{c8`8),"L SƼEA @rg\C=AI qRbmNEYLjz^~<0<,ba{1Ybm5mјv)l1VcŶw<јDcDcW0ŊvǴƈp_0?N[½(,z[S bq4UEazq#S➋(]b*J_/hFŏs4߃%$`iwq\}oߧ۩Ǘ~S/#L=߿S]ԣU:_vE³ae\6ūEw^~;{̞,9X=Y\DQL HC=cp?QXY6ʢw6`= p:xf۰inj:_ptYFPϚI Ị,%n$~L_1"Ⱦ}Nm̲uA)QUt0A&01jP P$I .3z!in Qs 7eo7/uvZum`!%mֱVRC=pPOUY{*d{'M)$٬ף{i1fC ng[l6₣]rɱYǠq YEfo :o 3+nՑ}e)stU^ptsP6^Gk 'fz u|cPt\8x\Ե5*AMyeLu(LC>9:Kͷ1g 77NsXykUq%ߢlSۃ<Գ96PpfBhl `74pO^.R4OGP0L:EVn|5+8B$Cw(^ QR}1GFڡNf C6߰No239lbmi5(158+'r 3߹崬pPA=z:Oڸ:SmN< ûyx$`6Vڬ?| h%W%N͕V8m`=6PGAߵo v:r=yUR̮T+ Z7X7v:l 5eWo=ޠZJӀ17ȀYFq;ti5:ůa;wpn;ᗬb $(C=u_<|4qx%|˴V*5h԰a $`y>L1hbB^:|`^Z\Xޤ4Z.?kruh?Ao7vj&h&h&h&h&h&h&h&h&h&h&h&h&h&L}L!}=IENDB`PK!ٮff"gaphor/ui/pixmaps/gaphor-48x48.pngPNG  IHDR00%bKGDC pHYsHHFk> vpAg00WIDATxwt?{fB( T "E J@b*A""tA tJ^!!?gf#彬{^3g߽{}άgO?'~7M^?q̣?<9} ?@C۟5@oUQlW-[ D MS\k&2$Y3_`Q-+uѶvNaxv97f2Vh lś4e〬!J/}Z͵U馿7RÒ+ B+'&~n$_:BEngm ==Ke*ӋuJsxt1~hL\'xN"٧ZK[ds3j,lruoqŠOQAԹE<ġe{T3TiPPW᳅z$EÒz{_i{E,h~@s.ut#~!ї^.yYK&JmVAR[0h-E/ +3O!xH.KO g}Cd [yH{8p3p+4P8ᬖV#Yf~"KCש?bO}!Ccc,XH[zA՚Yrg経3)I<'ȰIпȿ-+h^ʶ)rOFc,걍,s7 t'1t2\.li=.{BZ덅-q*.yKgkF˹]u[illX71 2|G{_vt鰊Xҋ8$qӔ4BNz/&Ul8EM}{S&iRbU?R~o_ -;/ K4OMKyuX*/o<lNV[dAr'/ b曰D,^vԺ/f0 GcmSJ^\jֽs&;bw q<4r+un+Qߖ2qB.+-ac/|§/݃i]oN.pCm+F3g% Ec"(4tzto*/[7ֵQ]s38nU=oO:fu Mel {cս 3,0>~Dr `jXu JthD({GKxS򎼆F#z/+=GwU9ʛujgk ^JUk+CCW' Gך ]qY8Zqg` j{+_l9.U. m}J(<XBml\akx_w%?^roګפA8JZRw! ts0{TF+iSVh3Ժd*;&5z;܇}NғpZrJ辥 YBpTҝ*x`@-]`~5zK#W;nK= rպ sRls@54iLdž*$@nIK`3GZTBqykX6[s͝H˖]cR#c+=|6a񗼦T {<-|>F6xߖK[qUrDr@0uC'ź^m"Ǟپmdr#ahl K>|7gb~I:tmL#=ާʝ ~z;Kѐ}WAt;{q([fcW4fu'D;MÇBjnlX_ KC沱$m(wn?D4Gt9u;[yCܳeQ4Y?Heԋ+|!X_55E˦hX2=*#C)|6,~K.+M24cNv/WB^o_a?[e-3y Hjqg@6rE 7A*; rA "$Xa A  ޅ0rs@v&OUL0jpѠr NN `L!U"< *NhI\K65geݻAg_.H=owQvThX~ĩA|t!giгJcOG2AzdZBrO5til9jrUB U(QRlRNH 2Cj|l&W  䀎"Π 1]d=gA%x0SE|#`S%A*^ȓ`HyxNE`vzrrL|q쐡X~R& \{?\݇.:a[6@/\2 |,ȵQA9I 5@;U/L@9TURU$d; (n[T[[z ̋C{`< rA79y}zN`L#d%%HN7@dJI S^σ1S< fDK(JaގIe+g}߂J% $;w@*d>H/gArUuڂ(3RIT 䶪"@*; ¨RM eTkHT|RU$d:.'Ů:Rof:w{PRtj./y` 1F)3ƂQ!?pP.YY/ =pnM^өSb0exQ d5ed11۴~)(g@T@Q\1 !:f$ ~e.((Ϋgn.f uK1UH0K;=X=Dz1E'XA6IS`8Q:1E>W@%,x '?c`8^Tv2 T i Vk-$تr d(o:lVQ &* 3Jf*T<6I Ud3HӪTU8*JTMit^*{/,} s߇~EѢUp+;r>0/oW2HN'Pk N0^t9.A9%]G$Q`8u<5*S*Io5Pq#ut# _ 9&3.i%@6y.i3RTXJ.njdi5P@ʫf$7AU(AV yD.^U Xk2Vk&ʩsN5 Nbup5W H/W|}Uptj /iT XZc{6Z 3>#Ez F8Ix sz* ^`[ŠeAc~`b)j+}@v慠,*2V}(on6p\ 7U"2pX9|Dwl=Zzaփ)g[+c%l~| JQB;7Rn6PWMBy<~`=r6HOYM>zѐXz &90St8p0u M@4.JL4# 'ee <2 d֗߃TVA)2rG  T [@HTS| S2C#t*NFYWMiUG`M14z0tgQ2|1B#yPu#ӲQDWqKhʑX0IAH"@%H f,'`8clqF$`)0Rq xU\o3JA*LNgU *ZSU o2 2ŵ\]'MӮ fUi~Y'F'wx7BlH%j7-7C}uf+ 2G݂ x5XH;]/'G^#8Y,xpbi )$dH4B 1LlYX=({Ani*q 䌊b H,@Gv.r SK!γssipLO`+q7WaK=r r~t^+N< R$y+ \+9V\9p<'ɥƣxνQDQ@bs;k37%j3g Pw 'CNk);% ..FhB3ـ/xu#&zfarYP# |i%`ᄃdr6_ivigD@> X_wY~2sumߎc*H =t Yf%m 骟ST6)xyy9`p]z;/d{`>i-[ ȓV_(Fn#诿^Xw_+%jWˣb{&dzq3v8VlWWj?ꔯc6ٝp#CG~o  SJI%6|m@C..b{1n }$E~` @#DjAjv5 8~FmJ}3;'ӽ@Zׂ '\|`P>kO?\h7IENDB`PK!%")(T(T"gaphor/ui/pixmaps/gaphor-96x96.pngPNG  IHDR``{bKGDC pHYsHHFk> vpAg``xSIDATxwtU{w{GEEņ+PD!MA:{ɁFI@ڞ{?N}o|32ZkϹZ-'|'|'|'|'|'|'|GY&4x> =9){[<Ɔ!%v4>+lJÇK6{AA $ѾBNGDBHSsaMM>ME hay w/` 6 X5O}2ļfJ:e9mtަR?y?7'G;Y] Y!rWR]:za_n:o*\>yc|fsEp:.O@tg\ws}ZVS=`(B6`LEFqNrimBCQ~SrvMe˓? ǔFfp5GhG_σxz-7,4. v]8sS,A?R͗S<ظ? 䒋!ߟO`\&y5gM6oH>gLg5яEa >1O|n&GO7*X$p0wrBB(x6Z~"jI(JyZ(>\`aHϻ?M Bqp64O6B:%LGG Y Z9/JaS#(FI&Qbf/>K{,CY[R0іϽ2enias|j]R] 9g ؋fnKsh?|v1s8AIw`MC &[Js:WgpM٤`c`(@d8Y33|Jփsf)I4}Gn9u:Aݲ\2G70S*>q ֬uݹw~)cƃ1s9D\(8 3p ~u!i2[*CW 20v]RfgzdLau/XR0:c3Z'W$M,}]la~{ V?Zcޗ)rdF#_#XZޘ<~7 \L_=D`Z!`My*1& i XU-ǬLdڴV'*ĿY?tnYnj:gqij\1jJٌoT|L61 ]*t@EY9qfܑ7CR3|nNR p&3Z$J0u^zS3vw&ָjs+s~np/aj'2vq4k:}# }LXi fEs|^M.A$p%,Qca'Bq# (R؝&oGu cp#kS<;9pϤumhk/*G1uR$Z+C9rڄ\l>b0˜Va^c`7 "w*fug=]<)ͤ(| I.cgb%wawIx;3]LXW+G&;}oww`i9m.SjHb(a\f`^/?ɻls] LrL(}~ܵri esqs`b7+3i͛Ǐ?nx2u u%77\^/w0^%C,An ҿj?{rH6#K<9%_LJth ]+֒I3Ge>W 93***u U+ij0%)#K72"k| f|˯'P!#!f@Nvp 0Mxn*KʗT8 L|ΚteR܄X/1XV1'g|[Nڧ:]ԧ:r*}rzg lʗX%Y҃5O.%#y|8xX}h:NEy3۴U=]3y̔TX9:vw??{)kNz1W+ӀwP9 q`[7DinF'KǤ5-k6[30#|?ܿ $ӕ3)R=`mg ǝ1@eO!SK 9Uw.'{4\~ gW*3 &!Ku’ᚍXfo0Ȼjo>D ސ2|7`ႦP7շJp<ã;e|[QƒlO:z ViUAyqܲSJX`ּYځ7|3a<)̨57{۵ j0B?ސ?<DP87 kjr.tƿ7b5gTyWI%YUL2S0on@a{ͫR@p&ISTPWJx;oU9 <5 3k+ƛv) *ZOckhjf{M~"!'µWr 4u.e6K1*! !yeNg_\1#oC\#-hLQv{Iݴ uoA\.s r<E??Vo6QN7@ &7&^pwJX+ʀ J5Wq$nfzMڒ(2qSO˾W* ̞գ2Ӱ&@3;I5*=JQa1󳾬Iiel\>v* YUklr]M81bD?0/)wmqhsNfQSUHR)bX|l~Mt9!B;է|`ǜ?#˧ 5s[vX.ȣ'12V2Uw-d.>-kY1\f!iO爼`>܍! |ƚP 682BhDzK5-M\٩[T*>Y8Ps9J/} q\i~_p܌ cxeJ.򻣱7|2 `WǢɹdH0:SO'~Dl94tBq=Xzu{e%,P@,*Ȏ>N8 q2)Ámxr*C!pbB3oyM LUeY[& v; F6d<ڤξ~_N%ύ9O#/` "Ad??q~΋%4&,3KˏA7ze }?(>2_}.ᗳwޯ65du$dqÊrD¿E`]C̮DWN8MAFTm dr1 (S_Ta>~{^ 2`O{5|X~fmu ]p.1BqX 1Jq"Xfe:D66h ?h&nHMd 砹DuՔh{Je]y?9q#܁:=ٶҘW|mچߨO~qC`|{y:=C> b*㋃OmS3;URzF<|p\`h=ެpe|\> Z/<|dgCdzmڃԲl$n)Zt*k{j5tQvI_kIm0`='`;U%8rMR d.='S[YlxY`F V&K%9}-3b)J"ANGLQ*A1Y_¡!oϰd]Oս{;t,Hh{; drk܍aN:<)7˺-pnQxhZ6|RENVC p}`Ny 6UJƁkXumBC \ +nJ4e@GM \nl >uq` \ܜpm@sNB3w3 eBY C{JGM2N9@ m33e(X 7:*|6s( VS P#o1BghqU>s~#ZM{qkW¶F>yoSk:e|\5t\QF:E}3yKc "N0Q=RD~72Jg,{zI \`mQdwTZ(76y=̀h \t]6`!֔W-@`)lHW}Q~Q7@ZIbNi!8$$'A `|}lɾU>f;Wjhi$&:3h3,beaq24Zj cFs4Mh7؟uR hCuaDU3ms$ÎMY*;)w]ꍸnq~ )Kـ>F8.B.7ayar+dH0_*b]G];_h_U,mOuCTw0#r#Ԩ"A|h}&(Ah},04Pԏ Z>pT k$;zKOuV40LO`6j $37BS&:Q{L'(m) ~ v4`XIJuX# 4'4FZq 40sdh2C1R7h4m;ѼJ1RhZ}bXLi?J↲0Yd~<۬bY_*,ds@1[8َ(T2g7~Z+mĸ^-y(kT'4 C=xۀ p1s ®F2Hwb&K 0֠7D 2r(e40Xj{ hYR $M}d =Qp۩: 8ԜFaCTs, Πd"8rsY .?JхunF->{ݓd;#B *{z"'A u9z? 'w Y\2g-Ϥ(p4˷ئ9Gܭ\.W{6giF= #P5vu Af-YB l`ڃ%pU[P2X!Udj-dJmC @s NC-d3^T@sA$pV`o"A~@ Hg&!AL28np`y=t5C@7)f:)$ 4) *DYJ* 9f"&. j^Opmf5Z~o]U-9Lw }?rj^cWӜ7@p+t0lRzA Isv=Edn;(1&g`1)u2JzU ̆܄=i7@B-rWDfXÀ#zk'̀B@/*!MD`(C^= 5TpOJ;7l@i=yxV}R^#WG'iD9݁>7Z85d91%+d b0h,A+cd&) +:fP4$rAce<@oӁڠ1Ah$ةf+A5߂u>41K4Qz8vrs|֯JU,hR VNS6mA;$㭷t;Ԍϝ^E/^0.R?Z<ļa!]*HcŦ cJ.mtt?nꁘNyebݙLTy1;@mt<\ Z(!| @a@6;`7|5-(^ݲxQ# ch%V*0J#zTN<ޏmxHLV<˼N<=+Q~0O}tNqL5&@Se @c,OFS ͐8`0aC'^M [)hԣh6Z24 ,s 8cƚ{΂OA=Vg[G<_8Э^O}{:d0^u/+f7?!T$}(*ؐ6{sKחDs?% :rzDOe <9fŞxW Dmʁ,=y @ !d2x^^8֚tfk>ꪛ 4R6`{p>Ձ>dIKDa}Sw3jWtb||ӮՀ>U]d`Kҷ0d e r3 tq k H iPͻ$!Zdx+fk AFs5v@1psnS5V_.oQԪ tD`zdI:$16eB$VYGYA4'p.HL˗(hlVcIqAcd K@L"N6 +FS _L}&0ɠ)@oS 4l!47AScL;+Fj ]ahY46@oS{`p YMgZĞҍ+"l*xf-ɨI_F$:#?o-j))dey2¯wRSWlظg.qkn~#@Ab'//תH>d& ˉa 0RS610A}O0 !HO wՏ ]zyZ u4PktZi y0uS$GP5D,h tnYn{UxLMg'rYQ3@y@T^r7nFXl*Hk"A^MAiz$KFP]2 ~% 4NF4Lޠq5/ސ֔CqDQ ^ig^M5OP4rMcJGƁ&R0AY o& V"hV292d/ (F+=4#6M|~ߞTs6IŪVw$6.&bޝ^;cUx0z덿dTYFe jXR'/!kݵL>tQfjm^j8=]'dxS=t5@!=Hsg`R"pD5cF;Տ 4p4U7?5@8L푮@d*.!X P!3[zDUMe.`'%j Hvr1%/hf6) ')l…0`'Hjcc(X{-tcY]$R 4M4/XhS*p4V:4& M0-I2+ t#*0]gVPG/w OV%n@`j 1=ee308$N9 iP ĨyG+` T7hGZƬF&O0ВɶGsK_@O-H- A}(`Ц &iOUQi Gy ^#e!`j Y V['--\ pfq4T^\"405dX13- l24h M8σ,-ASd5@MwJM5|tk*C@SnV&XAch=d3t:042`GS83[:@Cz& 4Nƛє hE{̠hj ;.94ҖBPy0- 7'\ҏ`ZL'ipjdz`2Y &<lmyV( 0\Y@biY6&d8xM` Ho % S!-)K.}Qj425L{~S pk_f5PZ ĭ~Lh<AT 70p $7Oz V\aYe/:9 WEBeSA3 EA@+ʂ&^XYn+5(XGxD+2yF @DЌdTi;+`ݔ&ct*h/ B@K@S"*Њ'҃`6 mS c2à1t,@4Dm Rg`" v4(,b퀮j8MA<N{`֚,j(@fiI@>ڨpjjiת3 i,=tjj}Qy$K#|ԇ3wzX#  i0M }A>=juY @ e4wE"pI=0pPm7Sn)`_kN"%8nz`s XN4VME4f1f%M/Vey h<̗M:M5HKChX5ڋita1hc= { ~`{rHk u5% ȴd.T2 إn<=2T3Q7!@} > 0T#x^y$MCk7=IۯRʥxV<`lghg9\,h ccnȓ$uHVs4, h|JqhM1\4|AvLc6X)TXŜ3R,8*AS z;t+@XҎ9`3S4NdHh8 `D H[o;L [&IFMI2`0Df_hL dIQ  k8+@; `!^;svr 觾ޣ4#u%Z -E_:#=[xh$cMPDC\Q_R붇 /if4i, W8h`0I<4=.@~e㟵xS;.AE,Xܡ"X( 4VސQ1\4 ALiB@Sh,-X gZ8>b#Xy * 7ӌь1^A7k@o4Pm 2ɕ@!݈Ҝ 2K}X z ~?O/Ai?lZu e՗1 4mH2h^OnF2~ lU7}-@y 7 !U?4F'>p̏(sƪ/o4oќJ;MWeCVYx<[8$qfk h'-Mл]rAoPp2h|EIS0+kOdFˣ55h"A)@%YMӉl&J~fXI`fw CSܠ RW@L|Ao RҠ|uSS+2ߘbFS;q ɻh ` f:q4T H ˼ j[902T XE( >Th ?V H2@d,0wi UI3`F譁2d3m{8RFP S LF-SAkQܰ=2 H T?*4RV͘< 8 t~+5 ^OSL= MHwCMq6svMfEA)oR`.?q8i#/(Rf=k@Ƞh s(H/xߴ\AInK)XFF4Rf'h'4V\3C(h +;`1 -*9XJ~uf&71ht5߁&PҠf4WAcAc$l Z[V'-iGd~d6" P2Gt ^FWyњ<0,i }AJ i@* i0) t`C"nk U0 8.ʫ {Տ@1k&`˻nmz%K 0k o4% UJ˭P lw `s5TЄ6f$80ǠM3) v|E Rpf`3_K8}AoRO(DNVcMu;tGv/&rn5Ld9h4#4LCA㩍/h2H;Yz3YJKxf7}Ӄ)9h4ô<ƄP?`PyLOj)`L"a p]0稺W;qdJOv/˵&@Y-Qu ,t҆2.ꭳny=־BS6 \d- c A3j05,`dkAs x i Z@N#|VAB6]p!Ha`?>]/H΁&>h;(f=1iuдbh }Y ([J,>7M]:Ҽ,3dhbLme`; ҍx`.{͐TM${1ҋޠiftMv h&pf8E̴O`P.o6ce:If+FO"mz$ &코bȠ!+y X# gAf}N96PN @>KA P=zi0u16՗& M+ 6ipz 7UdE=9h0?4dF0pQDhU? ZEIIzxAH(W,?'d,hoBܜ`L̫IsϦM!IM9R 4fh4G7S 624$Olk4Dy^ gsy-$YG7LoJe!K `1IMaׯXTMoA mMO~+k7dAiCh .H^`!riws@`%O~?c{vHOm(]A./ 5`a?|`.^壀~ Rc( MP\CLR?|g{rYD`T) Lᝑ.Ih=*Dk(]KOM1oG)֦0X4tzgA .Gkb,np88 ~z&.^4Zvnx3/oЗ`'e3T5h ! G @M0@c4LM0eN/VJevJM<7K% @ay4F*oYL$hux8ƙa`E3_vQà7A8Th3R&(h (b4 p.86#Dv6bI:L< T0LV@WAHw 5a`F 9An+óxÛ$T#e:D/hKAs $xZ=rXC%H[ԏz@_ guXIwX:A-gC(p/OW/5U} Y %|zä.8 `ESdvKO&KQTDKS 4M&3hyZ`t+֔U`"/), 4ѼFA84ƃK+@Hba%`E <g>ʠ)ăўf:[AcE;R9[:mI+Bd 0 $ NNY>i9`1PN H%Ɨ&R#8w-= KC3X6-!HouKG GH?U75\ Nݜy$ϼTk(rI}i0sZg pJ Տ3` j<, 2߃U> VEVP.XP!+Խh>aρ*Vro^悕]g=Xftk/-h"CM0dh @X4ŒMyF5bHRL!D&IT O, h{d!p_qdFx]i{ U7Aϼv})!\T7W(A#-@j=F{gy ^#E~<)ìV_5@h kMlS?򌆛AZ@VUGO'!S""ҿG |~ %`l׊3GggAW,y1T"1yʀ&ҙ)cy`Ř'dX;N h*4ְ4&4o30 #k`G޸~( ^M1}̕UHMd6 "mHM@i v7O J22ˠghy]ޠZDa*PW& qFFMtځ85Η@}g 0QC!Z @ `(M2N!PY8ꖡxLe$FMdԍni$}~{VgVPYPxTth0]@Qj/ 4f 5` j(/8٭[5œr do=\O57 7U!DYAMCNLp-g`F[@י^,-KW2Tڃ^+{6(X1,`%eu&#, -s1 #4Ӭ$4.c V/J4^^&5 %@mzfܠf `R`%I7u,EAE)Bӈ tb(h* zKQ⠫U$G|) T/e<-Bx&S IJiLB4$z i0]AFCy WM'g% MC[Cd&I@ fLq(8/5*@2 ̣k 5aڣxp9v)p}w,'+CSjg>y4q&~гaK~˺$ TNu}9gt=%SuC@㉥2h-ASiL;Z>[RLc+G -3 )f 5@SYDI7h3XnИTK*t爔~dn? XPs j %5@o;@6gp[ )nF4D Bǵ cՇ (n0o9^:܌dsn7X0'ӿk٭̃1n~ BL ' WxfA-F24Y^1:2g-`0ujNchth=$FxHҔ1< 3N*SL4X16XetbIyt/@ny$]+XT̋i,݄^#t=2dc0ZݤHn]lKHI+e$P@)[h<VlՕf>.koO'[{+Ĝl3L8.h= @3"r" |ʀ^! 7LƃuÌXI2Bq'4f&m5tlyoy~Fx [=Li!R@L*?n(ʀQd; lz;>iZ _ 7QSps p/?V>- ՏX93L2kå!~-₊irLf5,u wwq (`o' 89$ނ~I|gq$,Ht3z:h ~ldc`NU0ΖJNZ4+gƒ`M"A?@f̆?ނR?|ҵqWz@Kk.yI"!g.x.@36zY,}j:Ϡ8M 8K.i9 l0 WN8?uj͟ 懓%E=!a 6 vȽT9A16 PiC:ճ>{9 ss#?Zm4?2PE sdNЈA>ÇK 띪 b|S±:$誜>O(:6pAS+P`%f>XQOWm`.%j`03i>p~Pg+MPK'Pk2 c6yX[` ?sПo)f۰0{t_ <Ң_Yۮ/\jg3MWu 9(HL< K~I` #CCckLlPl$w8M f|䁂E\a*|'|'|'|'|'|'|h7IENDB`PK!Uܿ$gaphor/ui/pixmaps/generalization.pngPNG  IHDRw=bKGD uj! pHYs  ~tIME).tEXtCommentCreated with The GIMPd%n#IDATxMkSAs)77Xbm\B"`rتA{Hu!FP]Ki+M{qEaha=;gΙ,J9n?Ę%d2vUUrhC7x<`0(dY^l+tz8Os`OPȢ(Ñc~U1=}[Ȳp\1X,vP(ܿyk|x!>[m610>hn7X2p\&&'J^UU<{!^(ʓ^w$ IUGNԝ8z66M_LKt]_+J_6-cl̕PB-sce]h\9{~ ]׷uI23xpٙGRfV-nX,;֪X,"x5s]5m8^K@ _贓s ΆJ@Ӽr2͵=vk.$fH$2Tۍ'םu*bmlS}9IENDB`PK!$gaphor/ui/pixmaps/implementation.pngPNG  IHDRw=bKGD pHYs  ~tIME3:ڃtEXtCommentCreated with The GIMPd%n$IDATxMHA*쇛0 E/!B'[ROQxn㦡f.%A!FH nd%X5agv`Mǖ]]]N |>LԑHk{{twwCRJ'n趶6 = a)~Ng 纷O777FJJ Rʁ' !pbnTP9L& 860宏WtS)OB:FGH$4LӬsg0 m^mey H)%^|3!,RRt.ٶ}ͪ@8V),f&|>R1^ RlNfsz}}C@z @ ܺ9TJ544|QJ @PZkFFX[8bN6,k5kzB Rccp8p5(`]kpccz-Gw. :rаIENDB`PK!FFJgaphor/ui/pixmaps/include.pngPNG  IHDRw=bKGD"{ pHYs  d_tIME L IDATxMle;ݝnm7vcFm-ՀmhB LL<5$7b"<衑!H@~3;1;;㡻 ?y3yyy2"PƎ9ACSG"yr> 1B[ZZBG='Oܻ?h* B`tG3_GBPJ^|>m \5JGDi-W.T*LӿXY]( NsA+S4әz>(Nlf4M.yZC?j9;Y 6Y-; y?Uqy<^Уmj`vv*OJv<O_2JF4Y$iIQkmP{\.\mͩ.Z~gwmm-^F{7'ӧnwbU^]TX^2`~GWWۂX<`~ֺP֝EKV3}Wr䭑!֮fOV@urC͋M_@݆I0Q1d}42ytZS߬M~s7ۣ*pRNNd27Ew_oG7.K,k\YH8P,;M enkV@anܲ]W,WZ,yޥ:QwG B67ċ{EIENDB`PK!L͕"gaphor/ui/pixmaps/initial-node.pngPNG  IHDRw=bKGD pHYs  tIME 9sI]tEXtCommentCreated with The GIMPd%nIDATx=JQ_tDPbt%"m6Ԗv)6A$lsswF`?/[,k\`1p2i9M:k#|\`TG;Lx]aҊߠ&mP$}F+k\՝AXUЮ}jh86>}Ӻ_Š8~TQ<V7e n{~4IENDB`PK!L͕*gaphor/ui/pixmaps/initial-pseudo-state.pngPNG  IHDRw=bKGD pHYs  tIME 9sI]tEXtCommentCreated with The GIMPd%nIDATx=JQ_tDPbt%"m6Ԗv)6A$lsswF`?/[,k\`1p2i9M:k#|\`TG;Lx]aҊߠ&mP$}F+k\՝AXUЮ}jh86>}Ӻ_Š8~TQ<V7e n{~4IENDB`PK!""!gaphor/ui/pixmaps/interaction.pngPNG  IHDRw=bKGDC pHYs  ~tIME *(IDATx햱JAE +# )$"i?* D;KY!d ;c I4Y-6c` (|E2w7@")JVV&&>sRʉ֤i9kryrYjuk*@^# }O(=w7UyN>01Gq}bA=c;4Y(> ~̃uppQYX5zZ/ tObK$߲xDHpL<IENDB`PK!p0!gaphor/ui/pixmaps/interaction.xcfgimp xcf file00BB / gimp-commentCreated with The GIMP 000 New Layer#3     J00^00namamama    n00 New Layer     0000   / 00 New Layer#2     0000a++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++aa++++++++++++++++++++++++++++++++++++++++++++a00 Background     k0000   PK!`gaphor/ui/pixmaps/interface.pngPNG  IHDRw=bKGDC pHYs 7˭tIME !SɄgIDATxOa__Chl9VF祁}G]t5K Jꐭb0\h*;AJA{{|~=<@EzNs;ȹ<::ϚY%YNSԤ:::#H_NY׺ŋlbN/&،f }{z"aV/&w90uuun?z'&&n{$˱-24x "l6 |6`,[Յ(G2 pzT`2%N Tϭe!X, BPUI# J_Ƶn; rT a4s$m6p*+w'Ӎ^00l&gzbZ@Q$<7q\D" ٺ5 W@eMooo_ NXV676lNk4h4OAec677XXX8(T]]V&IX/$@SU*%@Xt~H NXe@w)zIENDB`PK!~[gaphor/ui/pixmaps/lifeline.pngPNG  IHDRw=bKGD uj! pHYs  ~tIME  5tZIDATxJA3DŅhȈ|A/t+dpEN*GS£n@9hx@)@јl2~ e=Q@k,TitM ;+=EP m3&6ݭ`vS= Yt}S_¼DK0VƍDVS0K혣sUzP_=ǸqWWLM :ɸD.M DyCA\`+Ѩ  Dn?>z`++}LQn_6o"WQ!u6#GqP!Z͠IENDB`PK!RBgaphor/ui/pixmaps/lifeline.xcfgimp xcf file00BB / gimp-commentCreated with The GIMP00 New Layer#4     F00Z00j   ----------------------00 New Layer#3     0000iiii00 New Layer#2     h00|00   HPK!⒁UUgaphor/ui/pixmaps/line.pngPNG  IHDRw=bKGD pHYs  tIMEotEXtCommentCreated with The GIMPd%nIDATx1Aa^p( H:0ӺPs$Uf2#T.E>l -.Xb)Pa3hЉ If&Ϣ]XfaqX.A"2ߕpĴ)ts*QhΧQ6#Ϗ58xe)""\9+ "f%?IENDB`PK!C((gaphor/ui/pixmaps/logo.pngPNG  IHDR]d%fbKGD pHYs  tIME *JWGtEXtCommentCreated with The GIMPd%nIDATxPSOEH-m) ]-V{0k](wMֻ+^əu:7w1\?f,A:T; g9Gs499 |fi!'y}yaHx&Ϟi>#W<= 0:m{bjϼeCLĥ^7߸5[L0*.%6Ssހ%/W7GVM';w3_`P嫵Օ=0B.z鿥RzUvFv+>BŖgMr6sF aD"ٳX_EZAt93mԖmaAa~ qKtZ^ig[AmZOmK7ݱᜧBkvòM޾{FڿË:ab%+L@tEny5\.gڬ7@v;@֓gIPdm9b% 0װWky0bm*w|mA  hgT򵥆dk$gδjY p`s850b %­0.Bpv^|8װkʖc?1}Fѕ n災Lfr7 qԴut5,ܱfrxэ H9,xcZg q/-u.RZOʷc[ nEI'V-+ʱ]kPkWB;F?9v @\n=BEo,}^<0ѵ9R‚|wֲ-^ITE-0`p*c.țcu 0-G0DH|K굛S^:6+;w#!_p>fрc$KL@P(0 @ĬOpj7),w?_ xh㕞˚6#ZO 039dɝ=9~?7a8%IBuq}f9k9m%-|| ⥯֓gYV?*m1@}S XuR. &&Zzl;8EG#nGY;V仃-GOhh6^^A.q) F0 :aB^SWg]^BI>gڬ63 K:K[D 0 {AӮzN `-țcYb&1zI66,ʟOURgĭԤ(Cɼ\E)x{h4 Ӽa*&c;Ɂ.ъ[hPvmamHpb[6tbR`88e D^s>jvfHN+,w㈹2+s?ۮK_\D@nղ-5ؤ?떒:^X]iS1%EVli]'u4mW6VXg 5O 9cϘ FCd]d)_[j bYtYd|, n?|}Q@J:i{?vx)"ݵm7~0hn>ܩ'e몼rAܩT+pKH,J{EI5bFHb é)[W]k9o xmBw_LOw|?0ݹ{,C##Sƥ< {QYѳP\<ESRb9MueEϪe%H*J= bdKݼeCиtZp3mV)F貍Mz!/,wKY!Hw 劄Yjڙ&L,)dAK$+1JZ)bK_ɖ[!Y9]8ϸPd!R~1y3<([{V!=rڅfZNQDѵ9Sy\B (7U:.fb\A.(Ij{?0 -uϸ1LrΞJ|*b]EA0[gvULj%.gFJ.喻Yh!$#=b?eua~."~mBN#5u]9Y9)+7xùw倂5ZqPԮ*(S&|E<_S=zo3?R[SܴlI hQڍV/sKo* Cq 0ӳT݂bE8-HWWWV`oIct?)Z9eC..疲 b]]YѳlIH@7[\EFȗuwܽό R /'/I l3Wu#vXt9znlsUlm_Ľvo=_w`dttrybjjόl<^v$S \wv~^`uA`@ J}nk@uװBx+F7e Τ\h_Bs>sŦ}ZN._,S~x~ Ca>3pcg8Npci&ZIsYzT` >RA8f洙>H1 pjjwi8|wL\B𧧥=yA5uE}hↇGT|hP0Is ' 2P< ?o)gfU{&'JzzZD|"OrB^?/Yl~㵅593Կ?~\ƍOy(.)j)2ht<3p5\@'PeH0בD'gmHsIn5u>Vٞ9ߋpBsgJ4M4D`)tO\޹{%ԭ䄎 x&ЪYXlP׉ 3#8KpIA嫵=^#?q@:X$ZQ]qyC"^.*oMT)MKo[%y:=>ʊ^)?Ow+eUte$ ] Vy!9OI1oADX$n災8 NqjuG i꺟X6(g28`#k"rmj{$0 8ODwJwMu$hΌY$}\epDl+Y>SXpVn/oQYXJ!w'xԎ/=z֎l&Wd"[H8t$ D$}~\E(B HY>rV:4B)Z\}qӷXѭ֤\{?\kҙhǔj+IP%bF"zcc]ZR RϢg%­=1޼eCPf:)8TMSX?S G!Ha\O݋*pFT@v:$1:T5X^lpȝ t7q as8520֐|Pyr,ָqǵ+pgq r>[LBJ֙&Mױ^ԩPHǫqQ{9'؞\a1o~ TK0Q~"hƉy^#GhM)SUV$Qg,M?m73wI_:|Z($_.C-A-#[sDQ=P[dr RKt9Of+n}')S.g4UbKW]|xBvJ qkimlҗX*L:MvǞ`Vo7}qRc )A@q"ȗPd8]'3uI36Pd;{+焰9wz4wY*_p+l]ĤlMry%+Lnw3yVJ)os85 .@Xp~?ܽ,_ oT[j(JYl؝-7ؤo>k2q_Yf"M 6ωml3V:+,w/ʟo+(f197qWPx Y=#ᑑi##4 ;RdũTLrDIg^u믲4NkXHznlj /]p>k~]4Qy&MJ환zloj})Vy:O,R{h]f#;]p+v ^0򵥋[6|~+M& ލލw|.yДi)jgr񓧳ߺcRŊk$; *%4_63>zDCcH&}V镅ysz0jJz#CFrg_rĮ#|+f޲!DaGFG'{{]T=|x:q R5,bRRpRpQnZזQxS@ueE򵥆8RWk[LrM:͉<<ۻvkݣO~D5dh x Dgœ@t @t@t] vlMV W#E;-I}Nl]y'$"ȂzXGĢW-7oxMfc=ZaYav8D<131$RZj=`/,N3f*q8yx]>1gfn{4@8&@SQʴIENDB`PK!ee gaphor/ui/pixmaps/meta-class.pngPNG  IHDRw=bKGD pHYs  tIME  tEXtCommentCreated with The GIMPd%nIDATx= 17 &B <&@/'X(bب,VŸi0D)$'uB]`z'+Q`:s>Dq"]oZ33z1v'Or|5}͌,nesæM5sn+8]eĀwrPIENDB`PK!&qgaphor/ui/pixmaps/node.pngPNG  IHDR88;bKGD uj! pHYs  ~tIME  CmEIDATxO\?X56LP+,yCQ&$۩-WljJq|K[\ s'XmӇ9K 1kZ|9s~왁\Z/6/4L[AMV 4kj˜_fsZ2ܯRHӯSfzfOǵ l3u(vUْ5-Uo zj"[gI" C'Kn{{e--wJ9T+/+Mj4T;{#G tHmM5]׀~(8}8M߳'?G<⅋GZSccPvs9˶m'`p`Ϩ&::ǣ`fZ|O&t(|]~yp@\.w4s<85X9iD`{-z p)ȱ̹%/8qqq*?,.6ViEN.^x DHsgϝ'рY2rFue%>$::QPX}kE1K@yYi")cĤ$Np8''pNfjy!6 {v6' xu^,5cpE0//+z`/@BBNcy-hnjÁfgIdd&R;rǎ,$v­Ήg6jUIgp)))>CnoJ !*:;),*bkX kԐ˞6tۙHɩ;NttfvFeM?l6 KC4 hB۾};%V@immzΞMaѻ9;wr7g;~"`>06w;X-sY9|i;4ŞM~AϽK~0x53#G|j)uϦ~) Ws3SO&55u烁MoE -P`k 0Ե VMMM^HNOOsOYkFUB011AcCW.5C pb|*۫=ȝ%lԆ 4u\*|2A(-bhHNMMp6ׯ^1M#Q` 7:VM5*ۃn&;A%!ccpUU,p_4 |87u}i00!|ߙx'xlX^ziW_oo?n*,xF8/dBX֞sWrnĪAeA˥n=}d'lOoo0pO4wT;D*=5ZVA3QHg~ޚ c Ս/ F"##ϠGJ{ˬQҁ\ $sC$kD $e=$e?$lB%hA&9B'1˙>( 3K)00 New Layer#4      0000/ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #  # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #  # # # # # # # # # # # # # # # # # # # # # # # # # # # # " """!#$%"&"#$ " ""00 New Layer#3      0000 c))))))))))))bc))))))))))))bc))))))))))))bc "!"  ! !#$%b00 New Layer#6      00 00 (2p2p2p2p00 New Layer#2      00 00 oooo00 New Layer     j00~00    $#  %"#$% %'()*+,-----------------,+*)(& &%$#%! ! #$ PK!-(l!gaphor/ui/pixmaps/object-node.pngPNG  IHDRw=sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<IDATHA A Ma#zZdwuCH&HCOJHF(j[I"=$e=}$_|^C+Z+$Vw$e S/J89DICGC( Hi|+EPV3mTZ3'FCoAe0 @w)ϻtaP/=l(IENDB`PK!:g>Ygaphor/ui/pixmaps/partition.pngPNG  IHDR``w8sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<hIDATxI@Q#V!%؁X؁%mWA|ﶰ &u;b  &@feYl>c,[71bĦ׀4n^k  &@L1b?Ynj8G31b_֞_^7 &@L1b  &@L1b  &@L1b  &@L1b  &@L1b  &@L1b~eb}ٟ_`b  &@L2Lyϒ爝1b ۘ1beb}/X;=IENDB`PK!T}SSgaphor/ui/pixmaps/pointer.pngPNG  IHDR}\bKGD pHYs  ~tIME:ఖIDATx픻 0C$#$ =4t,0 ,4dV!)+8/ˑ(|gy)- Q%Xz &`Hbx4A^ ].ĉZxLkCH6H4\,o ,jO ^.p(r- 4MC D/)rRn'm"C̶Pl=  uIENDB`PK!jXgaphor/ui/pixmaps/pointer.xcfgimp xcf fileBBg+Color      -g   7g   7g   7g 7  Drop-Shadow       """?)?*?*?*?*?*?*?*#*Ա#*i1#*i#"?*F""F#"#??"" New Layer     {@@@@PK!&6yUUgaphor/ui/pixmaps/profile.pngPNG  IHDRw=bKGD"{ pHYs  ~tIMEIDATxՕKOQ3S545КqFjb5B\%҅PtQӅBRI +?dE[H(N41<$|9srBBZ\ZfVkkkwRk.`xI-;"h/ 5,l,fG{(JC@N˲ǞwY g-gOޝ*`k)ZΠlZ?;,gx.*3LMb>c.]B,cu7\r`8\rNe0L/Q y]Rȳ8`x}Ӻ_Š8~TQ<V7e n{~4IENDB`PK!~;(gaphor/ui/pixmaps/send-signal-action.pngPNG  IHDR}\sRGBbKGDC pHYs  tIME{detEXtCommentCreated with The GIMPd%nIDATHݕKHQ Ӏ hT&jg`v* ZT(=EEE`+ z!:Aj6Q [pp=dGGPPORUe]]}jXjr?3=PTU׌ ɑ.`ڽht Y"oQ[[;իQ` 0-&7/K*KxX@=.T;ܻ{Yj||Xʶ%HbB!C.C ?mp薶HjKQ}>*ڿKmyPZKEE_XqӦXcogA gv.mc1p‎-R*"EБ#k@ Ƒ\)ۊR+b}*+àF0ju>ZRjiEP֭fff& ?\7Zu*౅PL&Õ˗2#S@?T$82P$݉짏1DJp˳%@<8ZڧOϧm۷&3<99Żoevv,y \;Z n]d2}}$imݜnjnB!z/CdX1"lV~m \ su"_+1BVRY`8xPb y%\2B8o0 I٤v֍ ~r~IENDB`PK!Y!} #gaphor/ui/pixmaps/signalactions.xcfgimp xcf file40BBK gimp-commentCreated with The GIMPgimp-image-grid(style solid) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches) 00 New Layer#2     f00z00   },a*`*B)$)-)M*u*x*P)7)-) N)x**bn,=+++F,zd+8++-+Z,`+3++>+gS00 New Layer#1     '00;00K   .,,------------------------.6.00 New Layer     0000ppp]A+]*)&*"(7'' &% $##0 # #s#f$%\&'z'J(.))\*E,@+'-4! Drop Shadow     h4!|4! $    3CF#FGFGE>-3#Y  D#o8 F&y1 F%ە@ F%V F#`" F"ڧb&  F"ݛT"  F!Z F ԁ- FߢL F٠X FT Fq$ Ff F؇8 Fy: Fā7  F p& F!N F"҂3  F"j' F#Z F#ܖ@ F$w/  F%l+  F&d&  D&؟^.3'yJ3DG(GFC:&  + PK!ZC gaphor/ui/pixmaps/state.pngPNG  IHDRw=sBIT|d pHYsvv}ՂtEXtSoftwarewww.inkscape.org<;IDATH핱JA BA>:A,D>@: A$ \FD8NHIMwZ܆wgQ"B6+w "  n)"Jfqs|Fa%uXg Sӎ])^W$z0x"&^|╉ < *^9QrR;9J_^qBkcÃ}cPk-3pҖ '"r 'YZGkBkMt@b^h7Qj(5`P {ݝnX!Xa<=Ed(k OOPLgY(|Ȟ! [&<DZ~9c[fǶXs2c[|*u[\|fbOwg&}7p|L:Txt]u;6;\M~hizߔ~Ǯh4YL_p& ݲNpٽ-jJ+ 3jȞ!CZSUȞ7&NFna!CU~k Av? ţ{cIENDB`PK!Ngaphor/ui/pixmaps/subsystem.pngPNG  IHDR``w8sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<_IDATxI`a#N!#8#5~?!iCH,c;:w  &@Lecr  z̾O{\ařחo+ &@L1b.pfbn[cb  &@L1b  &@L1b  &@L1b  &@L1b  &@L1b  &@L1b-k)] &@l8;a &@ls21b~͛&5_oIENDB`PK!   gaphor/ui/pixmaps/transition.pngPNG  IHDRw=sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATH1 PD| w6g@ONӋX _D>$ 3ben@~ l@ L겍xH* pnPL-3u޻T}NWJX-2IENDB`PK!kgaphor/ui/pixmaps/uml_icons.xcfgimp xcf file00BBg/ gimp-commentCreated with The GIMPUnnamedHA?@A?@@A@A@AAAAAAAAB8AB8AB8AB9AB9AB9AAAAAAB+AB+AB+AA?@noteApApBhABhABhABBBS gimp-commentCreated with The GIMPgimp-image-grid(style intersections) (fgcolor (color-rgba 0.000000 0.000000 0.000000 1.000000)) (bgcolor (color-rgba 1.000000 1.000000 1.000000 1.000000)) (xspacing 10.000000) (yspacing 10.000000) (spacing-unit inches) (xoffset 0.000000) (yoffset 0.000000) (offset-unit inches)  L 00note#3     9k0000   !------------- `00note#2     B0000#O-O-O-K-O-O-O-O-O-O-O-O#P-P-P-L-P-P-P-P-P-P-P-P#Q-Q-Q-L-Q-Q-Q-Q-Q-Q-Q-Q#$ <!<"<#<$<%<&<&<'<(<)<+++++++++++++++++++++++++++++++++a00note#1     @H00\00l",+*)('& % $ # " " ",+*)('& % $ # " " ",+*)('& % $ # " " ",+*)('& % $ # " " 00note     < 00 00 % &'()*+,-.% &'()*+,-.% &'()*+,-.#$%&'()*++,-00package      00 00 ```ar00 package#1      00 00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++00 package#2      r0000   `00class     0000```a++++++++++++b00class#1     0000+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++c+++++++++++++++00class#2     T00h00x   PK!l||gaphor/ui/pixmaps/use-case.pngPNG  IHDRw=bKGD uj! pHYs  tIME  <tEXtCommentCreated with The GIMPd%nIDATxKSa?g;mcºؠq)]fDB+HB$hmE. ]5CVQ.ܹ8a[;s`#HEО} not known" % name) def endElement(self, name): if name == "icon": assert self.id assert self.files add_stock_icon( self.id, self.icon_dir, self.files, self.element, self.option ) self.option = None elif name == "element": try: self.element = getattr(UML, self.data) except: raise ParserException("No element found with name %s" % self.data) elif name == "option": self.option = self.data elif name == "file": self.files.append(self.data) elif name == "stock-icons": pass def startElementNS(self, name, qname, attrs): if not name[0] or name[0] == XMLNS: a = {} for key, val in list(attrs.items()): a[key[1]] = val self.startElement(name[1], a) def endElementNS(self, name, qname): if not name[0] or name[0] == XMLNS: self.endElement(name[1]) def characters(self, content): """Read characters.""" self.data = self.data + content def load_stock_icons(): """Load stock icon definitions from the DataDir location (usually /usr/local/share/gaphor). """ parser = make_parser() icon_dir = os.path.abspath(pkg_resources.resource_filename("gaphor.ui", "pixmaps")) log.info("Icon dir: %s" % icon_dir) loader = StockIconLoader(icon_dir) parser.setFeature(handler.feature_namespaces, 1) parser.setContentHandler(loader) filename = os.path.abspath( pkg_resources.resource_filename("gaphor.ui", "icons.xml") ) parser.parse(filename) # load_stock_icons() # vim:sw=4:et PK!gaphor/ui/tests/__init__.pyPK!U%gaphor/ui/tests/test_consolewindow.pyfrom gaphor.ui.consolewindow import ConsoleWindow from gaphor.tests.testcase import TestCase class ConsoleWindowTestCase(TestCase): services = TestCase.services + [ "main_window", "ui_manager", "action_manager", "properties", ] def test1(self): from gi.repository import Gtk window = ConsoleWindow() assert ( len(window.action_group.list_actions()) == 2 ), window.action_group.list_actions() window.open() window.close() PK!U >#gaphor/ui/tests/test_diagrampage.pyimport unittest from gaphas.examples import Box from gaphor import UML from gaphor.application import Application from gaphor.diagram.comment import CommentItem from gaphor.ui.mainwindow import DiagramPage class DiagramPageTestCase(unittest.TestCase): def setUp(self): Application.init( services=[ "element_factory", "main_window", "ui_manager", "action_manager", "properties", "element_dispatcher", ] ) main_window = Application.get_service("main_window") main_window.open() self.element_factory = Application.get_service("element_factory") self.diagram = self.element_factory.create(UML.Diagram) self.page = DiagramPage(self.diagram) self.page.construct() self.assertEqual(self.page.diagram, self.diagram) self.assertEqual(self.page.view.canvas, self.diagram.canvas) self.assertEqual(len(self.element_factory.lselect()), 1) def tearDown(self): self.page.close() del self.page self.diagram.unlink() del self.diagram Application.shutdown() assert len(self.element_factory.lselect()) == 0 def test_creation(self): pass def test_placement(self): box = Box() self.diagram.canvas.add(box) self.diagram.canvas.update_now() self.page.view.request_update([box]) comment = self.diagram.create( CommentItem, subject=self.element_factory.create(UML.Comment) ) self.assertEqual(len(self.element_factory.lselect()), 2) PK!rX&gaphor/ui/tests/test_diagramtoolbox.pyfrom gaphor.tests.testcase import TestCase from gaphor import UML from gaphor.tests.testcase import TestCase from gaphor.ui.diagrampage import DiagramPage from gaphor.ui.diagramtoolbox import TOOLBOX_ACTIONS class WindowOwner(object): """ Placeholder object for a MainWindow. Should provide just enough methods to make the tests work. """ pass class DiagramToolboxTestCase(TestCase): services = ["element_factory", "properties", "element_dispatcher"] def setUp(self): TestCase.setUp(self) diagram = self.diagram tab = DiagramPage(WindowOwner()) tab.diagram = diagram tab.construct() self.tab = tab def tearDown(self): TestCase.tearDown(self) def test_toolbox_actions_shortcut_unique(self): shortcuts = {} for category, items in TOOLBOX_ACTIONS: for action_name, label, stock_id, shortcut in items: try: shortcuts[shortcut].append(action_name) except KeyError: shortcuts[shortcut] = [action_name] for key, val in list(shortcuts.items()): if key is not None: self.assertEqual(len(val), 1, "Duplicate toolbox shortcut") def test_standalone_construct_with_diagram(self): pass # is setUp() def _test_placement_action(self, action): self.tab.toolbox.action_group.get_action(action).activate() assert self.tab.view.tool # Ensure the factory is working self.tab.view.tool._factory() self.diagram.canvas.update() def test_placement_pointer(self): self.tab.toolbox.action_group.get_action("toolbox-pointer").activate() def test_placement_comment(self): self._test_placement_action("toolbox-comment") def test_placement_comment_line(self): self._test_placement_action("toolbox-comment-line") # Classes: def test_placement_class(self): self._test_placement_action("toolbox-class") def test_placement_interface(self): self._test_placement_action("toolbox-interface") def test_placement_package(self): self._test_placement_action("toolbox-package") def test_placement_association(self): self._test_placement_action("toolbox-association") def test_placement_dependency(self): self._test_placement_action("toolbox-dependency") def test_placement_generalization(self): self._test_placement_action("toolbox-generalization") def test_placement_implementation(self): self._test_placement_action("toolbox-implementation") # Components: def test_placement_component(self): self._test_placement_action("toolbox-component") def test_placement_node(self): self._test_placement_action("toolbox-node") def test_placement_artifact(self): self._test_placement_action("toolbox-artifact") # Actions: def test_placement_action(self): self._test_placement_action("toolbox-action") def test_placement_initial_node(self): self._test_placement_action("toolbox-initial-node") def test_placement_activity_final_node(self): self._test_placement_action("toolbox-activity-final-node") def test_placement_flow_final_node(self): self._test_placement_action("toolbox-flow-final-node") def test_placement_decision_node(self): self._test_placement_action("toolbox-decision-node") def test_placement_fork_node(self): self._test_placement_action("toolbox-fork-node") def test_placement_object_node(self): self._test_placement_action("toolbox-object-node") self.assertEqual(1, len(self.kindof(UML.ObjectNode))) def test_placement_partition(self): self._test_placement_action("toolbox-partition") self.assertEqual(0, len(self.kindof(UML.ActivityPartition))) def test_placement_flow(self): self._test_placement_action("toolbox-flow") # Use cases: def test_usecase(self): self._test_placement_action("toolbox-usecase") def test_actor(self): self._test_placement_action("toolbox-actor") def test_usecase_association(self): self._test_placement_action("toolbox-usecase-association") def test_include(self): self._test_placement_action("toolbox-include") def test_extend(self): self._test_placement_action("toolbox-extend") # Profiles: def test_profile(self): self._test_placement_action("toolbox-profile") def test_metaclass(self): self._test_placement_action("toolbox-metaclass") def test_stereotype(self): self._test_placement_action("toolbox-stereotype") def test_extension(self): self._test_placement_action("toolbox-extension") PK!s$gaphor/ui/tests/test_diagramtools.pyimport logging from gi.repository import Gdk from gaphor import UML from gaphor.core import inject from gaphor.diagram import items from gaphor.tests import TestCase from gaphor.ui.event import DiagramShow from gaphor.ui.interfaces import IUIComponent logging.basicConfig(level=logging.DEBUG) class DiagramItemConnectorTestCase(TestCase): services = TestCase.services + [ "main_window", "ui_manager", "action_manager", "properties", ] component_registry = inject("component_registry") def setUp(self): super(DiagramItemConnectorTestCase, self).setUp() mw = self.get_service("main_window") mw.open() self.main_window = mw self.component_registry.handle(DiagramShow(self.diagram)) def test_item_reconnect(self): # Setting the stage: ci1 = self.create(items.ClassItem, UML.Class) ci2 = self.create(items.ClassItem, UML.Class) a = self.create(items.AssociationItem) self.connect(a, a.head, ci1) self.connect(a, a.tail, ci2) self.assertTrue(a.subject) self.assertTrue(a.head_end.subject) self.assertTrue(a.tail_end.subject) the_association = a.subject # The act: perform button press event and button release view = self.component_registry.get_utility( IUIComponent, "diagrams" ).get_current_view() self.assertSame(self.diagram.canvas, view.canvas) p = view.get_matrix_i2v(a).transform_point(*a.head.pos) event = Gdk.Event() event.x, event.y, event.type, event.state = ( p[0], p[1], Gdk.EventType.BUTTON_PRESS, 0, ) view.do_event(event) self.assertSame(the_association, a.subject) event = Gdk.Event() event.x, event.y, event.type, event.state = ( p[0], p[1], Gdk.EventType.BUTTON_RELEASE, 0, ) view.do_event(event) self.assertSame(the_association, a.subject) PK!T%gaphor/ui/tests/test_elementeditor.pyfrom gaphor.ui.elementeditor import ElementEditor from gaphor.tests.testcase import TestCase class ElementEditorTestCase(TestCase): services = TestCase.services + [ "main_window", "ui_manager", "action_manager", "properties", ] def test1(self): from gi.repository import Gtk window = ElementEditor() assert ( len(window.action_group.list_actions()) == 1 ), window.action_group.list_actions() window.open() window.close() PK!\_B*B*"gaphor/ui/tests/test_handletool.py""" Test handle tool functionality. """ import unittest from zope import component from gaphas.aspect import Connector, ConnectionSink from gi.repository import Gdk, Gtk from gaphor import UML from gaphor.application import Application from gaphor.core import inject from gaphor.diagram.actor import ActorItem from gaphor.diagram.comment import CommentItem from gaphor.diagram.commentline import CommentLineItem from gaphor.diagram.interfaces import IConnect from gaphor.ui.diagramtools import ConnectHandleTool, DiagramItemConnector from gaphor.ui.event import DiagramShow from gaphor.ui.interfaces import IUIComponent class DiagramItemConnectorTestCase(unittest.TestCase): def setUp(self): Application.init( services=[ "adapter_loader", "element_factory", "main_window", "ui_manager", "properties_manager", "action_manager", "properties", "element_dispatcher", ] ) self.main_window = Application.get_service("main_window") self.main_window.init() self.main_window.open() self.element_factory = Application.get_service("element_factory") self.diagram = self.element_factory.create(UML.Diagram) self.comment = self.diagram.create( CommentItem, subject=self.element_factory.create(UML.Comment) ) self.commentline = self.diagram.create(CommentLineItem) def test_aspect_type(self): aspect = Connector(self.commentline, self.commentline.handles()[0]) assert isinstance(aspect, DiagramItemConnector) def test_query(self): assert component.queryMultiAdapter((self.comment, self.commentline), IConnect) def test_allow(self): aspect = Connector(self.commentline, self.commentline.handles()[0]) assert aspect.item is self.commentline assert aspect.handle is self.commentline.handles()[0] sink = ConnectionSink(self.comment, self.comment.ports()[0]) assert aspect.allow(sink) def test_connect(self): sink = ConnectionSink(self.comment, self.comment.ports()[0]) aspect = Connector(self.commentline, self.commentline.handles()[0]) aspect.connect(sink) canvas = self.diagram.canvas cinfo = canvas.get_connection(self.commentline.handles()[0]) assert cinfo, cinfo class HandleToolTestCase(unittest.TestCase): """ Handle connection tool integration tests. """ component_registry = inject("component_registry") def setUp(self): Application.init( services=[ "adapter_loader", "element_factory", "main_window", "ui_manager", "properties_manager", "action_manager", "properties", "element_dispatcher", ] ) self.main_window = Application.get_service("main_window") self.main_window.open() def shutDown(self): Application.shutdown() def get_diagram_view(self, diagram): """ Get a view for diagram. """ view = self.component_registry.get_utility( IUIComponent, "diagrams" ).get_current_view() # realize view, forces bounding box recalculation while Gtk.events_pending(): Gtk.main_iteration() return view def test_iconnect(self): """ Test basic glue functionality using CommentItem and CommentLine items. """ element_factory = Application.get_service("element_factory") diagram = element_factory.create(UML.Diagram) self.component_registry.handle(DiagramShow(diagram)) comment = diagram.create( CommentItem, subject=element_factory.create(UML.Comment) ) actor = diagram.create(ActorItem, subject=element_factory.create(UML.Actor)) actor.matrix.translate(200, 200) diagram.canvas.update_matrix(actor) line = diagram.create(CommentLineItem) view = self.get_diagram_view(diagram) assert view, "View should be available here" comment_bb = view.get_item_bounding_box(comment) # select handle: handle = line.handles()[-1] tool = ConnectHandleTool(view=view) tool.grab_handle(line, handle) handle.pos = (comment_bb.x, comment_bb.y) item = tool.glue(line, handle, handle.pos) self.assertTrue(item is not None) tool.connect(line, handle, handle.pos) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo.constraint is not None) self.assertTrue(cinfo.connected is actor, cinfo.connected) Connector(line, handle).disconnect() cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo is None) def test_iconnect_2(self): """Test connect/disconnect on comment and actor using comment-line. """ element_factory = Application.get_service("element_factory") diagram = element_factory.create(UML.Diagram) self.component_registry.handle(DiagramShow(diagram)) comment = diagram.create( CommentItem, subject=element_factory.create(UML.Comment) ) actor = diagram.create(ActorItem, subject=element_factory.create(UML.Actor)) line = diagram.create(CommentLineItem) view = self.get_diagram_view(diagram) assert view, "View should be available here" tool = ConnectHandleTool(view) # Connect one end to the Comment: handle = line.handles()[0] tool.grab_handle(line, handle) comment_bb = view.get_item_bounding_box(comment) handle.pos = (comment_bb.x1, comment_bb.y1) sink = tool.glue(line, handle, handle.pos) assert sink is not None assert sink.item is comment tool.connect(line, handle, handle.pos) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo is not None, None) self.assertTrue(cinfo.item is line) self.assertTrue(cinfo.connected is comment) # Connect the other end to the Actor: handle = line.handles()[-1] tool.grab_handle(line, handle) actor_bb = view.get_item_bounding_box(actor) handle.pos = actor_bb.x1, actor_bb.y1 sink = tool.glue(line, handle, handle.pos) self.assertTrue(sink.item is actor) tool.connect(line, handle, handle.pos) cinfo = view.canvas.get_connection(handle) self.assertTrue(cinfo.item is line) self.assertTrue(cinfo.connected is actor) # Try to connect far away from any item will only do a full disconnect self.assertEqual( len(comment.subject.annotatedElement), 1, comment.subject.annotatedElement ) self.assertTrue(actor.subject in comment.subject.annotatedElement) sink = tool.glue(line, handle, (500, 500)) self.assertTrue(sink is None, sink) tool.connect(line, handle, (500, 500)) cinfo = view.canvas.get_connection(handle) self.assertTrue(cinfo is None) def skiptest_connect_3(self): """Test connecting through events (button press/release, motion). """ element_factory = Application.get_service("element_factory") diagram = element_factory.create(UML.Diagram) comment = diagram.create( CommentItem, subject=element_factory.create(UML.Comment) ) # self.assertEqual(30, comment.height) # self.assertEqual(100, comment.width) actor = diagram.create(ActorItem, subject=element_factory.create(UML.Actor)) actor.matrix.translate(200, 200) diagram.canvas.update_matrix(actor) # assert actor.height == 60, actor.height # assert actor.width == 38, actor.width line = diagram.create(CommentLineItem) assert line.handles()[0].pos, (0.0, 0.0) assert line.handles()[-1].pos, (10.0, 10.0) view = self.get_diagram_view(diagram) assert view, "View should be available here" tool = ConnectHandleTool(view) tool.on_button_press(Gdk.Event(x=0, y=0, state=0)) tool.on_button_release(Gdk.Event(x=0, y=0, state=0)) handle = line.handles()[0] self.assertEqual( (0.0, 0.0), view.canvas.get_matrix_i2c(line).transform_point(*handle.pos) ) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo.connected is comment) # self.assertTrue(handle.connected_to is comment, 'c = ' + str(handle.connected_to)) # self.assertTrue(handle.connection_data is not None) # Grab the second handle and drag it to the actor tool.on_button_press(Gdk.Event(x=10, y=10, state=0)) tool.on_motion_notify(Gdk.Event(x=200, y=200, state=0xFFFF)) tool.on_button_release(Gdk.Event(x=200, y=200, state=0)) handle = line.handles()[-1] self.assertEqual( (200, 200), view.canvas.get_matrix_i2c(line).transform_point(handle.x, handle.y), ) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo.connected is actor) # self.assertTrue(handle.connection_data is not None) self.assertTrue(actor.subject in comment.subject.annotatedElement) # Press, release, nothing should change tool.on_button_press(Gdk.Event(x=200, y=200, state=0)) tool.on_motion_notify(Gdk.Event(x=200, y=200, state=0xFFFF)) tool.on_button_release(Gdk.Event(x=200, y=200, state=0)) handle = line.handles()[-1] self.assertEqual( (200, 200), view.canvas.get_matrix_i2c(line).transform_point(handle.x, handle.y), ) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo.connected is actor) # self.assertTrue(handle.connection_data is not None) self.assertTrue(actor.subject in comment.subject.annotatedElement) # Move second handle away from the actor. Should remove connection tool.on_button_press(Gdk.Event(x=200, y=200, state=0)) tool.on_motion_notify(Gdk.Event(x=500, y=500, state=0xFFFF)) tool.on_button_release(Gdk.Event(x=500, y=500, state=0)) handle = line.handles()[-1] self.assertEqual( (500, 500), view.canvas.get_matrix_i2c(line).transform_point(handle.x, handle.y), ) cinfo = diagram.canvas.get_connection(handle) self.assertTrue(cinfo is None) # self.assertTrue(handle.connection_data is None) self.assertEqual(len(comment.subject.annotatedElement), 0) PK! hAA"gaphor/ui/tests/test_mainwindow.pyimport unittest from gaphor import UML from gaphor.application import Application from gaphor.core import inject from gaphor.ui.event import DiagramShow from gaphor.ui.interfaces import IUIComponent class MainWindowTestCase(unittest.TestCase): def setUp(self): Application.init( services=[ "element_factory", "properties", "main_window", "ui_manager", "action_manager", ] ) component_registry = inject("component_registry") def tearDown(self): Application.shutdown() def get_current_diagram(self): return self.component_registry.get_utility( IUIComponent, "diagrams" ).get_current_diagram() def test_creation(self): # MainWindow should be created as resource main_w = Application.get_service("main_window") main_w.open() self.assertEqual(self.get_current_diagram(), None) def test_show_diagram(self): main_w = Application.get_service("main_window") element_factory = Application.get_service("element_factory") diagram = element_factory.create(UML.Diagram) main_w.open() self.component_registry.handle(DiagramShow(diagram)) self.assertEqual(self.get_current_diagram(), diagram) PK!ݰ!gaphor/ui/tests/test_namespace.pyfrom gaphor.tests.testcase import TestCase import gaphor.UML as UML from gaphor.ui.namespace import NamespaceModel from gaphor.application import Application class NamespaceTestCase(TestCase): services = ["element_factory"] def tearDown(self): pass def test(self): factory = Application.get_service("element_factory") ns = NamespaceModel(factory) m = factory.create(UML.Package) m.name = "m" assert m in ns._nodes assert ns.path_from_element(m) == (1,) assert ns.element_from_path((1,)) is m a = factory.create(UML.Package) a.name = "a" assert a in ns._nodes assert a in ns._nodes[None] assert m in ns._nodes assert ns.path_from_element(a) == (1,), ns.path_from_element(a) assert ns.path_from_element(m) == (2,), ns.path_from_element(m) a.package = m assert a in ns._nodes assert a not in ns._nodes[None] assert a in ns._nodes[m] assert m in ns._nodes assert a.package is m assert a in m.ownedMember assert a.namespace is m assert ns.path_from_element(a) == (1, 0), ns.path_from_element(a) c = factory.create(UML.Class) c.name = "c" assert c in ns._nodes assert ns.path_from_element(c) == (1,), ns.path_from_element(c) assert ns.path_from_element(m) == (2,), ns.path_from_element(m) assert ns.path_from_element(a) == (2, 0), ns.path_from_element(a) c.package = m assert c in ns._nodes assert c not in ns._nodes[None] assert c in ns._nodes[m] c.package = a assert c in ns._nodes assert c not in ns._nodes[None] assert c not in ns._nodes[m] assert c in ns._nodes[a] c.unlink() assert c not in ns._nodes assert c not in ns._nodes[None] assert c not in ns._nodes[m] assert c not in ns._nodes[a] PK!ySzgaphor/ui/toolbox.py""" Toolbox. """ import logging from zope import component from zope.interface import implementer from gi.repository import GObject from gi.repository import Gdk from gi.repository import Gtk from gaphor.core import _, inject, toggle_action, build_action_group from gaphor.interfaces import IActionProvider from gaphor.ui.interfaces import IUIComponent, IDiagramPageChange from gaphor.ui.diagramtoolbox import TOOLBOX_ACTIONS log = logging.getLogger(__name__) @implementer(IUIComponent, IActionProvider) class Toolbox(object): TARGET_STRING = 0 TARGET_TOOLBOX_ACTION = 1 DND_TARGETS = [ Gtk.TargetEntry.new("STRING", Gtk.TargetFlags.SAME_APP, TARGET_STRING), Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, TARGET_STRING), Gtk.TargetEntry.new( "gaphor/toolbox-action", Gtk.TargetFlags.SAME_APP, TARGET_TOOLBOX_ACTION ), ] title = _("Toolbox") placement = ("left", "diagrams") component_registry = inject("component_registry") main_window = inject("main_window") properties = inject("properties") menu_xml = """ """ def __init__(self, toolbox_actions=TOOLBOX_ACTIONS): self._toolbox = None self._toolbox_actions = toolbox_actions self.action_group = build_action_group(self) self.action_group.get_action("reset-tool-after-create").set_active( self.properties.get("reset-tool-after-create", True) ) self.buttons = [] self.shortcuts = {} def open(self): widget = self.construct() self.main_window.window.connect_after( "key-press-event", self._on_key_press_event ) self.component_registry.register_handler(self._on_diagram_page_change) return widget def close(self): if self._toolbox: self._toolbox.destroy() self._toolbox = None self.component_registry.unregister_handler(self._on_diagram_page_change) def construct(self): def toolbox_button(action_name, stock_id, label, shortcut): button = Gtk.ToggleToolButton.new() button.set_icon_name(stock_id) button.action_name = action_name if label: button.set_tooltip_text("%s (%s)" % (label, shortcut)) # Enable Drag and Drop inner_button = button.get_children()[0] inner_button.drag_source_set( Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON3_MASK, self.DND_TARGETS, Gdk.DragAction.COPY | Gdk.DragAction.LINK, ) inner_button.drag_source_set_icon_stock(stock_id) inner_button.connect( "drag-data-get", self._button_drag_data_get, action_name ) return button toolbox = Gtk.ToolPalette.new() toolbox.connect("destroy", self._on_toolbox_destroyed) for title, items in self._toolbox_actions: tool_item_group = Gtk.ToolItemGroup.new(title) for action_name, label, stock_id, shortcut in items: button = toolbox_button(action_name, stock_id, label, shortcut) tool_item_group.insert(button, -1) button.show() self.buttons.append(button) self.shortcuts[shortcut] = action_name toolbox.add(tool_item_group) tool_item_group.show() toolbox.show() self._toolbox = toolbox return toolbox def _on_key_press_event(self, view, event): """ Grab top level window events and select the appropriate tool based on the event. """ if event.get_state() & Gdk.ModifierType.SHIFT_MASK or ( event.get_state() == 0 or event.get_state() & Gdk.ModifierType.MOD2_MASK ): keyval = Gdk.keyval_name(event.keyval) self.set_active_tool(shortcut=keyval) def _on_toolbox_destroyed(self, widget): self._toolbox = None @toggle_action(name="reset-tool-after-create", label=_("_Reset tool"), active=False) def reset_tool_after_create(self, active): self.properties.set("reset-tool-after-create", active) @component.adapter(IDiagramPageChange) def _on_diagram_page_change(self, event): self.update_toolbox(event.diagram_page.toolbox.action_group) def update_toolbox(self, action_group): """ Update the buttons in the toolbox. Each button should be connected by an action. Each button is assigned a special _action_name_ attribute that can be used to fetch the action from the ui manager. """ if not self._toolbox: return for button in self.buttons: action_name = button.action_name action = action_group.get_action(action_name) if action: button.set_related_action(action) def set_active_tool(self, action_name=None, shortcut=None): """ Set the tool based on the name of the action """ # HACK: toolbox = self._toolbox if shortcut and toolbox: action_name = self.shortcuts.get(shortcut) log.debug("Action for shortcut %s: %s" % (shortcut, action_name)) if not action_name: return def _button_drag_data_get(self, button, context, data, info, time, action_name): """The drag-data-get event signal handler. The drag-data-get signal is emitted on the drag source when the drop site requests the data which is dragged. Args: button (Gtk.Button): The button that received the signal. context (Gdk.DragContext): The drag context. data (Gtk.SelectionData): The data to be filled with the dragged data. info (int): The info that has been registered with the target in the Gtk.TargetList time (int): The timestamp at which the data was received. """ data.set(type=data.get_target(), format=8, data=action_name.encode()) PK!bgaphor/ui/toplevelwindow.py""" Basic stuff for toplevel windows. """ import os.path from gi.repository import GdkPixbuf from gi.repository import Gtk import pkg_resources from zope.interface import implementer from gaphor.ui.interfaces import IUIComponent ICONS = ( "gaphor-24x24.png", "gaphor-48x48.png", "gaphor-96x96.png", "gaphor-256x256.png", ) @implementer(IUIComponent) class ToplevelWindow(object): menubar_path = "" toolbar_path = "" resizable = True def __init__(self): self.window = None def ui_component(self): raise NotImplementedError def construct(self): self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) self.window.set_title(self.title) self.window.set_size_request(*self.size) self.window.set_resizable(self.resizable) # set default icons of gaphor windows icon_dir = os.path.abspath( pkg_resources.resource_filename("gaphor.ui", "pixmaps") ) icons = ( GdkPixbuf.Pixbuf.new_from_file(os.path.join(icon_dir, f)) for f in ICONS ) self.window.set_icon_list(*icons) self.window.add_accel_group(self.ui_manager.get_accel_group()) if self.menubar_path or self.toolbar_path: # Create a full featured window. vbox = Gtk.VBox() self.window.add(vbox) vbox.show() menubar = self.ui_manager.get_widget(self.menubar_path) if menubar: vbox.pack_start(child=menubar, expand=False, fill=True, padding=0) toolbar = self.ui_manager.get_widget(self.toolbar_path) if toolbar: vbox.pack_start(child=toolbar, expand=False, fill=True, padding=0) vbox.pack_end( child=self.ui_component, expand=self.resizable, fill=True, padding=0 ) vbox.show() # TODO: add statusbar else: # Create a simple window. self.window.add(self.ui_component()) self.window.show() PK!H½F'gaphor-1.0.0.dist-info/entry_points.txtTN0 |"0HB *&QqNiٝ/NJkmtJ.wQ+w~d$_ĬMXPoIRcOUFhQޙ_XeBUEƪjhb嚲 @mZm_vMW"&#QYQu !~,a|N_2Pl喊]ybQj }tdz%ة+7#ȆqFX"v̄n`EQAp*$BnHnj'v[.O ^Sʎ|9ǿƘ>Ȟ`_ִSo`5c(3t(yCzP/_3! DY43<^[vÑ d.-;2Dȗ-mt;_L?bY>jX>MW5hOp޵͘ W v:wLe#BPK!**"gaphor-1.0.0.dist-info/LICENSE.txtApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and 2. You must cause any modified files to carry prominent notices stating that You changed the files; and 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {{ year }} {{ organization }} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.PK!HnHTUgaphor-1.0.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H6-Ҁ:>gaphor-1.0.0.dist-info/METADATA;v7v XlU")P$MӖHFF1Un e"l&,EME6,'r-QB.._G<6R%;>;lӡ^9ZA;c;숦4, f8H2LM20i6TI}O,5;ͦ'ЂG)R jz/d(pO=9}s}+#e/#_|ų^尥a{-O7{bp݌/\5㺗<<=>26L"51w.:rF:73@W1 C&td( l^hfŅJeHRL8!a$3E܈h1꾃?Ux Mnm+6ns0֟tX: 9k?&9wV R `AAHՋ[f2(im̆ywv(62]}c&n&~yDp ! \vl<~.?|8rQH\]E" ~4mZ[lb]7 v'ב >Kdwl˽K4=-lHH4YL+a (|h!"KtS+@L/xk@՛/T7}gu(vpCv> |r9HvSFmHpD^3 4Zrw̗~+Zg|c yXoxԔP0(݁~ }hW5U0` &^eVac|n3*O"PG7aBO4ڀa,ǿ4R _W*|yWTl?5,T wEì@XxU/*p"aA"Оj]CLDhdPZsq"e@jVZ80 -'j 4TNGS;T(kirwJBcM9 Ɲ!V2D&ֽ!*@en,&K'A JB kZ9 D2z:0CC퉺4 x T|DMby@ VZQ[=p\ VtFdy@+7 6Vy1' }}qgB $7D8m+]lS,gW^ i*@ԅ՜u9 2ĻSwgݗK4xh,' ms!d@ 9lAWj0>eDt1#؈0UdZ:KiOI /Xj0-qevސ~c3'Pޅ %ZiM]]]Apz<\@y }]^KAzR< (8U .êPaM껇pak8蒡1n9k'4PEp9 ,!w0ւJ0QSﲧH$ M/OTHcrj1(F)v0R#A)T8M&v!Q\q[?lmT|Eiq\JJi hvrk6C\]]-fܫ+W`0ҜENU &1Oaoy٨;?j 뻴R P򥓤5Lf9 %I,<E0`ٷud9mYsZj*B[s#˻腥WT.O8*} ! 5YQd႑3eFFީcrxU{LY!V~G%Rj-` Ӓ~ÝfA$["hJ….Q=-&7)B<57!3Ki-;ъ~Lx]L) Z]lt ~U†:%@掩.CQ@s5):cUmܴ @=F+lwEe4X@#nbO2a+*/zָWM-1bti˰=f™]I(` \jn! rFk9ݰ29xtha23EBQz蠗}y!O){EkI=;.Q6Pbg-%Uִՙ"f3<ţ{?"2F\բk 0wrT36xai6M@t(CvB%6;x3).BԚQ'"'&+*} f a9q&j(d֤/cAgBJ#yIBjNiz1D\`S >ĊKfQt8:zeku[73[4I鹓(%Fv-h+|[gNO =YDXoZJE=tUy{QDkR&LE^cݲw`j =Me2q- $>KԤT7Gʺ',j-(€rM4$FuT%!Cnydd c"*MxR0]BV.ZHEc~V'>{?=xusޫ (0 {kYӲ?ƧԚ?R5о$xl. \~!WV`ʅ;b!onou6]njV+l}yP<;x|o||:; MZrk!dē7G%og4`%l]^ |@Όu37k!H1kh Oc&ZyG ޜ%f"4zY'P9D: g4cJw&-' 'UYjrhq3PW,l o6pCnwvXDj9s`edy#j "(>|AP;dV#D:Y^Rlv7y@q(TR(@+`TMṪaj\Fw Lx;H6T/ {xNW@jvt[mU*ZP%}sgyVp(z"+RX`k[]ȞLa>лܟ+̪~\ewl|Y$qwKDBDx)GC.cJn6P85jZ.oy \_8mvMa:|E` (HasCχN'f /-$ Dfj` bixYc@b_+^VQ0KPSmՔ?Z-~ٻgw?[Ƚ+t-~-{~aݠ_UW>QsX&#amǾXG! ogqѝWQ@ĸӞ &հDRQO4Ҫ|³M%=B3((5:8r3D/cbmp$ԍxBP٘0/@CU6}[t[gM\6A  *+`,'J;:!Cu?[q\QՐ% EޠEU{l.-ؽ"WZrł\?Yن1 uz:mʛjm8y8=޻YPJ:HE^3ڱ}G2hY*!0 Xϟ?̣*<cޅdBjb{0~6cB/6M1n# EY3ˁAC| 8ī j̸N\B<IU03sҘ=; J`0 떴)V=p Dyk2fJY:\` ׍*z{#6 Z }eSJCrn͝n}*a1-$H r\;Ɨ3hg<;{c$$6 csFk-$w` *\76̫u rC嶮`J:F{q9ű*|FoHꟆ[Z}_15=\C XLz%0~CqՄ!YïYwnGQi  fDC~kF,=Ȣ+b>A ߒ^G[[%Q҈YMbMt#WMi?  !`GS'Lspwi a1[$>K g*{[kPPPEժxvlKVH5"x7ПG/ î0JC}WEӑlC Ul?F&)v?r*MOh[W} oMgʃo()6GKd\G]Zj) Mʲ%iG&iLZ8) lvt[`.,Ne-v2n0m[+`ڏqNDdx_!{_U${MaO2/Gq%teY^}Tծ߄ݿ9|A5k\m+KYS#{Et߶l_z%}4I{_ sƁ⺕@ɿP܎S2eF*7V%*=O#wj;•F?x?k#n `X%; dF$U0Ch'SFxn5yf&$Ё{E[aYks6qs7[n>xv*F}淾4hW2Lj2\kJ7 ֡VuW0Cz-L[}4ƕ6j:s:O),x_r[̣f1e8Ij,`$&J`g_S{=-Q!;J9-Qx͌I w`H2.PEwO3PASas6D-@O;Ju$No WIH1Ws0aƹ:bd?J|˶rMt>=f{I霽J"ӽFf@[y:%%u3`cXz d9Ejfs (rxUE_T}g?,4ۖ}wIpcdXd9)snٷ"nևjFc-$S9"W&Tj+TEHΏ?Kל]2oTӯjnĹbCLaLnj [k7$f(i+f"rk`g]AtO"ȐlCȋFm H;3xh@^X#*LrJJݠwoza4bp _XBe4cXW؋Dxoac5@Pe"+i:UClCa/ ́6W mD}G@EGJi L3ɾGy|qOqEhĸm/U= ŠljZOèIv}373Dd #E]ah)uөz m/ <6BC/Pu;ytfp'n5q82y_Qfi~SgF8 eŽk5 "ϥ=PFU-(UXu&ޜ9Tȡ~HK.ڟ,D潲+sBnԽ"xApMjR2. 5rn6]K!xG01Jlv)٭{o,GH6χe?-]>JەО[}h^_'W~sPKҡf.MuoޝNEgtI ',x=p4JIX:X38Ӏ~*x-tC/uBUs"tݱ pFzMh9ٷRqmq_?<'js>K7UU(gƦI0GZwܐ(N-&@w=D,Ef]t רYsD>'C,X~inqN,^4Oځcx^Qowu\<-uXl5ߑ^g,e1V~OJ$^O_ؑ4:BOS  $ =s]pCb#'pHv6r"KϞӟϝ\<ZP#KΝc@_;f_GC8w| +VR)ܞSS*@쉔8+XO:&ҩ2А9`gw{vq`,$s:^z'\ 1"n vj& LEcNuբbhLcEY 2m# 5<IM` p :a-o֠i`h$ ! k:GҴq@8)RqWGO[lj ? 9NE(=dDK6sLqE$쌃?Y{NeIp* :.Rn)uY7W5>ÿ")כ>$[ux03R$.\!}tb{5뙒:6,'h3;3dLyߙ@|T(>߱&#%ˑSX`a*/# ΃0Ghϰ/p~Wwײv4C.ǧ"Cd8 B1@me*A$%wzaA`%M>-"VhOeDxICzH378YmWSZ~h&#EL\mBiǥ>/!n r,2S&cƌJG8qdk 8 Q_`R%39̈́ K3e#UG ^‘V)A+'usƞ-`{N93JJy+3E JwD7OUZ($I;f ! XLTB!7yOB?{[?ލKJywpƃrSظ.8ھ-泫?||}._Q.}y'AS,ÙޮI3c}4[ xH0Gg-/Mv y 5wEpWV>쿇].s } &2WV,؂){?qϿx6`\=>kv0;Xq1E.o(EWU[5_\fU!کzq}vԣVh'TڴC?he<ŸaTcB4-船-8Rx&?~ CmNBd?-~v{:q ᅥf 4F:A ܠ%csY]Eo7u4\#122k>Ϝ<&+[4Q[ S/F8;NGNO=X5_Jc,햣=PՇ}o |QO\~w0``rc}h:Nl-(3H>p&xvy@zhyևxQjkD;'3E]'s05Ya?}+ǷgjM1;6xߗ* >7~r]&vï7i8իۋ/[ap92KΧm j"K?N8nlA[x~50EL?jU!Jvk"-Ժ(_#PtʻbS[ Ls0+n~]qJ וr/'ד&mD뽔zл1Khf.&܏Ӣ&׈K5ٸR-_g+yZ "LGl=y_?<ї~9R.z59Zj[@Wj>(Y5'^`/xu jK/yĕ\hW ">+ Ҷv>]tcOUN \LG|^^D=a2/뤣d:.C'5.cO.B" G֬7 Uر;Ml9x,LހG/>cW/Pj;[ڙz.ۦRW7Tn; \@RPn|l!ﵾrOyQ\2r"hڽJE9W]:]U®R+bsE;BXzc`GQTG)6(Q?1+,UDl/ȼ/URmVxwoOޘ̲ PNg_#1nTRi5!4{˔ɑ"IM7Y{e"CY[H_ O;hΉUTײH_v wwڣjUUR\ WZW OŠN(<{N8tV"wh<\c:\z|KUգyuǯ< ݍv<ϒEC~k,n z%؟mN8 jh.I :M)7`\7%՗A*)PUI"=G %F3QW#y[~q=( 3XRtO!X CqcUk6KrkTVxjʨ+]tVtO[CCT7`6_Cw(K]hvcK֛]譠0l?w_ϾHI,On]7T(~OQ||7Ld_Pa9QDgȷ[iBN/D&ӽC llVʊ䡮7> ¥n~o Q2w9bl/GV?OorZ`{F= gmwaî#npH|ԤڶavNʉ57$CC}t6O'?-[*=iϮ?7*!9ydFxªp`="aM*8SM3l,:zxesrnPНA;hO}ۊzSPh E$N|BI4I x93'-ǝ3Lڠx#3*]ɝO!+ STxu ?0GBuKM6pKeJItu(Eo5Go{%sHzj7G`]2@lc3v!01 Mw~BbG!RCYn6!9-$>p(q'G' }MrbYR0 \"![z7>pȟ6luVx% M(;YuLÛ{^wFr"9TH{8Uh䟙>^1PKm~nĥwtSMpNs;̝T ~^ J8[jT,~JL_*O=la ]qxX ~:AWK]xk!y'" mQxgaue^A:*z e}(gh4G8w3Su|I3ŃpoA=i+^ˌ S>q/̀T9֌&Uٙ.? ף^`9n0>C Z:ZUF]iTo5Ag{C,'oD!&F-?^[u[t|4`45&%gF Ǯ/$ {/vdHDYnUM[#uwMeG#2^;)Tή0Lg1 9 ?}o~M"o9 LN4 ۛvi/tZJn;|on/28 Ͼ=4:R+bG_' 2l+wƬ1;\GN7G _FFo̅fVtҌGa0ޢrW tƾnPFjOiOPv(%sȀs]R; VIerBAw4 :x<uHT"=\rP5(>IyKOO"{:r73cv[[yg/jOEf“F i ߉IL}3_K,w*#4+SF%QoyD3vu1mTݖB$G8Wr^ѷ/_ӖA8[OUPާ=Z=^{ ڹ_޿o@M*%t*}BJ}vMx: jFִ-")1z1oq141DAը0H\{a9MIWxtٟ?GN.2Mֶ[XQ9?w=׫~gKNvo;B8 AXJ)n ~eS9gJςR_SxZ$ Z:§F3ODNs?>\ⴃra|k\V)7NK/PW?(TCii5HMNK"C/rug,UU_wo':DoKd2RJC$Dq#;woy}e%`AYV$~ʰ$na\3!i=uΫ]uMG̘;BK9;Pr.hBAoB>ҳ5$liM[CsteqG)2n5зs.,ͮqY`^xh1Y('rf/y@}==ˡSNv3^ .3ZunpT?x*v;JG MMMj*۠;yn9|*0R]$ܡoSڑĪaUx"w~?wly!gh L GlNWc9`k" -p{]lpMLj`|'<_ X%)lC_ Q>lk9,2.̑z!xXbAĿg@\L-.w=CV?q٭6&V#҆8l`Qġ SpDWǿdAmWzvYHM( <c[SjYymߍo9qF#bv K DU {ҦC]X L.њ&wX}qfTTO0e/O~=svcŴ:"h4/i -2ڥna^> .Q}lUX)\vcl, 3kc/琦cZpvUɞvŃfz[;0BYTέğ۫Nw E8UBsoypkokR<d?x>h5#*ȝcBaqq.dA\|Ɛ51AP3onyX- '}P׼S.: _&dwCVl09ܙ \#| Ȧ5eWK%M Sn$!q;h]4(U>r2_p'HH˨zL+ &s@̄h2u㬎t``WWB> _"կMN߱#qbd(vHhREQA}d/؆ߨ[WDi*k\'xRf5zGk1UFO2e7E"m{n7ԙgaphor/adapters/profiles/extensionconnect.pyPK! +Kgaphor/adapters/profiles/metaclasseditor.pyPK!ڜ__*Ugaphor/adapters/profiles/stereotypepage.pyPK!*vrgaphor/adapters/profiles/tests/__init__.pyPK!0rgaphor/adapters/profiles/tests/test_extension.pyPK!ʆ6%zgaphor/adapters/profiles/tests/test_metaclasseditor.pyPK!C_G5V}gaphor/adapters/profiles/tests/test_stereotypepage.pyPK!xH gaphor/adapters/propertypages.pyPK!Zl 1gaphor/adapters/relationships.pyPK!y'bb"9gaphor/adapters/states/__init__.pyPK!C!k '9gaphor/adapters/states/propertypages.pyPK!(Fgaphor/adapters/states/tests/__init__.pyPK!(ߎm887Ggaphor/adapters/states/tests/test_transition_connect.pyPK!?V '_gaphor/adapters/states/vertexconnect.pyPK!!lgaphor/adapters/tests/__init__.pyPK!V\'vv%%mgaphor/adapters/tests/test_comment.pyPK!ō00'ދgaphor/adapters/tests/test_connector.pyPK!$Sgaphor/adapters/tests/test_editor.pyPK!(_*_*&Hgaphor/adapters/tests/test_grouping.pyPK! Orr+gaphor/adapters/tests/test_propertypages.pyPK!$gaphor/adapters/usecases/__init__.pyPK!*gaphor/adapters/usecases/tests/__init__.pyPK!-0gaphor/adapters/usecases/tests/test_extend.pyPK!S]0  .jgaphor/adapters/usecases/tests/test_include.pyPK!ؾ,,*gaphor/adapters/usecases/usecaseconnect.pyPK!,,Dgaphor/application.pyPK!6)"ccgaphor/core.pyPK!c==2gaphor/diagram/READMEPK!eP gaphor/diagram/__init__.pyPK!"]gaphor/diagram/actions/__init__.pyPK! gaphor/diagram/actions/action.pyPK!ԷLBLBgaphor/diagram/actions/flow.pyPK!#.]gaphor/diagram/actions/partition.pyPK!$((+mgaphor/diagram/actions/tests/test_action.pyPK!h^>)tngaphor/diagram/actions/tests/test_flow.pyPK!d2˸%%rgaphor/diagram/activitynodes.pyPK! Lgaphor/diagram/actor.pyPK!WL֪Zgaphor/diagram/artifact.pyPK!"<gaphor/diagram/classes/__init__.pyPK!a I I%|gaphor/diagram/classes/association.pyPK!a@d d $gaphor/diagram/classes/dependency.pyPK!n66(pgaphor/diagram/classes/generalization.pyPK!/Y(gaphor/diagram/classes/implementation.pyPK!h $$#$gaphor/diagram/classes/interface.pyPK!h5 %gaphor/diagram/classes/klass.pyPK!5Y!Bgaphor/diagram/classes/package.pyPK!(Fgaphor/diagram/classes/tests/__init__.pyPK!,@ 0Fgaphor/diagram/classes/tests/test_association.pyPK!b*Ugaphor/diagram/classes/tests/test_class.pyPK!, hgaphor/diagram/classes/tests/test_feature.pyPK!H  .elgaphor/diagram/classes/tests/test_interface.pyPK!ͥwgaphor/diagram/classifier.pyPK!PB9{gaphor/diagram/comment.pyPK!u igaphor/diagram/commentline.pyPK!AتJJgaphor/diagram/compartment.pyPK!!eOLL-gaphor/diagram/component.pyPK!%gaphor/diagram/components/__init__.pyPK!f#!&gaphor/diagram/components/subsystem.pyPK!+gaphor/diagram/components/tests/__init__.pyPK!a0,,gaphor/diagram/components/tests/test_node.pyPK!jPPgaphor/diagram/connector.pyPK!c c gaphor/diagram/diagramitem.pyPK!}   gaphor/diagram/diagramline.pyPK!x x h5gaphor/diagram/elementitem.pyPK!d.ABgaphor/diagram/extend.pyPK!2_Cgaphor/diagram/extension.pyPK!4&ӭUGgaphor/diagram/include.pyPK!i$9Jgaphor/diagram/interaction.pyPK!ך #Mgaphor/diagram/interfaces.pyPK!L Ygaphor/diagram/items.pyPK!U--egaphor/diagram/lifeline.pyPK!#(ɠ&&mgaphor/diagram/message.pyPK!\gaphor/diagram/nameditem.pyPK!"^^gaphor/diagram/node.pyPK!kdgaphor/diagram/objectnode.pyPK!#gaphor/diagram/profiles/__init__.pyPK!>e$8gaphor/diagram/profiles/metaclass.pyPK!kgaphor/diagram/simpleitem.pyPK!ozNN!gaphor/diagram/states/__init__.pyPK!;>>#agaphor/diagram/states/finalstate.pyPK! v`%gaphor/diagram/states/pseudostates.pyPK! | gaphor/diagram/states/state.pyPK!'}gaphor/diagram/states/tests/__init__.pyPK!Ȁ]]0gaphor/diagram/states/tests/test_pseudostates.pyPK!AA*mgaphor/diagram/states/tests/test_states.pyPK!ff.gaphor/diagram/states/tests/test_transition.pyPK!)7|oo# gaphor/diagram/states/transition.pyPK!0 ##Xgaphor/diagram/style.pyPK! 0gaphor/diagram/tests/__init__.pyPK!*0gaphor/diagram/tests/test_activitynodes.pyPK!zVd!d!3I9gaphor/diagram/tests/test_classifier_stereotypes.pyPK!`cc&Zgaphor/diagram/tests/test_connector.pyPK!ֺhh(cgaphor/diagram/tests/test_diagramitem.pyPK!0|'Sigaphor/diagram/tests/test_interfaces.pyPK!qm$kgaphor/diagram/tests/test_message.pyPK!K Oss'|gaphor/diagram/tests/test_objectnode.pyPK!Y\\'mgaphor/diagram/tests/test_simpleitem.pyPK!%%"gaphor/diagram/tests/test_style.pyPK!Bz44^gaphor/diagram/textelement.pyPK!Fgaphor/diagram/usecase.pyPK!Hj;Tgaphor/event.pyPK!jugaphor/i18n.pyPK!-7~~ugaphor/interfaces.pyPK!11%gaphor/misc/__init__.pyPK!0XE  gaphor/misc/colorbutton.pyPK!:##gaphor/misc/console.pyPK!oE%gaphor/misc/errorhandler.pyPK!wwEE gaphor/misc/gidlethread.pyPK!vDŽh9gaphor/misc/listmixins.pyPK!' Xgaphor/misc/odict.pyPK!,^gaphor/misc/rattr.pyPK!0  #cgaphor/misc/tests/test_xmlwriter.pyPK!}ٹImm=> gaphor/services/filemanager.pyPK!—gg!gaphor/services/helpservice.pyPK!MB***#!gaphor/services/properties.pyPK!>w%8!gaphor/services/propertydispatcher.pyPK!77#A!gaphor/services/sanitizerservice.pyPK!Ѷ.  "DU!gaphor/services/serviceregistry.pyPK!!^!gaphor/services/tests/__init__.pyPK!bb+^!gaphor/services/tests/test_actionmanager.pyPK!療ō )`!gaphor/services/tests/test_copyservice.pyPK!qU~2[m!gaphor/services/tests/test_diagramexportmanager.pyPK!@M0606/p!gaphor/services/tests/test_elementdispatcher.pyPK!Gdd),!gaphor/services/tests/test_filemanager.pyPK!(׭!gaphor/services/tests/test_properties.pyPK!.!gaphor/services/tests/test_sanitizerservice.pyPK!²--)!gaphor/services/tests/test_undomanager.pyPK! 66!gaphor/services/undomanager.pyPK!LL."gaphor/storage/__init__.pyPK!(#C+C+q/"gaphor/storage/parser.pyPK!^44Z"gaphor/storage/storage.pyPK! "gaphor/storage/tests/__init__.pyPK!Yx\*\*$N"gaphor/storage/tests/test_storage.pyPK!t9JJ#"gaphor/storage/tests/test_verify.pyPK!t t w"gaphor/storage/verify.pyPK!o11!"gaphor/tests/__init__.pyPK!; "gaphor/tests/test_action.pyPK!Jkk "gaphor/tests/test_application.pyPK!SVnn -"gaphor/tests/test_transaction.pyPK!K2{xx"gaphor/tests/testcase.pyPK!ᩏ"gaphor/tools/README.txtPK!G"gaphor/tools/__init__.pyPK!d }"gaphor/tools/gaphorconvert.pyPK!j@411h #gaphor/transaction.pyPK!^f@'  #gaphor/transaction.txtPK!Aff*#gaphor/ui/__init__.pyPK!5A0!!-#gaphor/ui/accelmap.pyPK!CC C 0#gaphor/ui/consolewindow.pyPK!P//i:#gaphor/ui/diagrampage.pyPK!;2z:R:R5j#gaphor/ui/diagramtoolbox.pyPK!gs^==#gaphor/ui/diagramtools.pyPK!Jp#gaphor/ui/elementeditor.pyPK!Wf$gaphor/ui/event.pyPK!͌*}$gaphor/ui/filedialog.pyPK!Qkx[$gaphor/ui/iconoption.pyPK!t"$gaphor/ui/icons.xmlPK!01]d7$gaphor/ui/interfaces.pyPK!-?$gaphor/ui/layout.pyPK!m^H$gaphor/ui/layout.xmlPK!+MMOI$gaphor/ui/mainwindow.pyPK!}@v@vr$gaphor/ui/namespace.pyPK! %gaphor/ui/pixmaps/__init__.pyPK!)!%gaphor/ui/pixmaps/accept-event-action.pngPK!Պ%gaphor/ui/pixmaps/action.pngPK!-)R%gaphor/ui/pixmaps/activity-final-node.pngPK!ߐ  )j%gaphor/ui/pixmaps/activity-final-node.xcfPK!:g>Y(#%gaphor/ui/pixmaps/activity-partition.pngPK!K%%gaphor/ui/pixmaps/actor.pngPK!ސ۝PP)%gaphor/ui/pixmaps/artifact.pngPK!C00+%gaphor/ui/pixmaps/artifact.xcfPK!I!@%gaphor/ui/pixmaps/association.pngPK!zmBccB%gaphor/ui/pixmaps/attribute.pngPK!aF(PPD%gaphor/ui/pixmaps/box.pngPK!ee F%gaphor/ui/pixmaps/class.pngPK!8rVV"G%gaphor/ui/pixmaps/comment-line.pngPK!xa=K%gaphor/ui/pixmaps/comment.pngPK!VQQM%gaphor/ui/pixmaps/component.pngPK!KKP%gaphor/ui/pixmaps/component.xcfPK!$$$X%gaphor/ui/pixmaps/connector.pngPK!/@@"Y%gaphor/ui/pixmaps/control-flow.pngPK!7#}\%gaphor/ui/pixmaps/decision-node.pngPK!إ}# _%gaphor/ui/pixmaps/dependency.pngPK!L/ d%gaphor/ui/pixmaps/device.pngPK!'M n%gaphor/ui/pixmaps/diagram.pngPK!ynLo%gaphor/ui/pixmaps/ellipse.pngPK!qUr%gaphor/ui/pixmaps/extend.pngPK!xGrΏv%gaphor/ui/pixmaps/extension.pngPK!-!ry%gaphor/ui/pixmaps/final-state.pngPK!@\\%|%gaphor/ui/pixmaps/flow-final-node.pngPK!¥L!%gaphor/ui/pixmaps/fork-node.pngPK!C "6%gaphor/ui/pixmaps/gaphor-24x24.pngPK!Cdd$%gaphor/ui/pixmaps/gaphor-256x256.pngPK!ٮff"%gaphor/ui/pixmaps/gaphor-48x48.pngPK!%")(T(T" &gaphor/ui/pixmaps/gaphor-96x96.pngPK!Uܿ$ b&gaphor/ui/pixmaps/generalization.pngPK!$ e&gaphor/ui/pixmaps/implementation.pngPK!FFJh&gaphor/ui/pixmaps/include.pngPK!L͕"@l&gaphor/ui/pixmaps/initial-node.pngPK!L͕*n&gaphor/ui/pixmaps/initial-pseudo-state.pngPK!""!o&gaphor/ui/pixmaps/interaction.pngPK!p0!q&gaphor/ui/pixmaps/interaction.xcfPK!`y&gaphor/ui/pixmaps/interface.pngPK!~[|&gaphor/ui/pixmaps/lifeline.pngPK!RB~&gaphor/ui/pixmaps/lifeline.xcfPK!⒁UU&gaphor/ui/pixmaps/line.pngPK!C((q&gaphor/ui/pixmaps/logo.pngPK!GKKў&gaphor/ui/pixmaps/message.pngPK!ee W&gaphor/ui/pixmaps/meta-class.pngPK!&q&gaphor/ui/pixmaps/node.pngPK!9&gaphor/ui/pixmaps/node.xcfPK!-(l!ʻ&gaphor/ui/pixmaps/object-node.pngPK!΄Y&gaphor/ui/pixmaps/partition.pngPK!T}SS&gaphor/ui/pixmaps/pointer.pngPK!jXN&gaphor/ui/pixmaps/pointer.xcfPK!&6yUU8&gaphor/ui/pixmaps/profile.pngPK!L͕!&gaphor/ui/pixmaps/pseudostate.pngPK!~;(&gaphor/ui/pixmaps/send-signal-action.pngPK!Y!} #f&gaphor/ui/pixmaps/signalactions.xcfPK!ZC &gaphor/ui/pixmaps/state.pngPK!G{{ &gaphor/ui/pixmaps/stereotype.pngPK!NH&gaphor/ui/pixmaps/subsystem.pngPK!   g&gaphor/ui/pixmaps/transition.pngPK!k&gaphor/ui/pixmaps/uml_icons.xcfPK!l||p'gaphor/ui/pixmaps/use-case.pngPK!=]]('gaphor/ui/questiondialog.pyPK!2'gaphor/ui/statuswindow.pyPK!Cfp'gaphor/ui/stock.pyPK!8''gaphor/ui/tests/__init__.pyPK!U%q''gaphor/ui/tests/test_consolewindow.pyPK!U >#)'gaphor/ui/tests/test_diagrampage.pyPK!rX&0'gaphor/ui/tests/test_diagramtoolbox.pyPK!s$C'gaphor/ui/tests/test_diagramtools.pyPK!T%L'gaphor/ui/tests/test_elementeditor.pyPK!\_B*B*"WN'gaphor/ui/tests/test_handletool.pyPK! hAA"x'gaphor/ui/tests/test_mainwindow.pyPK!ݰ!Z~'gaphor/ui/tests/test_namespace.pyPK!ySzI'gaphor/ui/toolbox.pyPK!bY'gaphor/ui/toplevelwindow.pyPK!H½F''gaphor-1.0.0.dist-info/entry_points.txtPK!**"'gaphor-1.0.0.dist-info/LICENSE.txtPK!HnHTUr'gaphor-1.0.0.dist-info/WHEELPK!H6-Ҁ:>'gaphor-1.0.0.dist-info/METADATAPK!HQFD3o'gaphor-1.0.0.dist-info/RECORDPKBBb<(