summaryrefslogtreecommitdiff
path: root/cad/src/experimental/demoapp_0.1/demoapp/tools/TrivalentGraphDrawingTool.py
blob: aac308de54dfb2e0f17accad238c989efbde0482 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
"""
TrivalentGraphDrawingTool.py - tool for drawing new nodes and edges into a trivalent graph

$Id$

bugs, fri eve 080613:
- sometimes app seemingly can get stuck; not seen recently
- might be fixed:
  can't drag an existing node i click in when no edge will be drawn by that click
  - not sure i like the lack of drag of node i connect edge to, either;
    but at least, if i click and it doesn't connect and has no rubberedge, i should be able to drag.
- lack of tip when it starts or just does an action is inconsistent;
  otoh the tip we want then might be better in a fixed bottom place
  than next to the cursor. plus we need prefs to turn off "empty space tips" and/or other tips.
  need to classify tips, then have pref for each kind, off or bottom or cursor.
  - mitigated by removing emoty space tips since rubber objs are sufficient
- should prohibit recconnecting two nodes that are already directly connected
  (ie making new edge identical to old one)
- since latest chg, has visual glitches (briefly seen tips that are now mostly gone)

sat aft:
- mouse leave doesn't stop showing the gray rubber node -- i guess we need to bind that event directly
- "drag this node" is wrong, you can also add an edge to it, or both... so fix tip,
  also fix behavior so if you do drag it, you're done, i.e. "drag it *or* add edge to it". tip: "drag or add edge".
- yellow *around* dragnode is distracting, should change it to *be* yellow.
- should make all these lines thickness 2


nfr:
- drag one node over another -> merge nodes (if let go)
- node cmenu command to dissolve node, merging edges, when node has exactly 2 edges
- want other tools, e.g. one for only making nodes, only dragging, only making edges, etc

"""

from pyglet.event import EVENT_HANDLED

from demoapp.foundation.MouseBehavior import Tool, CMD_RETVAL, ToolStateBehavior, Transition ##, SAME_STATE

from demoapp.tools.HuntForClickAction_ToolStateBehavior import HuntForClickAction_ToolStateBehavior
from demoapp.tools.HuntForReleaseAction_ToolStateBehavior import HuntForReleaseAction_ToolStateBehavior

from demoapp.geometry.vectors import A, V

from demoapp.models.TrivalentGraph_Graphics import TrivalentGraph_HighlightGraphics

# ==

class HuntForNode(HuntForClickAction_ToolStateBehavior): # rename: HuntForNodeFromNode?
    """
    Mouse motion draws out a new edge from a given node (if that's not None),
    and looks for places where clicks could create new nodes (almost anywhere).
    """
    # instance variables
    # (in superclass: _tip_and_highlight_info)
    node = None
    _ever_moved_off_self_node = False

    def __init__(self, tool, node):
        super(HuntForNode, self).__init__(tool)
        if node and not node.new_edge_ok():
            node = None
        self.node = node # last-clicked node, start of rubber edge to a new node

    def on_mouse_press_would_do(self, x, y, button, modifiers):
        """
        Click makes new node or finds existing one,
        and makes an edge to it from the last-clicked or last-made node if any.
        """
        model = self.model
        obj = self.pane.find_object(x, y) # might be None
        if self.node and obj is not self.node:
            self._ever_moved_off_self_node = True
        hh = self.tool._f_HighlightGraphics_descriptions
        if self.node and obj is self.node:
            if self._ever_moved_off_self_node:
                return Transition(
                    indicators = [hh.tip_text("click to stop drawing edges", obj), #e clarify text
                                  hh.highlight_refusal(obj)
                                  ],
                    command = None,
                    next_state = (HuntForNode, None)
                 )
            else:
                return Transition(
                    indicators = None,
                    command = None,
                    next_state = (HuntForNode, None)
                 )
        elif isinstance(obj, model.Node):
            # click on another node: connect our node to it, if both nodes permit
            if self.node and obj.new_edge_ok_to(self.node):
                return Transition(
                    indicators = [hh.tip_text("click to connect to this node", obj),
                                  hh.highlight_connect_to(obj),
                                  hh.rubber_edge(self.node, obj)
                                  ],
                    command = ("cmd_addEdge", obj, self.node),
                    next_state = (HuntForNode, obj)
                        ### TODO: sometimes, pass to HuntForNode None instead of obj,
                        # in order to "stop drawing edges" after making this connection,
                        # e.g. if it "closed a loop".
                 )
            elif self.node and obj.connected_to(self.node):
                return Transition(
                    indicators = [hh.tip_text("(already connected to this node)", obj),
                                  hh.highlight_refusal(obj)
                                  ],
                    command = None,
                    next_state = (DragNode, obj, (x,y))
                 )
            elif self.node:
                return Transition(
                    indicators = [hh.tip_text("(can't connect to this node)", obj),
                                  hh.highlight_refusal(obj)
                                  ],
                    command = None,
                    next_state = (DragNode, obj, (x,y))
                 )
            elif obj.new_edge_ok():
                return Transition(
                    indicators = [hh.tip_text("drag this node, or click to connect it to other nodes", obj),
                                  hh.highlight_drag(obj)
                                  ],
                    command = None,
                    next_state = (DragNode, obj, (x,y))
                 )
            else:
                return Transition(
                    indicators = [hh.tip_text("drag this node", obj),
                                  hh.highlight_drag(obj)
                                  ],
                    command = None,
                    next_state = (DragNode, obj, (x,y))
                 )
        elif obj is None:
            # click in empty space [todo: make sure not too near an existing node];
            # create a new node here [todo: depends on type of background object]
            # and maybe connect to it.
            if self.node: # todo: and not too close to any existing node
                return Transition(
                    indicators = [ # tip_text is not needed due to rubber objects:
                                   ## hh.tip_text("click to create new node and connect to it", (x,y)),
                                   hh.rubber_edge(self.node, (x,y)),
                                   hh.rubber_node((x,y))
                                   ],
                    command = ("cmd_addNodeAndConnectFrom", x, y, self.node), # todo: model coords
                    next_state = (DragNode, CMD_RETVAL, (x,y))
                 )
            else:
                return Transition(
                    indicators = [ # tip_text is not needed due to rubber objects:
                                   ## hh.tip_text("click to create new node (and start drawing edges)", (x,y)),
                                   hh.rubber_node((x,y))
                                   ],
                    command = ("cmd_addNode", x, y),
                    next_state = (DragNode, CMD_RETVAL, (x,y))
                 )
        elif isinstance(obj, model.Edge):
            # click on edge: make a node there, then act as if we clicked on it
            if self.node and self.node not in obj.nodes:
                return Transition(
                    indicators = [hh.tip_text("click to insert new node into this edge, and connect to it", (x,y)),
                                  hh.highlight_insert_into(obj),
                                  hh.rubber_node((x,y)),
                                  hh.rubber_edge(self.node, (x,y)),
                                      # fix: not quite correct position
                                      # (we want same corrected pos for tip and both highlights),
                                      # and doesn't indicate change to old edge
                                  ],
                    command = ("cmd_addNodeOnEdgeAndConnectFrom", x, y, obj, self.node),
                    next_state = (DragNode, CMD_RETVAL, (x,y))
                 )
            elif not self.node:
                return Transition(
                    indicators = [hh.tip_text("click to insert new node into this edge", (x,y)),
                                  hh.highlight_insert_into(obj),
                                  hh.rubber_node((x,y))
                                      # fix: not quite correct position (same as in above case)
                                  ],
                    command = ("cmd_addNodeOnEdge", x, y, obj),
                    next_state = (DragNode, CMD_RETVAL, (x,y))
                 )
            else:
                # self.node is one of the endpoints of this edge (obj)
                return None
        return None # for anything we don't understand, do nothing and use the handler below us...
            # (e.g. this lets us highlight and click buttons even if they are "behind" us,
            #  in the general case -- but not in current code, since even empty space has an action
            #  for us. For this state's code the buttons had better be in front or in another pane.)
    pass

