| 1 |
""" |
|---|
| 2 |
The file service is responsible for loading and saving the user data. |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
import gc |
|---|
| 6 |
import gobject, pango, gtk |
|---|
| 7 |
from zope import interface, component |
|---|
| 8 |
from gaphor.interfaces import IService, IActionProvider, IServiceEvent |
|---|
| 9 |
from gaphor.core import _, inject, action, build_action_group |
|---|
| 10 |
from gaphor import UML |
|---|
| 11 |
from gaphor.misc.gidlethread import GIdleThread, Queue, QueueEmpty |
|---|
| 12 |
from gaphor.misc.xmlwriter import XMLWriter |
|---|
| 13 |
from gaphor.misc.errorhandler import error_handler, ErrorHandlerAspect, weave_method |
|---|
| 14 |
DEFAULT_EXT='.gaphor' |
|---|
| 15 |
|
|---|
| 16 |
class FileManagerStateChanged(object): |
|---|
| 17 |
""" |
|---|
| 18 |
Event class used to send state changes on the ndo Manager. |
|---|
| 19 |
""" |
|---|
| 20 |
interface.implements(IServiceEvent) |
|---|
| 21 |
|
|---|
| 22 |
def __init__(self, service): |
|---|
| 23 |
self.service = service |
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
class FileManager(object): |
|---|
| 27 |
""" |
|---|
| 28 |
The file service, responsible for loading and saving Gaphor models. |
|---|
| 29 |
""" |
|---|
| 30 |
|
|---|
| 31 |
interface.implements(IService, IActionProvider) |
|---|
| 32 |
|
|---|
| 33 |
element_factory = inject('element_factory') |
|---|
| 34 |
gui_manager = inject('gui_manager') |
|---|
| 35 |
properties = inject('properties') |
|---|
| 36 |
|
|---|
| 37 |
menu_xml = """ |
|---|
| 38 |
<ui> |
|---|
| 39 |
<menubar name="mainwindow"> |
|---|
| 40 |
<menu action="file"> |
|---|
| 41 |
<placeholder name="primary"> |
|---|
| 42 |
<menuitem action="file-new" /> |
|---|
| 43 |
<menuitem action="file-new-template" /> |
|---|
| 44 |
<menuitem action="file-open" /> |
|---|
| 45 |
<menu name="recent" action="file-recent-files"> |
|---|
| 46 |
<menuitem action="file-recent-0" /> |
|---|
| 47 |
<menuitem action="file-recent-1" /> |
|---|
| 48 |
<menuitem action="file-recent-2" /> |
|---|
| 49 |
<menuitem action="file-recent-3" /> |
|---|
| 50 |
<menuitem action="file-recent-4" /> |
|---|
| 51 |
<menuitem action="file-recent-5" /> |
|---|
| 52 |
<menuitem action="file-recent-6" /> |
|---|
| 53 |
<menuitem action="file-recent-7" /> |
|---|
| 54 |
<menuitem action="file-recent-8" /> |
|---|
| 55 |
</menu> |
|---|
| 56 |
<separator /> |
|---|
| 57 |
<menuitem action="file-save" /> |
|---|
| 58 |
<menuitem action="file-save-as" /> |
|---|
| 59 |
<separator /> |
|---|
| 60 |
</placeholder> |
|---|
| 61 |
</menu> |
|---|
| 62 |
</menubar> |
|---|
| 63 |
<toolbar action="mainwindow-toolbar"> |
|---|
| 64 |
<toolitem action="file-open" /> |
|---|
| 65 |
<separator /> |
|---|
| 66 |
<toolitem action="file-save" /> |
|---|
| 67 |
<toolitem action="file-save-as" /> |
|---|
| 68 |
<separator /> |
|---|
| 69 |
</toolbar> |
|---|
| 70 |
</ui> |
|---|
| 71 |
""" |
|---|
| 72 |
|
|---|
| 73 |
def __init__(self): |
|---|
| 74 |
self._filename = None |
|---|
| 75 |
|
|---|
| 76 |
def init(self, app): |
|---|
| 77 |
self._app = app |
|---|
| 78 |
self.action_group = build_action_group(self) |
|---|
| 79 |
for name, label in (('file-recent-files', '_Recent files'),): |
|---|
| 80 |
a = gtk.Action(name, label, None, None) |
|---|
| 81 |
a.set_property('hide-if-empty', False) |
|---|
| 82 |
self.action_group.add_action(a) |
|---|
| 83 |
|
|---|
| 84 |
|
|---|
| 85 |
for i in xrange(0, 9): |
|---|
| 86 |
a = gtk.Action('file-recent-%d' % i, None, None, None) |
|---|
| 87 |
a.set_property('visible', False) |
|---|
| 88 |
self.action_group.add_action(a) |
|---|
| 89 |
a.connect('activate', self.load_recent, i) |
|---|
| 90 |
self.update_recent_files() |
|---|
| 91 |
|
|---|
| 92 |
def shutdown(self): |
|---|
| 93 |
pass |
|---|
| 94 |
|
|---|
| 95 |
def _set_filename(self, filename): |
|---|
| 96 |
if filename != self._filename: |
|---|
| 97 |
self._filename = filename |
|---|
| 98 |
self.update_recent_files(filename) |
|---|
| 99 |
|
|---|
| 100 |
filename = property(lambda s: s._filename, _set_filename) |
|---|
| 101 |
|
|---|
| 102 |
def update_recent_files(self, new_filename=None): |
|---|
| 103 |
recent_files = self.properties.get('recent-files', []) |
|---|
| 104 |
if new_filename and new_filename not in recent_files: |
|---|
| 105 |
recent_files.insert(0, new_filename) |
|---|
| 106 |
recent_files = recent_files[0:9] |
|---|
| 107 |
self.properties.set('recent-files', recent_files) |
|---|
| 108 |
|
|---|
| 109 |
for i in xrange(0, 9): |
|---|
| 110 |
self.action_group.get_action('file-recent-%d' % i).set_property('visible', False) |
|---|
| 111 |
|
|---|
| 112 |
for i, f in enumerate(recent_files): |
|---|
| 113 |
id = 'file-recent%d' % i |
|---|
| 114 |
a = self.action_group.get_action('file-recent-%d' % i) |
|---|
| 115 |
a.props.label = '_%d. %s' % (i+1, f.replace('_', '__')) |
|---|
| 116 |
a.props.tooltip = 'Load %s.' % f |
|---|
| 117 |
a.props.visible = True |
|---|
| 118 |
|
|---|
| 119 |
def load_recent(self, action, index): |
|---|
| 120 |
recent_files = self.properties.get('recent-files', []) |
|---|
| 121 |
filename = recent_files[index] |
|---|
| 122 |
self._load(filename) |
|---|
| 123 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 124 |
|
|---|
| 125 |
@action(name='file-new', stock_id='gtk-new') |
|---|
| 126 |
def new(self): |
|---|
| 127 |
element_factory = self.element_factory |
|---|
| 128 |
main_window = self.gui_manager.main_window |
|---|
| 129 |
if element_factory.size(): |
|---|
| 130 |
dialog = gtk.MessageDialog(main_window.window, |
|---|
| 131 |
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, |
|---|
| 132 |
gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, |
|---|
| 133 |
_("Opening a new model will flush the currently loaded model.\nAny changes made will not be saved. Do you want to continue?")) |
|---|
| 134 |
answer = dialog.run() |
|---|
| 135 |
dialog.destroy() |
|---|
| 136 |
if answer != gtk.RESPONSE_YES: |
|---|
| 137 |
return |
|---|
| 138 |
|
|---|
| 139 |
element_factory.flush() |
|---|
| 140 |
gc.collect() |
|---|
| 141 |
model = element_factory.create(UML.Package) |
|---|
| 142 |
model.name = _('New model') |
|---|
| 143 |
diagram = element_factory.create(UML.Diagram) |
|---|
| 144 |
diagram.package = model |
|---|
| 145 |
diagram.name= _('main') |
|---|
| 146 |
self.filename = None |
|---|
| 147 |
element_factory.notify_model() |
|---|
| 148 |
|
|---|
| 149 |
main_window.select_element(diagram) |
|---|
| 150 |
main_window.show_diagram(diagram) |
|---|
| 151 |
|
|---|
| 152 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 |
def _load(self, filename): |
|---|
| 156 |
try: |
|---|
| 157 |
from gaphor import storage |
|---|
| 158 |
log.debug('Loading from: %s' % filename) |
|---|
| 159 |
main_window = self.gui_manager.main_window |
|---|
| 160 |
queue = Queue() |
|---|
| 161 |
win = show_status_window(_('Loading...'), _('Loading model from %s') % filename, main_window.window, queue) |
|---|
| 162 |
gc.collect() |
|---|
| 163 |
worker = GIdleThread(storage.load_generator(filename, self.element_factory), queue) |
|---|
| 164 |
|
|---|
| 165 |
|
|---|
| 166 |
|
|---|
| 167 |
worker.start() |
|---|
| 168 |
worker.wait() |
|---|
| 169 |
if worker.error: |
|---|
| 170 |
log.error('Error while loading model from file %s: %s' % (filename, worker.error)) |
|---|
| 171 |
error_handler(message='Error while loading model from file %s' % filename, exc_info=worker.exc_info) |
|---|
| 172 |
|
|---|
| 173 |
|
|---|
| 174 |
|
|---|
| 175 |
view = main_window.tree_view |
|---|
| 176 |
|
|---|
| 177 |
self.filename = filename |
|---|
| 178 |
|
|---|
| 179 |
|
|---|
| 180 |
view.expand_root_nodes() |
|---|
| 181 |
finally: |
|---|
| 182 |
try: |
|---|
| 183 |
win.destroy() |
|---|
| 184 |
except: |
|---|
| 185 |
pass |
|---|
| 186 |
|
|---|
| 187 |
|
|---|
| 188 |
def _save(self, filename): |
|---|
| 189 |
if filename and len(filename) > 0: |
|---|
| 190 |
from gaphor import storage |
|---|
| 191 |
if not filename.endswith(DEFAULT_EXT): |
|---|
| 192 |
filename = filename + DEFAULT_EXT |
|---|
| 193 |
|
|---|
| 194 |
queue = Queue() |
|---|
| 195 |
log.debug('Saving to: %s' % filename) |
|---|
| 196 |
win = show_status_window('Saving...', 'Saving model to %s' % filename, self.gui_manager.main_window.window, queue) |
|---|
| 197 |
try: |
|---|
| 198 |
out = open(filename, 'w') |
|---|
| 199 |
|
|---|
| 200 |
worker = GIdleThread(storage.save_generator(XMLWriter(out), self.element_factory), queue) |
|---|
| 201 |
|
|---|
| 202 |
|
|---|
| 203 |
worker.start() |
|---|
| 204 |
worker.wait() |
|---|
| 205 |
if worker.error: |
|---|
| 206 |
log.error('Error while saving model to file %s: %s' % (filename, worker.error)) |
|---|
| 207 |
error_handler(message='Error while saving model to file %s' % filename, exc_info=worker.exc_info) |
|---|
| 208 |
out.close() |
|---|
| 209 |
|
|---|
| 210 |
self.filename = filename |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
|
|---|
| 214 |
finally: |
|---|
| 215 |
win.destroy() |
|---|
| 216 |
|
|---|
| 217 |
def _open_dialog(self, title): |
|---|
| 218 |
filesel = gtk.FileChooserDialog(title=title, |
|---|
| 219 |
action=gtk.FILE_CHOOSER_ACTION_OPEN, |
|---|
| 220 |
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK)) |
|---|
| 221 |
|
|---|
| 222 |
filter = gtk.FileFilter() |
|---|
| 223 |
filter.set_name("Gaphor models") |
|---|
| 224 |
filter.add_pattern("*.gaphor") |
|---|
| 225 |
filesel.add_filter(filter) |
|---|
| 226 |
|
|---|
| 227 |
filter = gtk.FileFilter() |
|---|
| 228 |
filter.set_name("All files") |
|---|
| 229 |
filter.add_pattern("*") |
|---|
| 230 |
filesel.add_filter(filter) |
|---|
| 231 |
|
|---|
| 232 |
if self.filename: |
|---|
| 233 |
filesel.set_current_name(self.filename) |
|---|
| 234 |
|
|---|
| 235 |
response = filesel.run() |
|---|
| 236 |
filename = filesel.get_filename() |
|---|
| 237 |
filesel.destroy() |
|---|
| 238 |
if not filename or response != gtk.RESPONSE_OK: |
|---|
| 239 |
return |
|---|
| 240 |
return filename |
|---|
| 241 |
|
|---|
| 242 |
|
|---|
| 243 |
@action(name='file-new-template', label=_('New from template')) |
|---|
| 244 |
def new_from_template(self): |
|---|
| 245 |
filename = self._open_dialog('New Gaphor model from template') |
|---|
| 246 |
if filename: |
|---|
| 247 |
self._load(filename) |
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
self.filename = None |
|---|
| 251 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 252 |
|
|---|
| 253 |
|
|---|
| 254 |
@action(name='file-open', stock_id='gtk-open') |
|---|
| 255 |
def open(self): |
|---|
| 256 |
filename = self._open_dialog('Open Gaphor model') |
|---|
| 257 |
if filename: |
|---|
| 258 |
self._load(filename) |
|---|
| 259 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 260 |
|
|---|
| 261 |
|
|---|
| 262 |
@action(name='file-save', stock_id='gtk-save') |
|---|
| 263 |
def save(self): |
|---|
| 264 |
filename = self.filename |
|---|
| 265 |
if filename: |
|---|
| 266 |
self._save(filename) |
|---|
| 267 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 268 |
else: |
|---|
| 269 |
self.save_as() |
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
@action(name='file-save-as', stock_id='gtk-save-as') |
|---|
| 273 |
def save_as(self): |
|---|
| 274 |
filename = self.filename |
|---|
| 275 |
filesel = gtk.FileChooserDialog(title=_('Save Gaphor model as'), |
|---|
| 276 |
action=gtk.FILE_CHOOSER_ACTION_SAVE, |
|---|
| 277 |
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) |
|---|
| 278 |
if filename: |
|---|
| 279 |
filesel.set_current_name(filename) |
|---|
| 280 |
response = filesel.run() |
|---|
| 281 |
filename = None |
|---|
| 282 |
if response == gtk.RESPONSE_OK: |
|---|
| 283 |
filename = filesel.get_filename() |
|---|
| 284 |
filesel.destroy() |
|---|
| 285 |
self._save(filename) |
|---|
| 286 |
component.handle(FileManagerStateChanged(self)) |
|---|
| 287 |
|
|---|
| 288 |
|
|---|
| 289 |
def show_status_window(title, message, parent=None, queue=None): |
|---|
| 290 |
""" |
|---|
| 291 |
Create a borderless window on the parent (main window), with a label and |
|---|
| 292 |
a progress bar. |
|---|
| 293 |
""" |
|---|
| 294 |
win = gtk.Window(gtk.WINDOW_TOPLEVEL) |
|---|
| 295 |
win.set_title(title) |
|---|
| 296 |
win.set_position(gtk.WIN_POS_CENTER_ON_PARENT) |
|---|
| 297 |
win.set_transient_for(parent) |
|---|
| 298 |
win.set_modal(True) |
|---|
| 299 |
win.set_resizable(False) |
|---|
| 300 |
win.set_decorated(False) |
|---|
| 301 |
win.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_SPLASHSCREEN) |
|---|
| 302 |
frame = gtk.Frame() |
|---|
| 303 |
win.add(frame) |
|---|
| 304 |
frame.set_shadow_type(gtk.SHADOW_ETCHED_IN) |
|---|
| 305 |
vbox = gtk.VBox(spacing=12) |
|---|
| 306 |
frame.add(vbox) |
|---|
| 307 |
vbox.set_border_width(12) |
|---|
| 308 |
label = gtk.Label(message) |
|---|
| 309 |
label.set_ellipsize(pango.ELLIPSIZE_MIDDLE) |
|---|
| 310 |
vbox.pack_start(label) |
|---|
| 311 |
progress_bar = gtk.ProgressBar() |
|---|
| 312 |
progress_bar.set_size_request(400, -1) |
|---|
| 313 |
vbox.pack_start(progress_bar, expand=False, fill=False, padding=0) |
|---|
| 314 |
|
|---|
| 315 |
def progress_idle_handler(progress_bar, queue): |
|---|
| 316 |
|
|---|
| 317 |
percentage = 0 |
|---|
| 318 |
try: |
|---|
| 319 |
while True: |
|---|
| 320 |
percentage = queue.get() |
|---|
| 321 |
except QueueEmpty: |
|---|
| 322 |
pass |
|---|
| 323 |
if percentage: |
|---|
| 324 |
progress_bar.set_fraction(min(percentage / 100.0, 100.0)) |
|---|
| 325 |
return True |
|---|
| 326 |
|
|---|
| 327 |
if queue: |
|---|
| 328 |
idle_id = gobject.idle_add(progress_idle_handler, progress_bar, queue, |
|---|
| 329 |
priority=gobject.PRIORITY_LOW) |
|---|
| 330 |
|
|---|
| 331 |
|
|---|
| 332 |
def remove_progress_idle_handler(window, idle_id): |
|---|
| 333 |
|
|---|
| 334 |
gobject.source_remove(idle_id) |
|---|
| 335 |
win.connect('destroy', remove_progress_idle_handler, idle_id) |
|---|
| 336 |
|
|---|
| 337 |
win.show_all() |
|---|
| 338 |
return win |
|---|
| 339 |
|
|---|
| 340 |
|
|---|