background: don't fall back to white
[mypaint:mypaint.git] / gui / filehandling.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2009 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import os, re
10 from glob import glob
11 import sys
12
13 import gtk
14 from gettext import gettext as _
15 from gettext import ngettext
16
17 from lib import document, helpers, tiledsurface
18 import drawwindow
19 import pygtkcompat
20
21 SAVE_FORMAT_ANY = 0
22 SAVE_FORMAT_ORA = 1
23 SAVE_FORMAT_PNGSOLID = 2
24 SAVE_FORMAT_PNGTRANS = 3
25 SAVE_FORMAT_PNGMULTI = 4
26 SAVE_FORMAT_JPEG = 5
27
28 # Utility function to work around the fact that gtk FileChooser/FileFilter
29 # does not have an easy way to use case insensitive filters
30 def get_case_insensitive_glob(string):
31     '''Ex: '*.ora' => '*.[oO][rR][aA]' '''
32     ext = string.split('.')[1]
33     globlist = ["[%s%s]" % (c.lower(), c.upper()) for c in ext]
34     return '*.%s' % ''.join(globlist)
35
36 def add_filters_to_dialog(filters, dialog):
37     for name, patterns in filters:
38         f = gtk.FileFilter()
39         f.set_name(name)
40         for p in patterns:
41             f.add_pattern(get_case_insensitive_glob(p))
42         dialog.add_filter(f)
43
44 def dialog_set_filename(dialog, s):
45     # According to pygtk docu we should use set_filename(),
46     # however doing so removes the selected filefilter.
47     path, name = os.path.split(s)
48     dialog.set_current_folder(path)
49     dialog.set_current_name(name)
50
51 class FileHandler(object):
52     def __init__(self, app):
53         self.app = app
54         #NOTE: filehandling and drawwindow are very tightly coupled
55         self.save_dialog = None
56
57         ag = app.builder.get_object('FileActions')
58
59         ra = gtk.RecentAction('OpenRecent', _('Open Recent'), _('Open Recent files'), None)
60         ra.set_show_tips(True)
61         ra.set_show_numbers(True)
62         rf = gtk.RecentFilter()
63         rf.add_application('mypaint')
64         ra.add_filter(rf)
65         ra.set_sort_type(gtk.RECENT_SORT_MRU)
66         ra.connect('item-activated', self.open_recent_cb)
67         ag.add_action(ra)
68
69         for action in ag.list_actions():
70             self.app.kbm.takeover_action(action)
71
72         self._filename = None
73         self.current_file_observers = []
74         self.file_opened_observers = []
75         self.active_scrap_filename = None
76         self.lastsavefailed = False
77         self.set_recent_items()
78
79         self.file_filters = [ #(name, patterns)
80         (_("All Recognized Formats"), ("*.ora", "*.png", "*.jpg", "*.jpeg")),
81         (_("OpenRaster (*.ora)"), ("*.ora",)),
82         (_("PNG (*.png)"), ("*.png",)),
83         (_("JPEG (*.jpg; *.jpeg)"), ("*.jpg", "*.jpeg")),
84         ]
85         self.saveformats = [ #(name, extension, options)
86         (_("By extension (prefer default format)"), None, {}), #0
87         (_("OpenRaster (*.ora)"), '.ora', {}), #1
88         (_("PNG solid with background (*.png)"), '.png', {'alpha': False}), #2
89         (_("PNG transparent (*.png)"), '.png', {'alpha': True}), #3
90         (_("Multiple PNG transparent (*.XXX.png)"), '.png', {'multifile': True}), #4
91         (_("JPEG 90% quality (*.jpg; *.jpeg)"), '.jpg', {'quality': 90}), #5
92         ]
93         self.ext2saveformat = {
94         '.ora': SAVE_FORMAT_ORA, 
95         '.png': SAVE_FORMAT_PNGSOLID, 
96         '.jpeg': SAVE_FORMAT_JPEG, 
97         '.jpg': SAVE_FORMAT_JPEG}
98         self.config2saveformat = {
99         'openraster': SAVE_FORMAT_ORA,
100         'jpeg-90%': SAVE_FORMAT_JPEG,
101         'png-solid': SAVE_FORMAT_PNGSOLID,
102         }
103
104     def set_recent_items(self):
105         # this list is consumed in open_last_cb
106
107         # Note: i.exists() does not work on Windows if the pathname
108         # contains utf-8 characters. Since GIMP also saves its URIs
109         # with utf-8 characters into this list, I assume this is a
110         # gtk bug.  So we use our own test instead of i.exists().
111         self.recent_items = [
112                 i for i in pygtkcompat.gtk.recent_manager_get_default().get_items()
113                 if "mypaint" in i.get_applications() and os.path.exists(helpers.uri2filename(i.get_uri()))
114         ]
115         self.recent_items.reverse()
116
117     def get_filename(self):
118         return self._filename
119
120     def set_filename(self, value):
121         self._filename = value
122         for f in self.current_file_observers:
123             f(self.filename)
124
125         if self.filename:
126             if self.filename.startswith(self.get_scrap_prefix()):
127                 self.active_scrap_filename = self.filename
128
129     filename = property(get_filename, set_filename)
130
131     def init_save_dialog(self):
132         dialog = gtk.FileChooserDialog(_("Save..."), self.app.drawWindow,
133                                        gtk.FILE_CHOOSER_ACTION_SAVE,
134                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
135                                         gtk.STOCK_SAVE, gtk.RESPONSE_OK))
136         self.save_dialog = dialog
137         dialog.set_default_response(gtk.RESPONSE_OK)
138         dialog.set_do_overwrite_confirmation(True)
139         add_filters_to_dialog(self.file_filters, dialog)
140
141         # Add widget for selecting save format
142         box = gtk.HBox()
143         label = gtk.Label(_('Format to save as:'))
144         label.set_alignment(0.0, 0.0)
145         combo = self.saveformat_combo = gtk.combo_box_new_text()
146         for name, ext, opt in self.saveformats:
147             combo.append_text(name)
148         combo.set_active(0)
149         combo.connect('changed', self.selected_save_format_changed_cb)
150         box.pack_start(label)
151         box.pack_start(combo, expand=False)
152         dialog.set_extra_widget(box)
153         dialog.show_all()
154
155     def selected_save_format_changed_cb(self, widget):
156         """When the user changes the selected format to save as in the dialog, 
157         change the extension of the filename (if existing) immediately."""
158         dialog = self.save_dialog
159         filename = dialog.get_filename()
160         if filename:
161             filename = filename.decode('utf-8')
162             filename, ext = os.path.splitext(filename)
163             if ext:
164                 saveformat = self.saveformat_combo.get_active()
165                 ext = self.saveformats[saveformat][1]
166                 if ext is not None:
167                     dialog_set_filename(dialog, filename+ext)
168
169     def confirm_destructive_action(self, title=_('Confirm'), question=_('Really continue?')):
170         self.doc.model.split_stroke() # finish stroke in progress
171         t = self.doc.model.unsaved_painting_time
172         # enough changes to bother asking? (useful for fast develop-and-test)
173         if t < 8: # (used to be 30, see https://gna.org/bugs/?17955)
174             return True
175
176         if t > 120:
177             t = int(round(t/60))
178             t = ngettext('This will discard %d minute of unsaved painting.',
179                          'This will discard %d minutes of unsaved painting.',
180                          t) % t
181         else:
182             t = int(round(t))
183             t = ngettext('This will discard %d second of unsaved painting.',
184                          'This will discard %d seconds of unsaved painting.',
185                          t) % t
186         d = gtk.Dialog(title, self.app.drawWindow, gtk.DIALOG_MODAL)
187
188         b = d.add_button(gtk.STOCK_DISCARD, gtk.RESPONSE_OK)
189         b.set_image(gtk.image_new_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_BUTTON))
190         d.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
191         b = d.add_button(_("_Save as Scrap"), gtk.RESPONSE_APPLY)
192         b.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_BUTTON))
193
194         # d.set_has_separator(False)
195         d.set_default_response(gtk.RESPONSE_CANCEL)
196         l = gtk.Label()
197         l.set_markup("<b>%s</b>\n\n%s" % (question,t))
198         l.set_padding(10, 10)
199         l.show()
200         d.vbox.pack_start(l)
201         response = d.run()
202         d.destroy()
203         if response == gtk.RESPONSE_APPLY:
204             self.save_scrap_cb(None)
205             return True
206         return response == gtk.RESPONSE_OK
207
208     def new_cb(self, action):
209         if not self.confirm_destructive_action():
210             return
211         self.doc.model.clear()
212         # Match scratchpad to canvas background
213         # TODO make this into a preference
214         if self.app.scratchpad_doc:
215             self.app.scratchpad_doc.model.set_background(self.doc.model.background)
216         self.filename = None
217         self.set_recent_items()
218         self.app.doc.reset_view_cb(None)
219
220     @staticmethod
221     def gtk_main_tick():
222         if pygtkcompat.USE_GTK3:
223             # FIXME: use something better
224             return
225         while gtk.events_pending():
226             gtk.main_iteration(False)
227
228     @drawwindow.with_wait_cursor
229     def open_file(self, filename):
230         try:
231             self.doc.model.load(filename, feedback_cb=self.gtk_main_tick)
232         except document.SaveLoadError, e:
233             self.app.message_dialog(str(e),type=gtk.MESSAGE_ERROR)
234         else:
235             self.filename = os.path.abspath(filename)
236             for func in self.file_opened_observers:
237                 func(self.filename)
238             print 'Loaded from', self.filename
239             self.app.doc.reset_view_cb(None)
240             # try to restore the last used brush and color
241             si = self.doc.model.layer.get_last_stroke_info()
242             if si:
243                 self.doc.restore_brush_from_stroke_info(si)
244
245     def open_scratchpad(self, filename):
246         try:
247             self.app.scratchpad_doc.model.load(filename, feedback_cb=self.gtk_main_tick)
248             self.app.scratchpad_filename = os.path.abspath(filename)
249             self.app.preferences["scratchpad.last_opened_scratchpad"] = self.app.scratchpad_filename
250         except document.SaveLoadError, e:
251             self.app.message_dialog(str(e), type=gtk.MESSAGE_ERROR)
252         else:
253             self.app.scratchpad_filename = os.path.abspath(filename)
254             self.app.preferences["scratchpad.last_opened_scratchpad"] = self.app.scratchpad_filename
255             print 'Loaded scratchpad from', self.app.scratchpad_filename
256             self.app.scratchpad_doc.reset_view_cb(None)
257
258     @drawwindow.with_wait_cursor
259     def save_file(self, filename, export=False, **options):
260         thumbnail_pixbuf = self.save_doc_to_file(filename, self.doc, export=export, **options)
261         if not export:
262             self.filename = os.path.abspath(filename)
263             recent_mgr = pygtkcompat.gtk.recent_manager_get_default()
264             uri = helpers.filename2uri(self.filename)
265             recent_data = dict(app_name='mypaint',
266                                app_exec=sys.argv_unicode[0].encode('utf-8'),
267                                # todo: get mime_type
268                                mime_type='application/octet-stream')
269             if pygtkcompat.USE_GTK3:
270                 # No Gtk.RecentData.new() as of 3.4.2-0ubuntu0.3,
271                 # nor can we set the fields of an empty one :(
272                 recent_mgr.add_item(uri)
273             else:
274                 recent_mgr.add_full(uri, recent_data)
275         if not thumbnail_pixbuf:
276             thumbnail_pixbuf = self.doc.model.render_thumbnail()
277         helpers.freedesktop_thumbnail(filename, thumbnail_pixbuf)
278
279     @drawwindow.with_wait_cursor
280     def save_scratchpad(self, filename, export=False, **options):
281         if self.app.scratchpad_doc.model.unsaved_painting_time or export or not os.path.exists(filename):
282             self.save_doc_to_file(filename, self.app.scratchpad_doc, export=export, **options)
283         if not export:
284             self.app.scratchpad_filename = os.path.abspath(filename)
285             self.app.preferences["scratchpad.last_opened_scratchpad"] = self.app.scratchpad_filename
286
287     def save_doc_to_file(self, filename, doc, export=False, **options):
288         thumbnail_pixbuf = None
289         try:
290             x, y, w, h =  doc.model.get_bbox()
291             if w == 0 and h == 0:
292                 w, h = tiledsurface.N, tiledsurface.N # TODO: support for other sizes
293             thumbnail_pixbuf = doc.model.save(filename, feedback_cb=self.gtk_main_tick, **options)
294             self.lastsavefailed = False
295         except document.SaveLoadError, e:
296             self.lastsavefailed = True
297             self.app.message_dialog(str(e),type=gtk.MESSAGE_ERROR)
298         else:
299             file_location = None
300             if not export:
301                 file_location = os.path.abspath(filename)
302                 print 'Saved to', file_location
303             else:
304                 file_location = os.path.abspath(filename)
305                 print 'Exported to', os.path.abspath(file_location)
306
307         return thumbnail_pixbuf
308
309
310
311     def update_preview_cb(self, file_chooser, preview):
312         filename = file_chooser.get_preview_filename()
313         if filename:
314             filename = filename.decode('utf-8')
315             pixbuf = helpers.freedesktop_thumbnail(filename)
316             if pixbuf:
317                 # if pixbuf is smaller than 256px in width, copy it onto a transparent 256x256 pixbuf
318                 pixbuf = helpers.pixbuf_thumbnail(pixbuf, 256, 256, True)
319                 preview.set_from_pixbuf(pixbuf)
320                 file_chooser.set_preview_widget_active(True)
321             else:
322                 #TODO display "no preview available" image
323                 pass
324
325     def get_open_dialog(self, filename=None, start_in_folder=None, file_filters=[]):
326         dialog = gtk.FileChooserDialog(_("Open..."), self.app.drawWindow,
327                                        gtk.FILE_CHOOSER_ACTION_OPEN,
328                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
329                                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
330         dialog.set_default_response(gtk.RESPONSE_OK)
331         add_filters_to_dialog(file_filters, dialog)
332
333         if filename:
334             dialog.set_filename(filename)
335         elif start_in_folder and os.path.isdir(start_in_folder):
336             dialog.set_current_folder(start_in_folder)
337
338         return dialog
339
340     def open_cb(self, action):
341         if not self.confirm_destructive_action():
342             return
343         dialog = gtk.FileChooserDialog(_("Open..."), self.app.drawWindow,
344                                        gtk.FILE_CHOOSER_ACTION_OPEN,
345                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
346                                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
347         dialog.set_default_response(gtk.RESPONSE_OK)
348
349         preview = gtk.Image()
350         dialog.set_preview_widget(preview)
351         dialog.connect("update-preview", self.update_preview_cb, preview)
352
353         add_filters_to_dialog(self.file_filters, dialog)
354
355         if self.filename:
356             dialog.set_filename(self.filename)
357         else:
358             # choose the most recent save folder
359             self.set_recent_items()
360             for item in reversed(self.recent_items):
361                 uri = item.get_uri()
362                 fn = helpers.uri2filename(uri)
363                 dn = os.path.dirname(fn)
364                 if os.path.isdir(dn):
365                     dialog.set_current_folder(dn)
366                     break
367         try:
368             if dialog.run() == gtk.RESPONSE_OK:
369                 dialog.hide()
370                 self.open_file(dialog.get_filename().decode('utf-8'))
371         finally:
372             dialog.destroy()
373
374     def open_scratchpad_dialog(self):
375         dialog = gtk.FileChooserDialog(_("Open Scratchpad..."), self.app.drawWindow,
376                                        gtk.FILE_CHOOSER_ACTION_OPEN,
377                                        (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,                            
378                                         gtk.STOCK_OPEN, gtk.RESPONSE_OK))
379         dialog.set_default_response(gtk.RESPONSE_OK)
380
381         preview = gtk.Image()
382         dialog.set_preview_widget(preview)
383         dialog.connect("update-preview", self.update_preview_cb, preview)
384
385         add_filters_to_dialog(self.file_filters, dialog)
386
387         if self.app.scratchpad_filename:
388             dialog.set_filename(self.app.scratchpad_filename)
389         else:
390             # choose the most recent save folder
391             self.set_recent_items()
392             for item in reversed(self.recent_items):
393                 uri = item.get_uri()
394                 fn = helpers.uri2filename(uri)
395                 dn = os.path.dirname(fn)
396                 if os.path.isdir(dn):
397                     dialog.set_current_folder(dn)
398                     break
399         try:
400             if dialog.run() == gtk.RESPONSE_OK:
401                 dialog.hide()
402                 self.app.scratchpad_filename = dialog.get_filename().decode('utf-8')
403                 self.open_scratchpad(self.app.scratchpad_filename)
404         finally:
405             dialog.destroy()  
406
407     def save_cb(self, action):
408         if not self.filename:
409             self.save_as_cb(action)
410         else:
411             self.save_file(self.filename)
412
413     def save_as_cb(self, action):
414         start_in_folder = None
415         if self.filename:
416             current_filename = self.filename
417         else:
418             current_filename = ''
419             # choose the most recent save folder
420             self.set_recent_items()
421             for item in reversed(self.recent_items):
422                 uri = item.get_uri()
423                 fn = helpers.uri2filename(uri)
424                 dn = os.path.dirname(fn)
425                 if os.path.isdir(dn):
426                     start_in_folder = dn
427                     break
428
429         if action.get_name() == 'Export':
430             # Do not change working file
431             self.save_as_dialog(self.save_file, suggested_filename = current_filename, export=True)
432         else:
433             self.save_as_dialog(self.save_file, suggested_filename = current_filename)
434
435     def save_scratchpad_as_dialog(self, export = False):
436         start_in_folder = None
437         if self.app.scratchpad_filename:
438             current_filename = self.app.scratchpad_filename
439         else:
440             current_filename = ''
441             start_in_folder = self.get_scratchpad_prefix()
442
443         self.save_as_dialog(self.save_scratchpad, suggested_filename = current_filename, export = export)
444
445     def save_as_dialog(self, save_method_reference, suggested_filename=None, start_in_folder=None, export = False, **options):
446         if not self.save_dialog:
447             self.init_save_dialog()
448         dialog = self.save_dialog
449         # Set the filename in the dialog
450         if suggested_filename:
451             dialog_set_filename(dialog, suggested_filename)
452         else:
453             dialog_set_filename(dialog, '')
454             # Recent directory?
455             if start_in_folder:
456                 dialog.set_current_folder(start_in_folder)
457
458         try:
459             # Loop until we have filename with an extension
460             while dialog.run() == gtk.RESPONSE_OK:
461                 filename = dialog.get_filename().decode('utf-8')
462                 name, ext = os.path.splitext(filename)
463                 saveformat = self.saveformat_combo.get_active()
464
465                 # If no explicitly selected format, use the extension to figure it out
466                 if saveformat == SAVE_FORMAT_ANY:
467                     cfg = self.app.preferences['saving.default_format']
468                     default_saveformat = self.config2saveformat[cfg]
469                     if ext:
470                         try: 
471                             saveformat = self.ext2saveformat[ext]
472                         except KeyError:
473                             saveformat = default_saveformat
474                     else:
475                         saveformat = default_saveformat
476
477                 desc, ext_format, options = self.saveformats[saveformat]
478
479                 # 
480                 if ext:
481                     if ext_format != ext:
482                         # Minor ugliness: if the user types '.png' but
483                         # leaves the default .ora filter selected, we
484                         # use the default options instead of those
485                         # above. However, they are the same at the moment.
486                         options = {}
487                     assert(filename)
488                     dialog.hide()
489                     if export:
490                         # Do not change working file
491                         save_method_reference(filename, True, **options)
492                     else:
493                         save_method_reference(filename, **options)
494                     break
495
496                 filename = name + ext_format
497
498                 # trigger overwrite confirmation for the modified filename
499                 dialog_set_filename(dialog, filename)
500                 dialog.response(gtk.RESPONSE_OK)
501
502         finally:
503             dialog.hide()
504             dialog.destroy()  # avoid GTK crash: https://gna.org/bugs/?17902
505             self.save_dialog = None
506
507
508
509     def save_scrap_cb(self, action):
510         filename = self.filename
511         prefix = self.get_scrap_prefix()
512         self.app.filename = self.save_autoincrement_file(filename, prefix, main_doc = True)
513
514     def save_scratchpad_cb(self, action):
515         filename = self.app.scratchpad_filename
516         prefix = self.get_scratchpad_prefix()
517         self.app.scratchpad_filename = self.save_autoincrement_file(filename, prefix, main_doc = False)
518
519     def save_autoincrement_file(self, filename, prefix, main_doc = True):
520         # If necessary, create the folder(s) the scraps are stored under
521         prefix_dir = os.path.dirname(prefix)
522         if not os.path.exists(prefix_dir): 
523             os.makedirs(prefix_dir)
524
525         number = None
526         if filename:
527             junk, file_fragment = os.path.split(filename)
528             if file_fragment.startswith("_md5"):
529                 #store direct, don't attempt to increment
530                 if main_doc:
531                     self.save_file(filename)
532                 else:
533                     self.save_scratchpad(filename)
534                 return filename
535
536             l = re.findall(re.escape(prefix) + '([0-9]+)', filename)
537             if l:
538                 number = l[0]
539
540         if number:
541             # reuse the number, find the next character
542             char = 'a'
543             for filename in glob(prefix + number + '_*'):
544                 c = filename[len(prefix + number + '_')]
545                 if c >= 'a' and c <= 'z' and c >= char:
546                     char = chr(ord(c)+1)
547             if char > 'z':
548                 # out of characters, increase the number
549                 filename = None
550                 return self.save_autoincrement_file(filename, prefix, main_doc)
551             filename = '%s%s_%c' % (prefix, number, char)
552         else:
553             # we don't have a scrap filename yet, find the next number
554             maximum = 0
555             for filename in glob(prefix + '[0-9][0-9][0-9]*'):
556                 filename = filename[len(prefix):]
557                 res = re.findall(r'[0-9]*', filename)
558                 if not res: continue
559                 number = int(res[0])
560                 if number > maximum:
561                     maximum = number
562             filename = '%s%03d_a' % (prefix, maximum+1)
563
564         # Add extension
565         cfg = self.app.preferences['saving.default_format']
566         default_saveformat = self.config2saveformat[cfg]
567         filename += self.saveformats[default_saveformat][1]
568
569         assert not os.path.exists(filename)
570         if main_doc:
571             self.save_file(filename)
572         else:
573             self.save_scratchpad(filename)
574         return filename
575
576     def get_scrap_prefix(self):
577         prefix = self.app.preferences['saving.scrap_prefix']
578         prefix = helpers.expanduser_unicode(prefix.decode('utf-8'))
579         prefix = os.path.abspath(prefix)
580         if os.path.isdir(prefix):
581             if not prefix.endswith(os.path.sep):
582                 prefix += os.path.sep
583         return prefix
584
585     def get_scratchpad_prefix(self):
586         # TODO make this something pulled from preferences #PALETTE1
587         prefix = os.path.abspath(os.path.join(self.app.confpath, 'scratchpads'))
588         if os.path.isdir(prefix):
589             if not prefix.endswith(os.path.sep):
590                 prefix += os.path.sep
591         return prefix
592
593     def get_scratchpad_default(self):
594         # TODO get the default name from preferences
595         return os.path.join(self.get_scratchpad_prefix(), "scratchpad_default.ora")
596
597     def get_scratchpad_autosave(self):
598         # TODO get the default name from preferences
599         return os.path.join(self.get_scratchpad_prefix(), "autosave.ora")
600
601     def get_gimp_prefix(self):
602         from lib import helpers
603         homepath =  helpers.expanduser_unicode(u'~')
604         if sys.platform == 'win32':
605             # using patched win32 glib using correct CSIDL_LOCAL_APPDATA
606             import glib
607             confpath = os.path.join(glib.get_user_config_dir().decode('utf-8'),'gimp-2.6')
608         elif homepath == '~':
609             confpath = os.path.join(prefix, 'UserData')
610         else:
611             confpath = os.path.join(homepath, '.gimp-2.6')
612         return confpath       
613
614     def list_scraps(self):
615         prefix = self.get_scrap_prefix()
616         return self.list_prefixed_dir(prefix)
617
618     def list_scratchpads(self):
619         prefix = self.get_scratchpad_prefix()
620         files = self.list_prefixed_dir(prefix)
621         if os.path.isdir(os.path.join(prefix, "special")):
622             files += self.list_prefixed_dir(os.path.join(prefix, "special") + os.path.sep)
623         return files
624
625     def list_prefixed_dir(self, prefix):
626         filenames = []
627         for ext in ['png', 'ora', 'jpg', 'jpeg']:
628             filenames += glob(prefix + '[0-9]*.' + ext)
629             filenames += glob(prefix + '[0-9]*.' + ext.upper())
630             # For the special linked scratchpads
631             filenames += glob(prefix + '_md5[0-9a-f]*.' + ext)
632         filenames.sort()
633         return filenames
634
635     def list_scraps_grouped(self):
636         filenames = self.list_scraps()
637         return self.list_files_grouped(filenames)
638
639     def list_scratchpads_grouped(self):
640         filenames = self.list_scratchpads()
641         return self.list_files_grouped(filenames)
642
643     def list_files_grouped(self, filenames):
644         """return scraps grouped by their major number"""
645         def scrap_id(filename):
646             s = os.path.basename(filename)
647             if s.startswith("_md5"):
648                 return s
649             return re.findall('([0-9]+)', s)[0]
650         groups = []
651         while filenames:
652             group = []
653             sid = scrap_id(filenames[0])
654             while filenames and scrap_id(filenames[0]) == sid:
655                 group.append(filenames.pop(0))
656             groups.append(group)
657         return groups
658
659     def open_recent_cb(self, action):
660         """Callback for RecentAction"""
661         if not self.confirm_destructive_action():
662             return
663         uri = action.get_current_uri()
664         fn = helpers.uri2filename(uri)
665         self.open_file(fn)
666
667     def open_last_cb(self, action):
668         """Callback to open the last file"""
669         if not self.recent_items:
670             return
671         if not self.confirm_destructive_action():
672             return
673         uri = self.recent_items.pop().get_uri()
674         fn = helpers.uri2filename(uri)
675         self.open_file(fn)
676
677     def open_scrap_cb(self, action):
678         groups = self.list_scraps_grouped()
679         if not groups:
680             msg = _('There are no scrap files named "%s" yet.') % \
681                 (self.get_scrap_prefix() + '[0-9]*')
682             self.app.message_dialog(msg, gtk.MESSAGE_WARNING)
683             return
684         if not self.confirm_destructive_action():
685             return
686         next = action.get_name() == 'NextScrap'
687
688         if next: idx = 0
689         else:    idx = -1
690         for i, group in enumerate(groups):
691             if self.active_scrap_filename in group:
692                 if next: idx = i + 1
693                 else:    idx = i - 1
694         filename = groups[idx%len(groups)][-1]
695         self.open_file(filename)
696
697     def reload_cb(self, action):
698         if self.filename and self.confirm_destructive_action():
699             self.open_file(self.filename)
700
701     def delete_scratchpads(self, filenames):
702         prefix = self.get_scratchpad_prefix()
703         prefix = os.path.abspath(prefix)
704         for filename in filenames:
705             if os.path.isfile(filename) and os.path.abspath(filename).startswith(prefix):
706                 os.remove(filename)
707                 print "Removed %s" % filename
708
709     def delete_default_scratchpad(self):
710         if os.path.isfile(self.get_scratchpad_default()):
711             os.remove(self.get_scratchpad_default())
712             print "Removed the scratchpad default file"
713
714     def delete_autosave_scratchpad(self):
715         if os.path.isfile(self.get_scratchpad_autosave()):
716             os.remove(self.get_scratchpad_autosave())
717             print "Removed the scratchpad autosave file"