root/gaphor/tags/gaphor-0.12.5/gaphor/diagram/flow.py
| Revision 2073, 17.0 kB (checked in by wrobe..@pld-linux.org, 1 year ago) | |
|---|---|
| |
| Line | |
|---|---|
| 1 | """ |
| 2 | Control flow and object flow implementation. |
| 3 | |
| 4 | Contains also implementation to split flows using activity edge connectors. |
| 5 | """ |
| 6 | |
| 7 | from math import atan, pi, sin, cos |
| 8 | |
| 9 | from gaphor import UML |
| 10 | from gaphor.diagram.diagramline import NamedLine |
| 11 | from gaphor.diagram.style import ALIGN_LEFT, ALIGN_RIGHT, ALIGN_TOP |
| 12 | |
| 13 | |
| 14 | node_classes = { |
| 15 | UML.ForkNode: UML.JoinNode, |
| 16 | UML.DecisionNode: UML.MergeNode, |
| 17 | UML.JoinNode: UML.ForkNode, |
| 18 | UML.MergeNode: UML.DecisionNode, |
| 19 | } |
| 20 | |
| 21 | |
| 22 | |
| 23 | class FlowItem(NamedLine): |
| 24 | """ |
| 25 | Representation of control flow and object flow. Flow item has name and |
| 26 | guard. It can be splitted into two flows with activity edge connectors. |
| 27 | """ |
| 28 | |
| 29 | __uml__ = UML.ControlFlow |
| 30 | |
| 31 | __style__ = { |
| 32 | 'name-align': (ALIGN_RIGHT, ALIGN_TOP), |
| 33 | 'name-padding': (5, 15, 5, 5), |
| 34 | } |
| 35 | |
| 36 | def __init__(self, id = None): |
| 37 | NamedLine.__init__(self, id) |
| 38 | self._guard = self.add_text('guard.value', editable=True) |
| 39 | |
| 40 | def postload(self): |
| 41 | if self.subject and self.subject.guard: |
| 42 | self._guard.text = self.subject.guard.value |
| 43 | super(FlowItem, self).postload() |
| 44 | |
| 45 | def on_subject_notify(self, pspec, notifiers = ()): |
| 46 | NamedLine.on_subject_notify(self, pspec, |
| 47 | ('guard', 'guard.value',) + notifiers) |
| 48 | if self.subject: |
| 49 | self.on_subject_notify__guard(self.subject) |
| 50 | self.on_subject_notify__guard_value(self.subject) |
| 51 | self.request_update() |
| 52 | |
| 53 | |
| 54 | def on_subject_notify__guard(self, subject, pspec=None): |
| 55 | if not subject.guard or not subject.guard.value: |
| 56 | self._guard.text = '' |
| 57 | self.request_update() |
| 58 | |
| 59 | |
| 60 | def on_subject_notify__guard_value(self, subject, pspec=None): |
| 61 | self.request_update() |
| 62 | |
| 63 | |
| 64 | def set_guard(self, value): |
| 65 | self.subject.guard.value = value |
| 66 | self._guard.text = value |
| 67 | |
| 68 | |
| 69 | def draw_tail(self, context): |
| 70 | cr = context.cairo |
| 71 | cr.line_to(0, 0) |
| 72 | cr.stroke() |
| 73 | cr.move_to(15, -6) |
| 74 | cr.line_to(0, 0) |
| 75 | cr.line_to(15, 6) |
| 76 | |
| 77 | |
| 78 | #class ACItem(TextElement): |
| 79 | class ACItem(object): |
| 80 | """ |
| 81 | Activity edge connector. It is a circle with name inside. |
| 82 | """ |
| 83 | |
| 84 | RADIUS = 10 |
| 85 | def __init__(self, id): |
| 86 | #TextElement.__init__(self, id) |
| 87 | #self._circle = diacanvas.shape.Ellipse() |
| 88 | #self._circle.set_line_width(2.0) |
| 89 | #self._circle.set_fill_color(diacanvas.color(255, 255, 255)) |
| 90 | #self._circle.set_fill(diacanvas.shape.FILL_SOLID) |
| 91 | #self.show_border = False |
| 92 | |
| 93 | # set new value notification function to change activity edge |
| 94 | # connector name globally |
| 95 | vnf = self.on_subject_notify__value |
| 96 | def f(subject, pspec): |
| 97 | vnf(subject, pspec) |
| 98 | if self.parent._opposite: |
| 99 | self.parent._opposite._connector.subject.value = subject.value |
| 100 | self.on_subject_notify__value = f |
| 101 | |
| 102 | |
| 103 | def move_center(self, x, y): |
| 104 | """ |
| 105 | Move center of item to point (x, y). Other parts of item are |
| 106 | aligned to this point. |
| 107 | """ |
| 108 | a = self.props.affine |
| 109 | x -= self.RADIUS |
| 110 | y -= self.RADIUS |
| 111 | self.props.affine = (a[0], a[1], a[2], a[3], x, y) |
| 112 | |
| 113 | |
| 114 | def on_update(self, affine): |
| 115 | """ |
| 116 | Center name of activity edge connector and put a circle around it. |
| 117 | """ |
| 118 | r = self.RADIUS * 2 |
| 119 | x = self.RADIUS |
| 120 | y = self.RADIUS |
| 121 | |
| 122 | self._circle.ellipse(center = (x, y), width = r, height = r) |
| 123 | |
| 124 | # get label size and move it so it is centered with circle |
| 125 | w, h = self.get_size() |
| 126 | x, y = x - w / 2, y - h / 2 |
| 127 | self._name.set_pos((x, y)) |
| 128 | self._name_bounds = (x, y, x + w, y + h) |
| 129 | |
| 130 | TextElement.on_update(self, affine) |
| 131 | |
| 132 | self.set_bounds((-1, -1, r + 1, r + 1)) |
| 133 | |
| 134 | |
| 135 | #class CFlowItem(FlowItem): |
| 136 | # """ |
| 137 | # Abstract class for flows with activity edge connector. Flow with |
| 138 | # activity edge connector references other one, which has activity edge |
| 139 | # connector with same name (it is called opposite one). |
| 140 | # |
| 141 | # Such flows have active and inactive ends. Active end is connected to |
| 142 | # any node and inactive end is connected only to activity edge connector. |
| 143 | # """ |
| 144 | # |
| 145 | # def __init__(self, id = None): |
| 146 | # FlowItem.__init__(self, id) |
| 147 | # |
| 148 | # self._connector = ACItem('value') |
| 149 | # |
| 150 | # factory = resource(UML.ElementFactory) |
| 151 | # self._connector.subject = factory.create(UML.LiteralSpecification) |
| 152 | # self.add(self._connector) |
| 153 | # |
| 154 | # self._opposite = None |
| 155 | # |
| 156 | # # when flow item with connector is deleted, then kill opposite, too |
| 157 | # self.unlink_handler_id = self.connect('__unlink__', self.kill_opposite) |
| 158 | # |
| 159 | # |
| 160 | # def kill_opposite(self, source, name): |
| 161 | # # do not allow to be killed by opposite |
| 162 | # self._opposite.disconnect(self._opposite.unlink_handler_id) |
| 163 | # self._opposite.unlink() |
| 164 | # |
| 165 | # |
| 166 | # def save(self, save_func): |
| 167 | # """ |
| 168 | # Save connector name and opposite flow with activity edge connector. |
| 169 | # """ |
| 170 | # FlowItem.save(self, save_func) |
| 171 | # save_func('opposite', self._opposite, True) |
| 172 | # save_func('connector-name', self._connector.subject.value) |
| 173 | # |
| 174 | # |
| 175 | # def load(self, name, value): |
| 176 | # """ |
| 177 | # Load connector name and opposite flow with activity edge connector. |
| 178 | # """ |
| 179 | # if name == 'connector-name': |
| 180 | # self._connector.subject.value = value |
| 181 | # elif name == 'opposite': |
| 182 | # self._opposite = value |
| 183 | # else: |
| 184 | # FlowItem.load(self, name, value) |
| 185 | # |
| 186 | # |
| 187 | # def on_update(self, affine): |
| 188 | # """ |
| 189 | # Draw flow line and activity edge connector. |
| 190 | # """ |
| 191 | # # get parent line points to determine angle |
| 192 | # # used to rotate position of activity edge connector |
| 193 | # p1, p2 = self.get_line() |
| 194 | # |
| 195 | # # calculate position of connector center |
| 196 | # r = self._connector.RADIUS |
| 197 | # #x = p1[0] < p2[0] and r or -r |
| 198 | # x = p1[0] < p2[0] and -r or r |
| 199 | # y = 0 |
| 200 | # x, y = rotate(p1, p2, x, y, p1[0], p1[1]) |
| 201 | # |
| 202 | # self._connector.move_center(x, y) |
| 203 | # |
| 204 | # FlowItem.on_update(self, affine) |
| 205 | # |
| 206 | # |
| 207 | # def confirm_connect_handle(self, handle): |
| 208 | # """See NamedLine.confirm_connect_handle(). |
| 209 | # """ |
| 210 | # c1 = self.get_active_handle().connected_to # source |
| 211 | # c2 = self._opposite.get_active_handle().connected_to # target |
| 212 | # |
| 213 | # # set correct relationship between connected items; |
| 214 | # # it should be (source, target) not (target, source); |
| 215 | # # otherwise we are looking for non-existing or wrong relationship |
| 216 | # if isinstance(self, CFlowItemB): |
| 217 | # c1, c2 = c2, c1 |
| 218 | # |
| 219 | # self.connect_items(c1, c2) |
| 220 | # self._opposite.set_subject(self.subject) |
| 221 | # |
| 222 | # |
| 223 | # def allow_connect_handle(self, handle, connecting_to): |
| 224 | # if handle == self.get_inactive_handle(): |
| 225 | # return False |
| 226 | # return FlowItem.allow_connect_handle(self, handle, connecting_to) |
| 227 | # |
| 228 | # |
| 229 | # def confirm_disconnect_handle (self, handle, was_connected_to): |
| 230 | # """See NamedLine.confirm_disconnect_handle(). |
| 231 | # """ |
| 232 | # c1 = self.get_active_handle().connected_to # source |
| 233 | # c2 = self._opposite.get_active_handle().connected_to # target |
| 234 | # self.disconnect_items(c1, c2, was_connected_to) |
| 235 | # self._opposite.set_subject(None) |
| 236 | # |
| 237 | # |
| 238 | # |
| 239 | #class CFlowItemA(CFlowItem): |
| 240 | # """ |
| 241 | # * Is used for split flows, as is CFlowItemB * |
| 242 | # |
| 243 | # Flow with activity edge connector, which starts from node and points to |
| 244 | # activity edge connector. |
| 245 | # """ |
| 246 | # def __init__(self, id): |
| 247 | # CFlowItem.__init__(self, id) |
| 248 | # self.create_guard() |
| 249 | # |
| 250 | # |
| 251 | # def on_update(self, affine): |
| 252 | # self.update_guard(affine) |
| 253 | # CFlowItem.on_update(self, affine) |
| 254 | # |
| 255 | # |
| 256 | # def get_line(self): |
| 257 | # p1 = self.handles[-1].get_pos_i() |
| 258 | # p2 = self.handles[-2].get_pos_i() |
| 259 | # return p1, p2 |
| 260 | # |
| 261 | # |
| 262 | # def get_active_handle(self): |
| 263 | # """ |
| 264 | # Return source handle as active one. |
| 265 | # """ |
| 266 | # return self.handles[0] |
| 267 | # |
| 268 | # |
| 269 | # def get_inactive_handle(self): |
| 270 | # """ |
| 271 | # Return target handle as inactive one. |
| 272 | # """ |
| 273 | # return self.handles[-1] |
| 274 | # |
| 275 | # |
| 276 | # |
| 277 | #class CFlowItemB(CFlowItem): |
| 278 | # """ |
| 279 | # Flow with activity edge connector, which starts from activity edge |
| 280 | # connector and points to a node. |
| 281 | # """ |
| 282 | # def __init__(self, id): |
| 283 | # CFlowItem.__init__(self, id) |
| 284 | # self.create_name() |
| 285 | # |
| 286 | # |
| 287 | # def on_update(self, affine): |
| 288 | # self.update_name(affine) |
| 289 | # CFlowItem.on_update(self, affine) |
| 290 | # |
| 291 | # |
| 292 | # def get_line(self): |
| 293 | # p1 = self.handles[0].get_pos_i() |
| 294 | # p2 = self.handles[1].get_pos_i() |
| 295 | # return p1, p2 |
| 296 | # |
| 297 | # |
| 298 | # def get_active_handle(self): |
| 299 | # """ |
| 300 | # Return target handle as active one. |
| 301 | # """ |
| 302 | # return self.handles[-1] |
| 303 | # |
| 304 | # |
| 305 | # def get_inactive_handle(self): |
| 306 | # """ |
| 307 | # Return source handle as inactive one. |
| 308 | # """ |
| 309 | # return self.handles[0] |
| 310 | # |
| 311 | # |
| 312 | #def move_collection(src, target, name): |
| 313 | # """ |
| 314 | # Copy collection from one object to another. |
| 315 | # |
| 316 | # src - source object |
| 317 | # target - target object |
| 318 | # name - name of attribute, which is collection to copy |
| 319 | # """ |
| 320 | # # first make of copy of collection, because assigning |
| 321 | # # element to target collection moves this element |
| 322 | # for flow in list(getattr(src, name)): |
| 323 | # getattr(target, name).append(flow) |
| 324 | # |
| 325 | |
| 326 | #def is_fd(node): |
| 327 | # """ |
| 328 | # Check if node is fork or decision node. |
| 329 | # """ |
| 330 | # return isinstance(node, (UML.ForkNode, UML.DecisionNode)) |
| 331 | # |
| 332 | # |
| 333 | #def change_node_class(node): |
| 334 | # """ |
| 335 | # If UML constraints for fork, join, decision and merge nodes are not |
| 336 | # met, then create new node depending on input node class, i.e. create |
| 337 | # fork node from join node or merge node from decision node. |
| 338 | # |
| 339 | # If constraints are met, then return node itself. |
| 340 | # """ |
| 341 | # if is_fd(node) and len(node.incoming) > 1 \ |
| 342 | # or not is_fd(node) and len(node.incoming) < 2: |
| 343 | # |
| 344 | # factory = resource(UML.ElementFactory) |
| 345 | # cls = node_classes[node.__class__] |
| 346 | # log.debug('creating %s' % cls) |
| 347 | # nn = factory.create(cls) |
| 348 | # move_collection(node, nn, 'incoming') |
| 349 | # move_collection(node, nn, 'outgoing') |
| 350 | # else: |
| 351 | # nn = node |
| 352 | # |
| 353 | # assert nn is not None |
| 354 | # |
| 355 | # # we have to accept zero of outgoing edges in case of fork/descision |
| 356 | # # nodes |
| 357 | # assert is_fd(nn) and len(nn.incoming) <= 1 \ |
| 358 | # or not is_fd(nn) and len(nn.incoming) >= 1, '%s' % nn |
| 359 | # assert is_fd(nn) and len(nn.outgoing) >= 0 \ |
| 360 | # or not is_fd(nn) and len(nn.outgoing) <= 1, '%s' % nn |
| 361 | # return nn |
| 362 | # |
| 363 | # |
| 364 | #def combine_nodes(node): |
| 365 | # """ |
| 366 | # Create fork/join (decision/merge) nodes combination as described in UML |
| 367 | # specification. |
| 368 | # """ |
| 369 | # log.debug('combining nodes') |
| 370 | # |
| 371 | # cls = node_classes[node.__class__] |
| 372 | # log.debug('creating %s' % cls) |
| 373 | # factory = resource(UML.ElementFactory) |
| 374 | # target = factory.create(cls) |
| 375 | # |
| 376 | # source = node |
| 377 | # if is_fd(node): |
| 378 | # source = target |
| 379 | # move_collection(node, target, 'incoming') |
| 380 | # |
| 381 | # # create new fork node |
| 382 | # cls = node_classes[target.__class__] |
| 383 | # log.debug('creating %s' % cls) |
| 384 | # target = factory.create(cls) |
| 385 | # move_collection(node, target, 'outgoing') |
| 386 | # else: |
| 387 | # # fork node is created, referenced by target |
| 388 | # move_collection(node, target, 'outgoing') |
| 389 | # |
| 390 | # assert not is_fd(source) |
| 391 | # assert is_fd(target) |
| 392 | # |
| 393 | # # create flow |
| 394 | # c1 = count_object_flows(source, 'incoming') |
| 395 | # c2 = count_object_flows(target, 'outgoing') |
| 396 | # |
| 397 | # if c1 > 0 or c2 > 0: |
| 398 | # flow = factory.create(UML.ControlFlow) |
| 399 | # else: |
| 400 | # flow = factory.create(UML.ObjectFlow) |
| 401 | # flow.source = source |
| 402 | # flow.target = target |
| 403 | # |
| 404 | # assert len(source.incoming) > 1 |
| 405 | # assert len(source.outgoing) == 1 |
| 406 | # |
| 407 | # assert len(target.incoming) == 1 |
| 408 | # assert len(target.outgoing) > 1 |
| 409 | # |
| 410 | # return source |
| 411 | # |
| 412 | # |
| 413 | #def decombine_nodes(source): |
| 414 | # """ |
| 415 | # Create node depending on source argument which denotes combination of |
| 416 | # fork/join (decision/merge) nodes as described in UML specification. |
| 417 | # |
| 418 | # Combination of nodes is destroyed. |
| 419 | # """ |
| 420 | # log.debug('decombining nodes') |
| 421 | # flow = source.outgoing[0] |
| 422 | # target = flow.target |
| 423 | # |
| 424 | # if len(source.incoming) < 2: |
| 425 | # # create fork or decision |
| 426 | # cls = target.__class__ |
| 427 | # else: |
| 428 | # # create join or merge |
| 429 | # cls = source.__class__ |
| 430 | # |
| 431 | # factory = resource(UML.ElementFactory) |
| 432 | # node = factory.create(cls) |
| 433 | # |
| 434 | # move_collection(source, node, 'incoming') |
| 435 | # move_collection(target, node, 'outgoing') |
| 436 | # |
| 437 | # assert source != node |
| 438 | # |
| 439 | # # delete target and combining flow |
| 440 | # # source should be deleted by caller |
| 441 | # target.unlink() |
| 442 | # flow.unlink() |
| 443 | # |
| 444 | # # return new node |
| 445 | # return node |
| 446 | |
| 447 | |
| 448 | #def determine_node_on_connect(el): |
| 449 | # """ |
| 450 | # Determine classes of nodes depending on amount of incoming |
| 451 | # and outgoing edges. This method is called when flow is attached |
| 452 | # to node. |
| 453 | # |
| 454 | # If there is more than one incoming edge and more than one |
| 455 | # outgoing edge, then create two nodes and combine them with |
| 456 | # flow as described in UML specification. |
| 457 | # """ |
| 458 | # subject = el.subject |
| 459 | # if not isinstance(subject, tuple(node_classes.keys())): |
| 460 | # return |
| 461 | # |
| 462 | # new_subject = subject |
| 463 | # |
| 464 | # if len(subject.incoming) > 1 and len(subject.outgoing) > 1: |
| 465 | # new_subject = combine_nodes(subject) |
| 466 | # el.props.combined = True |
| 467 | # |
| 468 | # else: |
| 469 | # new_subject = change_node_class(subject) |
| 470 | # |
| 471 | # change_node_subject(el, new_subject) |
| 472 | # |
| 473 | # if el.props.combined: |
| 474 | # check_combining_flow(el) |
| 475 | |
| 476 | |
| 477 | #def determine_node_on_disconnect(el): |
| 478 | # """ |
| 479 | # Determine classes of nodes depending on amount of incoming |
| 480 | # and outgoing edges. This method is called when flow is dettached |
| 481 | # from node. |
| 482 | # |
| 483 | # If there are combined nodes and there is no need for them, then replace |
| 484 | # combination with appropriate node (i.e. replace with fork node when |
| 485 | # there are less than two incoming edges). This way data model is kept as |
| 486 | # simple as possible. |
| 487 | # """ |
| 488 | # subject = el.subject |
| 489 | # if not isinstance(subject, tuple(node_classes.keys())): |
| 490 | # return |
| 491 | # |
| 492 | # new_subject = subject |
| 493 | # |
| 494 | # if el.props.combined: |
| 495 | # cs = subject.outgoing[0].target |
| 496 | # # decombine node when there is no more than one incoming |
| 497 | # # and no more than one outgoing flow |
| 498 | # if len(subject.incoming) < 2 or len(cs.outgoing) < 2: |
| 499 | # new_subject = decombine_nodes(subject) |
| 500 | # el.props.combined = False |
| 501 | # else: |
| 502 | # check_combining_flow(el) |
| 503 | # |
| 504 | # else: |
| 505 | # new_subject = change_node_class(subject) |
| 506 | # |
| 507 | # change_node_subject(el, new_subject) |
| 508 | |
| 509 | |
| 510 | #def change_node_subject(el, new_subject): |
| 511 | # """ |
| 512 | # Change element's subject if new subject is different than element's |
| 513 | # subject. If subject is changed, then old subject is destroyed. |
| 514 | # """ |
| 515 | # subject = el.subject |
| 516 | # if new_subject != subject: |
| 517 | # log.debug('changing subject of ui node %s' % el) |
| 518 | # el.set_subject(new_subject) |
| 519 | # |
| 520 | # log.debug('deleting node %s' % subject) |
| 521 | # subject.unlink() |
| 522 | |
| 523 | |
| 524 | #def create_flow(cls, flow): |
| 525 | # """ |
| 526 | # Create new flow of class cls. Flow data from flow argument are copied |
| 527 | # to new created flow. Old flow is destroyed. |
| 528 | # """ |
| 529 | # factory = resource(UML.ElementFactory) |
| 530 | # f = factory.create(cls) |
| 531 | # f.source = flow.source |
| 532 | # f.target = flow.target |
| 533 | # flow.unlink() |
| 534 | # return f |
| 535 | |
| 536 | |
| 537 | #def count_object_flows(node, attr): |
| 538 | # """ |
| 539 | # Count incoming or outgoing object flows. |
| 540 | # """ |
| 541 | # return len(getattr(node, attr) |
| 542 | # .select(lambda flow: isinstance(flow, UML.ObjectFlow))) |
| 543 | # |
| 544 | # |
| 545 | #def check_combining_flow(el): |
| 546 | # """ |
| 547 | # Set object flow as combining flow when incoming or outgoing flow count |
| 548 | # is greater than zero. Otherwise change combining flow to control flow. |
| 549 | # """ |
| 550 | # subject = el.subject |
| 551 | # flow = subject.outgoing[0] # combining flow |
| 552 | # combined = flow.target # combined node |
| 553 | # |
| 554 | # c1 = count_object_flows(subject, 'incoming') |
| 555 | # c2 = count_object_flows(combined, 'outgoing') |
| 556 | # |
| 557 | # log.debug('combined incoming and outgoing object flow count: (%d, %d)' % (c1, c2)) |
| 558 | # |
| 559 | # if (c1 > 0 or c2 > 0) and isinstance(flow, UML.ControlFlow): |
| 560 | # log.debug('changing combing flow to object flow') |
| 561 | # create_flow(UML.ObjectFlow, flow) |
| 562 | # elif c1 == 0 and c2 == 0 and isinstance(flow, UML.ObjectFlow): |
| 563 | # log.debug('changing combing flow to control flow') |
| 564 | # create_flow(UML.ControlFlow, flow) |
| 565 | # |
| 566 | |
| 567 | #def create_connector_end(connector, role): |
| 568 | # """ |
| 569 | # Create Connector End, set role and attach created end to |
| 570 | # connector. |
| 571 | # """ |
| 572 | # end = resource(UML.ElementFactory).create(UML.ConnectorEnd) |
| 573 | # end.role = role |
| 574 | # connector.end = end |
| 575 | # assert end in role.end |
| 576 | # return end |
| 577 | # |
| 578 | |
| 579 | #def rotate(p1, p2, a, b, x, y): |
| 580 | # """ |
| 581 | # Rotate point (a, b) by angle, which is determined by line (p1, p2). |
| 582 | # |
| 583 | # Rotated point is moved by vector (x, y). |
| 584 | # """ |
| 585 | # try: |
| 586 | # angle = atan((p1[1] - p2[1]) / (p1[0] - p2[0])) |
| 587 | # except ZeroDivisionError: |
| 588 | # da = p1[1] < p2[1] and 1.5 or -1.5 |
| 589 | # angle = pi * da |
| 590 | # |
| 591 | # sin_angle = sin(angle) |
| 592 | # cos_angle = cos(angle) |
| 593 | # return (cos_angle * a - sin_angle * b + x, |
| 594 | # sin_angle * a + cos_angle * b + y) |
| 595 | |
| 596 | # vim:sw=4:et:ai |
Note: See TracBrowser for help on using the browser.