# ==

class DragNode(HuntForReleaseAction_ToolStateBehavior): # works, except for bugs in merging operation (doesn't delete enough edges)
    def __init__(self, tool, node, dragstart_pos):
        super(DragNode, self).__init__(tool)
        self.node = node
        # todo: assert isinstance(node, Node), "DragNode needs a Node, not %r" % (node,)
        # for now:
        assert node, "DragNode needs a Node, not %r" % (node,) # only checks existence, not class
        ### ISSUE: are node.pos and dragstart_pos in same coords? probably not. need model to pane, vice versa. #####
        self.dragstart_pos = A(dragstart_pos)
        self.node_offset = node.pos - self.dragstart_pos

    def on_mouse_release_would_do(self, x, y, button, modifiers):
        model = self.model
        obj = self.pane.find_object(x, y,
                                    excluding = [self.node] + list(self.node.edges)
                                    ) # might be None
        hh = self.tool._f_HighlightGraphics_descriptions
        if isinstance(obj, model.Node):
            return Transition(
                indicators = [hh.tip_text("merge with existing node", obj),
                              hh.highlight_merge(self.node, obj),
                              ],
                command = ("cmd_MergeNodes", self.node, obj),
                next_state = (HuntForNode, None)
             )
        elif V(x, y) != self.dragstart_pos:
            # it moved; don't draw edges from it during subsequent mouse motion
            # (future: small motions should not count, for this or for dragging;
            #  could be done here, or by caller not sending drag events at first)
            return Transition( None, None, (HuntForNode, None) )
        else:
            # it didn't move; do perhaps draw edges from it during subsequent motion
            return Transition( None, None, (HuntForNode, self.node) )
                #### review: should this state DragNode be "called" and at this point "return",
                # rather than being jumped to and hardcoding what to jump back out to?
                # if so, how to pass None or self.node?

    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
        "[extends superclass version]"
        self.node.pos = self.node_offset + V(x, y)
        res = super(DragNode, self).on_mouse_drag(x, y, dx, dy, buttons, modifiers)
            # superclass method handles tip & highlighting specific to this potential release-point
        return res

    def on_mouse_release(self, x, y, button, modifiers):
        "[extends superclass version]"
        self.node.pos = self.node_offset + V(x, y)
        res = super(DragNode, self).on_mouse_release(x, y, button, modifiers)
        return res

    ### REVIEW: what about on_draw_handle? (to highlight this node to indicate we're dragging it)
    # different look for could drag or is dragging? text tip "dragging"?

    pass

# ==

class TrivalentGraphDrawingTool(Tool):
    _default_state = (HuntForNode, None)
    HighlightGraphics_class = TrivalentGraph_HighlightGraphics
        # instance ends up in self._f_HighlightGraphics_instance,
        # but this code mostly uses self._f_HighlightGraphics_descriptions instead.
    pass

# end