| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
import gtk |
|---|
| 5 |
from cairo import Matrix |
|---|
| 6 |
|
|---|
| 7 |
from zope import component |
|---|
| 8 |
from gaphor import UML |
|---|
| 9 |
from gaphor.core import _, inject, transactional, action, build_action_group |
|---|
| 10 |
from gaphor.diagram import get_diagram_item |
|---|
| 11 |
from gaphor.diagram.items import DiagramItem |
|---|
| 12 |
from gaphor.transaction import Transaction |
|---|
| 13 |
from gaphor.ui.diagramview import DiagramView |
|---|
| 14 |
from gaphor.ui.diagramtoolbox import DiagramToolbox |
|---|
| 15 |
from event import DiagramSelectionChange |
|---|
| 16 |
|
|---|
| 17 |
class DiagramTab(object): |
|---|
| 18 |
|
|---|
| 19 |
element_factory = inject('element_factory') |
|---|
| 20 |
action_manager = inject('action_manager') |
|---|
| 21 |
|
|---|
| 22 |
menu_xml = """ |
|---|
| 23 |
<ui> |
|---|
| 24 |
<menubar action="mainwindow"> |
|---|
| 25 |
<menu action="edit"> |
|---|
| 26 |
<menuitem action="diagram-delete" /> |
|---|
| 27 |
<separator /> |
|---|
| 28 |
<menuitem action="diagram-select-all" /> |
|---|
| 29 |
<menuitem action="diagram-unselect-all" /> |
|---|
| 30 |
</menu> |
|---|
| 31 |
<menu action="diagram"> |
|---|
| 32 |
<placeholder name="secondary"> |
|---|
| 33 |
<menuitem action="diagram-zoom-in" /> |
|---|
| 34 |
<menuitem action="diagram-zoom-out" /> |
|---|
| 35 |
<menuitem action="diagram-zoom-100" /> |
|---|
| 36 |
<separator /> |
|---|
| 37 |
<menuitem action="diagram-close" /> |
|---|
| 38 |
</placeholder> |
|---|
| 39 |
</menu> |
|---|
| 40 |
</menubar> |
|---|
| 41 |
<toolbar name='mainwindow-toolbar'> |
|---|
| 42 |
<separator /> |
|---|
| 43 |
<toolitem action="diagram-zoom-in" /> |
|---|
| 44 |
<toolitem action="diagram-zoom-out" /> |
|---|
| 45 |
<toolitem action="diagram-zoom-100" /> |
|---|
| 46 |
</toolbar> |
|---|
| 47 |
</ui> |
|---|
| 48 |
""" |
|---|
| 49 |
|
|---|
| 50 |
def __init__(self, owning_window): |
|---|
| 51 |
self.diagram = None |
|---|
| 52 |
self.view = None |
|---|
| 53 |
self.owning_window = owning_window |
|---|
| 54 |
self.action_group = build_action_group(self) |
|---|
| 55 |
self.toolbox = None |
|---|
| 56 |
|
|---|
| 57 |
title = property(lambda s: s.diagram and s.diagram.name or _('<None>')) |
|---|
| 58 |
|
|---|
| 59 |
def get_diagram(self): |
|---|
| 60 |
return self.diagram |
|---|
| 61 |
|
|---|
| 62 |
def get_view(self): |
|---|
| 63 |
return self.view |
|---|
| 64 |
|
|---|
| 65 |
def get_canvas(self): |
|---|
| 66 |
return self.diagram.canvas |
|---|
| 67 |
|
|---|
| 68 |
def set_diagram(self, diagram): |
|---|
| 69 |
if self.diagram: |
|---|
| 70 |
self.diagram.disconnect(self._on_diagram_event) |
|---|
| 71 |
self.diagram = diagram |
|---|
| 72 |
if diagram: |
|---|
| 73 |
diagram.connect(('name', '__unlink__'), self._on_diagram_event) |
|---|
| 74 |
|
|---|
| 75 |
if self.view: |
|---|
| 76 |
self.view.hadjustment.set_value(0.0) |
|---|
| 77 |
self.view.vadjustment.set_value(0.0) |
|---|
| 78 |
|
|---|
| 79 |
|
|---|
| 80 |
def construct(self): |
|---|
| 81 |
""" |
|---|
| 82 |
Create the widget. |
|---|
| 83 |
|
|---|
| 84 |
Returns: the newly created widget. |
|---|
| 85 |
""" |
|---|
| 86 |
assert self.diagram |
|---|
| 87 |
|
|---|
| 88 |
table = gtk.Table(2,2, False) |
|---|
| 89 |
|
|---|
| 90 |
frame = gtk.Frame() |
|---|
| 91 |
frame.set_shadow_type(gtk.SHADOW_IN) |
|---|
| 92 |
table.attach(frame, 0, 1, 0, 1, |
|---|
| 93 |
gtk.EXPAND | gtk.FILL | gtk.SHRINK, |
|---|
| 94 |
gtk.EXPAND | gtk.FILL | gtk.SHRINK) |
|---|
| 95 |
|
|---|
| 96 |
view = DiagramView(diagram=self.diagram) |
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 |
frame.add(view) |
|---|
| 100 |
|
|---|
| 101 |
sbar = gtk.VScrollbar(view.vadjustment) |
|---|
| 102 |
table.attach(sbar, 1, 2, 0, 1, gtk.FILL, |
|---|
| 103 |
gtk.EXPAND | gtk.FILL | gtk.SHRINK) |
|---|
| 104 |
|
|---|
| 105 |
sbar = gtk.HScrollbar(view.hadjustment) |
|---|
| 106 |
table.attach(sbar, 0, 1, 1, 2, gtk.EXPAND | gtk.FILL | gtk.SHRINK, |
|---|
| 107 |
gtk.FILL) |
|---|
| 108 |
|
|---|
| 109 |
view.connect('focus-changed', self._on_view_selection_changed) |
|---|
| 110 |
view.connect('selection-changed', self._on_view_selection_changed) |
|---|
| 111 |
view.connect_after('key-press-event', self._on_key_press_event) |
|---|
| 112 |
view.connect('drag-data-received', self._on_drag_data_received) |
|---|
| 113 |
|
|---|
| 114 |
self.view = view |
|---|
| 115 |
|
|---|
| 116 |
table.show_all() |
|---|
| 117 |
self.widget = table |
|---|
| 118 |
|
|---|
| 119 |
self.toolbox = DiagramToolbox(view) |
|---|
| 120 |
|
|---|
| 121 |
return table |
|---|
| 122 |
|
|---|
| 123 |
@action(name='diagram-close', stock_id='gtk-close') |
|---|
| 124 |
def close(self): |
|---|
| 125 |
""" |
|---|
| 126 |
Tab is destroyed. Do the same thing that would |
|---|
| 127 |
be done if File->Close was pressed. |
|---|
| 128 |
""" |
|---|
| 129 |
|
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 |
self.owning_window.remove_tab(self) |
|---|
| 133 |
self.set_diagram(None) |
|---|
| 134 |
|
|---|
| 135 |
|
|---|
| 136 |
del self.view |
|---|
| 137 |
del self.diagram |
|---|
| 138 |
|
|---|
| 139 |
@action(name='diagram-zoom-in', stock_id='gtk-zoom-in') |
|---|
| 140 |
def zoom_in(self): |
|---|
| 141 |
self.view.zoom(1.2) |
|---|
| 142 |
|
|---|
| 143 |
@action(name='diagram-zoom-out', stock_id='gtk-zoom-out') |
|---|
| 144 |
def zoom_out(self): |
|---|
| 145 |
self.view.zoom(1 / 1.2) |
|---|
| 146 |
|
|---|
| 147 |
@action(name='diagram-zoom-100', stock_id='gtk-zoom-100') |
|---|
| 148 |
def zoom_100(self): |
|---|
| 149 |
zx = self.view.matrix[0] |
|---|
| 150 |
self.view.zoom(1 / zx) |
|---|
| 151 |
|
|---|
| 152 |
@action(name='diagram-select-all', label='_Select all', accel='<Control>a') |
|---|
| 153 |
def select_all(self): |
|---|
| 154 |
self.view.select_all() |
|---|
| 155 |
|
|---|
| 156 |
@action(name='diagram-unselect-all', label='Des_elect all', |
|---|
| 157 |
accel='<Control><Shift>a') |
|---|
| 158 |
def unselect_all(self): |
|---|
| 159 |
self.view.unselect_all() |
|---|
| 160 |
|
|---|
| 161 |
@action(name='diagram-delete', stock_id='gtk-delete') |
|---|
| 162 |
@transactional |
|---|
| 163 |
def delete_selected_items(self): |
|---|
| 164 |
items = self.view.selected_items |
|---|
| 165 |
for i in list(items): |
|---|
| 166 |
if isinstance(i, DiagramItem): |
|---|
| 167 |
s = i.subject |
|---|
| 168 |
if s and len(s.presentation) == 1: |
|---|
| 169 |
s.unlink() |
|---|
| 170 |
i.unlink() |
|---|
| 171 |
else: |
|---|
| 172 |
if i.canvas: |
|---|
| 173 |
i.canvas.remove(i) |
|---|
| 174 |
|
|---|
| 175 |
def may_remove_from_model(self, view): |
|---|
| 176 |
""" |
|---|
| 177 |
Check if there are items which will be deleted from the model |
|---|
| 178 |
(when their last views are deleted). If so request user |
|---|
| 179 |
confirmation before deletion. |
|---|
| 180 |
""" |
|---|
| 181 |
items = self.view.selected_items |
|---|
| 182 |
last_in_model = filter(lambda i: i.subject and len(i.subject.presentation) == 1, items) |
|---|
| 183 |
log.debug('Last in model: %s' % str(last_in_model)) |
|---|
| 184 |
if last_in_model: |
|---|
| 185 |
return self.confirm_deletion_of_items(last_in_model) |
|---|
| 186 |
return True |
|---|
| 187 |
|
|---|
| 188 |
def confirm_deletion_of_items(self, last_in_model): |
|---|
| 189 |
""" |
|---|
| 190 |
Request user confirmation on deleting the item from the model. |
|---|
| 191 |
""" |
|---|
| 192 |
s = '' |
|---|
| 193 |
for item in last_in_model: |
|---|
| 194 |
s += '%s\n' % str(item) |
|---|
| 195 |
|
|---|
| 196 |
dialog = gtk.MessageDialog( |
|---|
| 197 |
None, |
|---|
| 198 |
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, |
|---|
| 199 |
gtk.MESSAGE_WARNING, |
|---|
| 200 |
gtk.BUTTONS_YES_NO, |
|---|
| 201 |
'This will remove the following selected items from the model:\n%s\nAre you sure?' % s |
|---|
| 202 |
) |
|---|
| 203 |
dialog.set_transient_for(self.owning_window.window) |
|---|
| 204 |
value = dialog.run() |
|---|
| 205 |
dialog.destroy() |
|---|
| 206 |
if value == gtk.RESPONSE_YES: |
|---|
| 207 |
return True |
|---|
| 208 |
return False |
|---|
| 209 |
|
|---|
| 210 |
def _on_key_press_event(self, view, event): |
|---|
| 211 |
""" |
|---|
| 212 |
Handle the 'Delete' key. This can not be handled directly (through |
|---|
| 213 |
GTK's accelerators) since otherwise this key will confuse the text |
|---|
| 214 |
edit stuff. |
|---|
| 215 |
""" |
|---|
| 216 |
if view.is_focus(): |
|---|
| 217 |
if event.keyval == 0xFFFF and \ |
|---|
| 218 |
(event.state == 0 or event.state & gtk.gdk.MOD2_MASK): |
|---|
| 219 |
self.delete_selected_items() |
|---|
| 220 |
elif event.keyval == 0xFF08 and \ |
|---|
| 221 |
(event.state == 0 or event.state & gtk.gdk.MOD2_MASK): |
|---|
| 222 |
self.delete_selected_items() |
|---|
| 223 |
else: |
|---|
| 224 |
print '%x' %event.keyval, event.state |
|---|
| 225 |
|
|---|
| 226 |
|
|---|
| 227 |
|
|---|
| 228 |
def _on_view_selection_changed(self, view, selection_or_focus): |
|---|
| 229 |
component.handle(DiagramSelectionChange(view, view.focused_item, view.selected_items)) |
|---|
| 230 |
|
|---|
| 231 |
def _on_diagram_event(self, element, pspec): |
|---|
| 232 |
if pspec == '__unlink__': |
|---|
| 233 |
self.close() |
|---|
| 234 |
elif pspec.name == 'name': |
|---|
| 235 |
print 'Trying to set name to', element.name |
|---|
| 236 |
self.owning_window.set_tab_label(self, element.name) |
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| 239 |
def _on_drag_data_received(self, view, context, x, y, data, info, time): |
|---|
| 240 |
""" |
|---|
| 241 |
Handle data dropped on the canvas. |
|---|
| 242 |
""" |
|---|
| 243 |
|
|---|
| 244 |
if data and data.format == 8 and info == DiagramView.TARGET_ELEMENT_ID: |
|---|
| 245 |
|
|---|
| 246 |
element = self.element_factory.lookup(data.data) |
|---|
| 247 |
assert element |
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
item_class = get_diagram_item(type(element)) |
|---|
| 252 |
if isinstance(element, UML.Diagram): |
|---|
| 253 |
self.action_manager.execute('OpenModelElement') |
|---|
| 254 |
elif item_class: |
|---|
| 255 |
tx = Transaction() |
|---|
| 256 |
item = self.diagram.create(item_class) |
|---|
| 257 |
assert item |
|---|
| 258 |
inverse = Matrix(*view.matrix) |
|---|
| 259 |
inverse.invert() |
|---|
| 260 |
cx, cy = inverse.transform_point(x + view.hadjustment.value, |
|---|
| 261 |
y + view.vadjustment.value) |
|---|
| 262 |
|
|---|
| 263 |
ix, iy = view.canvas.get_matrix_c2i(item, calculate=True).transform_point(max(0, cx), max(0, cy)) |
|---|
| 264 |
item.matrix.translate(ix, iy) |
|---|
| 265 |
item.subject = element |
|---|
| 266 |
tx.commit() |
|---|
| 267 |
view.unselect_all() |
|---|
| 268 |
view.focused_item = item |
|---|
| 269 |
|
|---|
| 270 |
else: |
|---|
| 271 |
log.warning('No graphical representation for UML element %s' % type(element).__name__) |
|---|
| 272 |
context.finish(True, False, time) |
|---|
| 273 |
else: |
|---|
| 274 |
context.finish(False, False, time) |
|---|
| 275 |
|
|---|