root/gaphor/tags/gaphor-0.12.0/gaphor/ui/diagramtools.py

Revision 2106, 13.1 kB (checked in by arj..@yirdis.nl, 1 year ago)

added frame around text edit window (thanks artur!)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 Tools for handling items on the canvas.
3
4 Although Gaphas has quite a few useful tools, some tools need to be extended:
5  - PlacementTool: should perform undo
6  - HandleTool: shoudl support adapter based connection protocol
7  - TextEditTool: should support adapter based edit protocol
8 """
9
10 import gtk
11 from cairo import Matrix
12 from zope import component
13
14 from gaphas.geometry import distance_point_point, distance_point_point_fast, \
15                             distance_line_point, distance_rectangle_point
16 from gaphas.item import Line
17 from gaphas.tool import Tool, HandleTool, PlacementTool as _PlacementTool
18 from gaphas.tool import ToolChain, HoverTool, ItemTool, RubberbandTool
19
20
21
22 from gaphor.core import inject, Transaction, transactional
23
24 from gaphor.diagram.interfaces import IEditor, IConnect
25
26 __version__ = '$Revision$'
27
28
29 class ConnectHandleTool(HandleTool):
30     """
31     Handle Tool (acts on item handles) that uses the IConnect protocol
32     to connect items to one-another.
33
34     It also adds handles to lines when a line is grabbed on the middle of
35     a line segment (points are drawn by the LineSegmentPainter).
36
37     Attributes:
38      - _adapter: current adapter used to connect items
39     """
40     GLUE_DISTANCE = 10
41
42     def __init__(self):
43         super(ConnectHandleTool, self).__init__()
44         self._adapter = None
45
46
47     def glue(self, view, item, handle, vx, vy):
48         """
49         Find the nearest item that the handle may connect to.
50
51         This is done by checking for an IConnect adapter for all items in the
52         proximity of ``(vx, xy)``.  If such an adapter exists, the glue
53         position is determined. The item with the glue point closest to the
54         handle will be glued to.
55
56         view: The view
57         item: The item who's about to connect, owner of handle
58         handle: the handle to connect
59         vx, vy: handle position in view coordinates
60         """
61         # localize methods
62         v2i = view.get_matrix_v2i
63         i2v = view.get_matrix_i2v
64         drp = distance_rectangle_point
65         get_item_bounding_box = view.get_item_bounding_box
66         query_adapter = component.queryMultiAdapter
67
68         dist = self.GLUE_DISTANCE
69         max_dist = dist
70         glue_pos = (0, 0)
71         glue_item = None
72         for i in view.get_items_in_rectangle((vx - dist, vy - dist,
73                                               dist * 2, dist * 2),
74                                              reverse=True):
75             if i is item:
76                 continue
77            
78             b = get_item_bounding_box(i)
79             ix, iy = v2i(i).transform_point(vx, vy)
80             if drp(b, (vx, vy)) >= max_dist:
81                 continue
82            
83             adapter = query_adapter((i, item), IConnect)
84             if adapter:
85                 pos = adapter.glue(handle)
86                 self._adapter = adapter
87                 if pos:
88                     d = i.point(ix, iy)
89                     if d <= dist:
90                         dist = d
91                         glue_pos = pos
92                         glue_item = i
93
94         if dist < max_dist:
95             handle.pos = glue_pos
96
97         # Return the glued item, this can be used by connect() to
98         # determine which item it should connect to
99         return glue_item
100
101
102     def connect(self, view, item, handle, vx, vy):
103         """
104         Find an item near ``handle`` that ``item`` can connect to and connect.
105         
106         This is done by attempting a glue() operation. If there is something
107         to glue (the handles are already positioned), the IConnect.connect
108         is called for (glued_item, item).
109         """
110         connected = False
111         try:
112             glue_item = self.glue(view, item, handle, vx, vy)
113
114             if glue_item:
115                 assert handle in self._adapter.line.handles()
116                 self._adapter.connect(handle)
117
118                 connected = True
119             elif handle and handle.connected_to:
120                 handle.disconnect()
121
122         finally:
123             self._adapter = None
124
125
126         return connected
127
128     def disconnect(self, view, item, handle):
129         """
130         Disconnect the handle from the element by removing constraints.
131         Do not yet release the connection on model level, since the handle
132         may be connected to the same item on some other place.
133         """
134         if handle.connected_to:
135             adapter = component.queryMultiAdapter((handle.connected_to, item), IConnect)
136             adapter.disconnect_constraints(handle)
137        
138     def on_button_press(self, context, event):
139         """
140         In addition to the normal behavior, the button press event creates
141         new handles if it is activated on the middle of a line segment.
142         """
143         if super(ConnectHandleTool, self).on_button_press(context, event):
144             return True
145
146         view = context.view
147         item = view.hovered_item
148         if item and item is view.focused_item and isinstance(item, Line):
149             handles = item.handles()
150             x, y = context.view.get_matrix_v2i(item).transform_point(event.x, event.y)
151             for h1, h2 in zip(handles[:-1], handles[1:]):
152                 xp = (h1.x + h2.x) / 2
153                 yp = (h1.y + h2.y) / 2
154                 if distance_point_point_fast((x,y), (xp, yp)) <= 4:
155                     segment = handles.index(h1)
156                     item.split_segment(segment)
157
158                     # Reconnect all constraints:
159                     for i, h in view.canvas.get_connected_items(item):
160                         adapter = component.getMultiAdapter((item, i), IConnect)
161                         adapter.disconnect_constraints(h)
162                         adapter.connect_constraints(h)
163
164                     self.grab_handle(item, item.handles()[segment + 1])
165                     context.grab()
166                     return True
167
168     def on_button_release(self, context, event):
169         grabbed_handle = self._grabbed_handle
170         grabbed_item = self._grabbed_item
171         if super(ConnectHandleTool, self).on_button_release(context, event):
172             if grabbed_handle and grabbed_item:
173                 handles = grabbed_item.handles()
174                 if handles[0] is grabbed_handle or handles[-1] is grabbed_handle:
175                     return True
176                 segment = handles.index(grabbed_handle)
177                 before = handles[segment - 1]
178                 after = handles[segment + 1]
179                 d, p = distance_line_point(before.pos, after.pos, grabbed_handle.pos)
180                 if d < 2:
181                     grabbed_item.merge_segment(segment)
182
183                     # Reconnect all constraints:
184                     for i, h in context.view.canvas.get_connected_items(grabbed_item):
185                         adapter = component.getMultiAdapter((grabbed_item, i), IConnect)
186                         adapter.disconnect_constraints(h)
187                         adapter.connect_constraints(h)
188
189             return True
190
191
192 class TextEditTool(Tool):
193     """
194     Text edit tool. Allows for elements that can adapt to the
195     IEditable interface to be edited.
196     """
197
198     def create_edit_window(self, view, x, y, text, *args):
199         """
200         Create a popup window with some editable text.
201         """
202         window = gtk.Window()
203         window.set_property('decorated', False)
204         window.set_resize_mode(gtk.RESIZE_IMMEDIATE)
205         #window.set_modal(True)
206         window.set_parent_window(view.window)
207         buffer = gtk.TextBuffer()
208         if text:
209             buffer.set_text(text)
210             startiter, enditer = buffer.get_bounds()
211             buffer.move_mark_by_name('selection_bound', startiter)
212             buffer.move_mark_by_name('insert', enditer)
213         text_view = gtk.TextView()
214         text_view.set_buffer(buffer)
215         #text_view.set_border_width(2)
216         text_view.set_left_margin(2)
217         text_view.set_right_margin(2)
218         text_view.show()
219        
220         frame = gtk.Frame()
221         frame.set_shadow_type(gtk.SHADOW_IN)
222         #frame.set_border_width(1)
223         frame.add(text_view)
224         frame.show()
225
226         window.add(frame)
227         #window.set_border_width(1)
228         window.size_allocate(gtk.gdk.Rectangle(int(x), int(y), 50, 50))
229         #window.move(int(x), int(y))
230         cursor_pos = view.get_toplevel().get_screen().get_display().get_pointer()
231         window.move(cursor_pos[1], cursor_pos[2])
232         window.connect('focus-out-event', self._on_focus_out_event,
233                        buffer, *args)
234         text_view.connect('key-press-event', self._on_key_press_event,
235                           buffer, *args)
236         #text_view.set_size_request(50, 50)
237         window.show()
238         #text_view.grab_focus()
239         #window.set_uposition(event.x, event.y)
240         #window.focus
241
242     @transactional
243     def submit_text(self, widget, buffer, editor):
244         """
245         Submit the final text to the edited item.
246         """
247         text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
248         editor.update_text(text)
249         widget.get_toplevel().destroy()
250
251     def on_double_click(self, context, event):
252         view = context.view
253         item = view.hovered_item
254         if item:
255             try:
256                 editor = IEditor(item)
257             except TypeError:
258                 # Could not adapt to IEditor
259                 return False
260             x, y = view.get_matrix_v2i(item).transform_point(event.x, event.y)
261             if editor.is_editable(x, y):
262                 text = editor.get_text()
263                 # get item at cursor
264                 self.create_edit_window(context.view, event.x, event.y,
265                                         text, editor)
266                 return True
267
268     def _on_key_press_event(self, widget, event, buffer, editor):
269         if event.keyval == gtk.keysyms.Return:
270             self.submit_text(widget, buffer, editor)
271         elif event.keyval == gtk.keysyms.Escape:
272             widget.get_toplevel().destroy()
273
274     def _on_focus_out_event(self, widget, event, buffer, editor):
275         self.submit_text(widget, buffer, editor)
276
277
278 class PlacementTool(_PlacementTool):
279     """
280     PlacementTool is used to place items on the canvas.
281     """
282
283     def __init__(self, item_factory, after_handler=None, handle_index=-1):
284         """
285         item_factory is a callable. It is used to create a CanvasItem
286         that is displayed on the diagram.
287         """
288         _PlacementTool.__init__(self, factory=item_factory,
289                                       handle_tool=ConnectHandleTool(),
290                                       handle_index=handle_index)
291         self.after_handler = after_handler
292         self._tx = None
293
294     def on_button_press(self, context, event):
295         self._tx = Transaction()
296         view = context.view
297         view.unselect_all()
298         if _PlacementTool.on_button_press(self, context, event):
299             try:
300                 opposite = self.new_item.opposite(self.new_item.handles()[self._handle_index])
301             except (KeyError, AttributeError):
302                 pass
303             else:
304                 # Connect opposite handle first, using the HandleTool's
305                 # mechanisms
306
307                 # First make sure all matrices are updated:
308                 view.canvas.update_matrix(self.new_item)
309                 view.update_matrix(self.new_item)
310
311                 vx, vy = event.x, event.y
312
313                 item = self.handle_tool.glue(view, self.new_item, opposite, vx, vy)
314                 if item:
315                     self.handle_tool.connect(view, self.new_item, opposite, vx, vy)
316             return True
317         return False
318            
319     def on_button_release(self, context, event):
320         try:
321             if self.after_handler:
322                 self.after_handler()
323             return _PlacementTool.on_button_release(self, context, event)
324         finally:
325             self._tx.commit()
326             self._tx = None
327
328
329
330 class TransactionalToolChain(ToolChain):
331     """
332     In addition to a normal toolchain, this chain begins an undo-transaction
333     at button-press and commits the transaction at button-release.
334     """
335
336     def __init__(self):
337         ToolChain.__init__(self)
338         self._tx = None
339
340     def on_button_press(self, context, event):
341         self._tx = Transaction()
342         return ToolChain.on_button_press(self, context, event)
343
344     def on_button_release(self, context, event):
345         try:
346             return ToolChain.on_button_release(self, context, event)
347         finally:
348             if self._tx:
349                 self._tx.commit()
350                 self._tx = None
351
352     def on_double_click(self, context, event):
353         tx = Transaction()
354         try:
355             return ToolChain.on_double_click(self, context, event)
356         finally:
357             tx.commit()
358
359     def on_triple_click(self, context, event):
360         tx = Transaction()
361         try:
362             return ToolChain.on_triple_click(self, context, event)
363         finally:
364             tx.commit()
365
366
367 def DefaultTool():
368     """
369     The default tool chain build from HoverTool, ItemTool and HandleTool.
370     """
371     from gaphor.ui.groupingtools import GroupItemTool
372
373     chain = TransactionalToolChain()
374     chain.append(HoverTool())
375     chain.append(ConnectHandleTool())
376     chain.append(GroupItemTool())
377     chain.append(TextEditTool())
378     chain.append(RubberbandTool())
379     return chain
380
381
382 # vim:sw=4:et:ai
Note: See TracBrowser for help on using the browser.