1 """All ROX applications that can save documents should use drag-and-drop saving.
2 The document itself should use the Saveable mix-in class and override some of the
3 methods to actually do the save.
5 If you want to save a selection then you can create a new object specially for
6 the purpose and pass that to the SaveBox."""
10 from rox
import alert
, info
, g
, _
, filer
11 from rox
import choices
, get_local_path
, TRUE
, FALSE
18 def _write_xds_property(context
, value
):
19 win
= context
.source_window
21 win
.property_change('XdndDirectSave0', 'text/plain', 8,
22 gdk
.PROP_MODE_REPLACE
,
25 win
.property_delete('XdndDirectSave0')
27 def _read_xds_property(context
, delete
):
28 win
= context
.source_window
29 retval
= win
.property_get('XdndDirectSave0', 'text/plain', delete
)
34 def image_for_type(type):
35 'Search <Choices> for a suitable icon. Returns a pixbuf, or None.'
36 media
, subtype
= type.split('/', 1)
37 path
= choices
.load('MIME-icons', media
+ '_' + subtype
+ '.png')
39 path
= choices
.load('MIME-icons', media
+ '.png')
41 return gdk
.pixbuf_new_from_file(path
)
45 def _report_save_error():
46 "Report a SaveAbort nicely, otherwise use report_exception()"
47 type, value
= sys
.exc_info()[:2]
48 if isinstance(value
, AbortSave
):
51 rox
.report_exception()
53 class AbortSave(Exception):
54 """Raise this to cancel a save. If a message is given, it is displayed
55 in a normal alert box (not in the report_exception style). If the
56 message is None, no message is shown (you should have already shown
58 def __init__(self
, message
):
59 self
.message
= message
63 rox
.alert(self
.message
)
66 """This class describes the interface that an object must provide
67 to work with the SaveBox/SaveArea widgets. Inherit from it if you
68 want to save. All methods can be overridden, but normally only
69 save_to_stream() needs to be."""
71 def set_uri(self
, uri
):
72 """When the data is safely saved somewhere this is called
73 with its new name. Mark your data as unmodified and update
74 the filename for next time. Saving to another application
75 won't call this method. Default method does nothing."""
78 def save_to_stream(self
, stream
):
79 """Write the data to save to the stream. When saving to a
80 local file, stream will be the actual file, otherwise it is a
82 raise Exception('You forgot to write the save_to_stream() method...'
85 def save_to_file(self
, path
):
86 """Write data to file. Raise an exception on error.
87 The default creates a temporary file, uses save_to_stream() to
88 write to it, then renames it over the original. If the temporary file
89 can't be created, it writes directly over the original."""
91 tmp
= 'tmp-' + `random
.randrange(1000000)`
92 tmp
= os
.path
.join(os
.path
.dirname(path
), tmp
)
95 return os
.fdopen(os
.open(path
, os
.O_CREAT | os
.O_WRONLY
, 0600), 'wb')
100 # Can't create backup... try a direct write
105 self
.save_to_stream(file)
112 if tmp
and os
.path
.exists(tmp
):
113 if os
.path
.getsize(tmp
) == 0 or \
114 rox
.confirm(_("Delete temporary file '%s'?") % tmp
,
117 raise AbortSave(None)
118 self
.save_set_permissions(path
)
121 def save_to_selection(self
, selection_data
):
122 """Write data to the selection. The default method uses save_to_stream()."""
123 from cStringIO
import StringIO
125 self
.save_to_stream(stream
)
126 selection_data
.set(selection_data
.target
, 8, stream
.getvalue())
129 def save_set_permissions(self
, path
):
130 """The default save_to_file() creates files with the mode 0600
131 (user read/write only). After saving has finished, it calls this
132 method to set the final permissions. The save_set_permissions():
133 - sets it to 0666 masked with the umask (if save_mode is None), or
134 - sets it to save_mode (not masked) otherwise."""
135 if self
.save_mode
is not None:
136 os
.chmod(path
, self
.save_mode
)
138 mask
= os
.umask(0077) # Get the current umask
139 os
.umask(mask
) # Set it back how it was
140 os
.chmod(path
, 0666 & ~mask
)
143 """Time to close the savebox. Default method does nothing."""
147 """Discard button clicked, or document safely saved. Only called if a SaveBox
148 was created with discard=1.
149 The user doesn't want the document any more, even if it's modified and unsaved.
151 raise Exception("Sorry... my programmer forgot to tell me how to handle Discard!")
153 save_to_stream
._rox
_default
= 1
154 save_to_file
._rox
_default
= 1
155 save_to_selection
._rox
_default
= 1
156 def can_save_to_file(self
):
157 """Indicates whether we have a working save_to_stream or save_to_file
158 method (ie, whether we can save to files). Default method checks that
159 one of these two methods has been overridden."""
160 if not hasattr(self
.save_to_stream
, '_rox_default'):
161 return 1 # Have user-provided save_to_stream
162 if not hasattr(self
.save_to_file
, '_rox_default'):
163 return 1 # Have user-provided save_to_file
165 def can_save_to_selection(self
):
166 """Indicates whether we have a working save_to_stream or save_to_selection
167 method (ie, whether we can save to selections). Default methods checks that
168 one of these two methods has been overridden."""
169 if not hasattr(self
.save_to_stream
, '_rox_default'):
170 return 1 # Have user-provided save_to_stream
171 if not hasattr(self
.save_to_selection
, '_rox_default'):
172 return 1 # Have user-provided save_to_file
175 def save_cancelled(self
):
176 """If you multitask during a save (using a recursive mainloop) then the
177 user may click on the Cancel button. This function gets called if so, and
178 should cause the recursive mainloop to return."""
179 raise Exception("Lazy programmer error: can't abort save!")
181 class SaveArea(g
.VBox
):
182 """A SaveArea contains the widgets used in a save box. You can use
183 this to put a savebox area in a larger window."""
184 def __init__(self
, document
, uri
, type):
185 """'document' must be a subclass of Saveable.
186 'uri' is the file's current location, or a simple name (eg 'TextFile')
187 if it has never been saved.
188 'type' is the MIME-type to use (eg 'text/plain').
190 g
.VBox
.__init
__(self
, FALSE
, 0)
192 self
.document
= document
193 self
.initial_uri
= uri
195 drag_area
= self
._create
_drag
_area
(type)
196 self
.pack_start(drag_area
, TRUE
, TRUE
, 0)
200 entry
.connect('activate', lambda w
: self
.save_to_file_in_entry())
202 self
.pack_start(entry
, FALSE
, TRUE
, 4)
207 def _set_icon(self
, type):
208 pixbuf
= image_for_type(type)
210 self
.icon
.set_from_pixbuf(pixbuf
)
212 self
.icon
.set_from_stock(g
.STOCK_MISSING_IMAGE
, g
.ICON_SIZE_DND
)
214 def _create_drag_area(self
, type):
215 align
= g
.Alignment()
216 align
.set(.5, .5, 0, 0)
218 self
.drag_box
= g
.EventBox()
219 self
.drag_box
.set_border_width(4)
220 self
.drag_box
.add_events(gdk
.BUTTON_PRESS_MASK
)
221 align
.add(self
.drag_box
)
223 self
.icon
= g
.Image()
226 self
._set
_drag
_source
(type)
227 self
.drag_box
.connect('drag_begin', self
.drag_begin
)
228 self
.drag_box
.connect('drag_end', self
.drag_end
)
229 self
.drag_box
.connect('drag_data_get', self
.drag_data_get
)
230 self
.drag_in_progress
= 0
232 self
.drag_box
.add(self
.icon
)
236 def set_type(self
, type, icon
= None):
237 """Change the icon and drag target to 'type'.
238 If 'icon' is given (as a GtkImage) then that icon is used,
239 otherwise an appropriate icon for the type is used."""
241 self
.icon
.set_from_pixbuf(icon
.get_pixbuf())
244 self
._set
_drag
_source
(type)
246 def _set_drag_source(self
, type):
247 if self
.document
.can_save_to_file():
248 targets
= [('XdndDirectSave0', 0, TARGET_XDS
)]
251 if self
.document
.can_save_to_selection():
252 targets
= targets
+ [(type, 0, TARGET_RAW
),
253 ('application/octet-stream', 0, TARGET_RAW
)]
256 raise Exception("Document %s can't save!" % self
.document
)
257 self
.drag_box
.drag_source_set(gdk
.BUTTON1_MASK | gdk
.BUTTON3_MASK
,
259 gdk
.ACTION_COPY | gdk
.ACTION_MOVE
)
261 def save_to_file_in_entry(self
):
262 """Call this when the user clicks on an OK button you provide."""
263 uri
= self
.entry
.get_text()
264 path
= get_local_path(uri
)
267 if not self
.confirm_new_path(path
):
270 self
.set_sensitive(FALSE
)
272 self
.document
.save_to_file(path
)
274 self
.set_sensitive(TRUE
)
280 rox
.info(_("Drag the icon to a directory viewer\n"
281 "(or enter a full pathname)"))
283 def drag_begin(self
, drag_box
, context
):
284 self
.drag_in_progress
= 1
285 self
.destroy_on_drag_end
= 0
289 pixbuf
= self
.icon
.get_pixbuf()
291 drag_box
.drag_source_set_icon_pixbuf(pixbuf
)
293 uri
= self
.entry
.get_text()
302 _write_xds_property(context
, leaf
)
304 def drag_data_get(self
, widget
, context
, selection_data
, info
, time
):
305 if info
== TARGET_RAW
:
307 self
.set_sensitive(FALSE
)
309 self
.document
.save_to_selection(selection_data
)
311 self
.set_sensitive(TRUE
)
314 _write_xds_property(context
, None)
318 _write_xds_property(context
, None)
320 if self
.drag_in_progress
:
321 self
.destroy_on_drag_end
= 1
325 elif info
!= TARGET_XDS
:
326 _write_xds_property(context
, None)
327 alert("Bad target requested!")
332 # Get the path that the destination app wants us to save to.
333 # If it's local, save and return Success
334 # (or Error if save fails)
335 # If it's remote, return Failure (remote may try another method)
336 # If no URI is given, return Error
338 uri
= _read_xds_property(context
, FALSE
)
340 path
= get_local_path(uri
)
342 if not self
.confirm_new_path(path
):
346 self
.set_sensitive(FALSE
)
348 self
.document
.save_to_file(path
)
350 self
.set_sensitive(TRUE
)
351 self
.data_sent
= TRUE
354 self
.data_sent
= FALSE
359 to_send
= 'F' # Non-local transfer
361 alert("Remote application wants to use " +
362 "Direct Save, but I can't read the " +
363 "XdndDirectSave0 (type text/plain) " +
366 selection_data
.set(selection_data
.target
, 8, to_send
)
369 _write_xds_property(context
, None)
370 path
= get_local_path(uri
)
378 def confirm_new_path(self
, path
):
379 """Use wants to save to this path. If it's different to the original path,
380 check that it doesn't exist and ask for confirmation if it does. Returns true
381 to go ahead with the save."""
382 if path
== self
.initial_uri
:
384 if not os
.path
.exists(path
):
386 return rox
.confirm(_("File '%s' already exists -- overwrite it?") % path
,
387 g
.STOCK_DELETE
, _('_Overwrite'))
389 def set_uri(self
, uri
):
390 "Data is safely saved somewhere. Update the document's URI. Internal."
391 self
.document
.set_uri(uri
)
393 def drag_end(self
, widget
, context
):
394 self
.drag_in_progress
= 0
395 if self
.destroy_on_drag_end
:
399 self
.document
.save_done()
401 class SaveBox(g
.Dialog
):
402 """A SaveBox is a GtkDialog that contains a SaveArea and, optionally, a Discard button.
403 Calls rox.toplevel_(un)ref automatically.
406 def __init__(self
, document
, uri
, type = 'text/plain', discard
= FALSE
):
407 """See SaveArea.__init__.
408 If discard is TRUE then an extra discard button is added to the dialog."""
409 g
.Dialog
.__init
__(self
)
410 self
.set_has_separator(FALSE
)
412 self
.add_button(g
.STOCK_CANCEL
, g
.RESPONSE_CANCEL
)
413 self
.add_button(g
.STOCK_SAVE
, g
.RESPONSE_OK
)
414 self
.set_default_response(g
.RESPONSE_OK
)
417 discard_area
= g
.HButtonBox()
419 def discard_clicked(event
):
422 button
= rox
.ButtonMixed(g
.STOCK_DELETE
, _('_Discard'))
423 discard_area
.pack_start(button
, FALSE
, TRUE
, 2)
424 button
.connect('clicked', discard_clicked
)
425 button
.unset_flags(g
.CAN_FOCUS
)
426 button
.set_flags(g
.CAN_DEFAULT
)
427 self
.vbox
.pack_end(discard_area
, FALSE
, TRUE
, 0)
428 self
.vbox
.reorder_child(discard_area
, 0)
430 discard_area
.show_all()
432 self
.set_title(_('Save As:'))
433 self
.set_position(g
.WIN_POS_MOUSE
)
434 self
.set_wmclass('savebox', 'Savebox')
435 self
.set_border_width(1)
437 # Might as well make use of the new nested scopes ;-)
438 self
.set_save_in_progress(0)
439 class BoxedArea(SaveArea
):
440 def set_uri(area
, uri
):
441 document
.set_uri(uri
)
448 def set_sensitive(area
, sensitive
):
450 # Might have been destroyed by now...
451 self
.set_save_in_progress(not sensitive
)
452 SaveArea
.set_sensitive(area
, sensitive
)
453 save_area
= BoxedArea(document
, uri
, type)
454 self
.save_area
= save_area
457 self
.build_main_area()
461 # Have to do this here, or the selection gets messed up
462 save_area
.entry
.grab_focus()
463 g
.Editable
.select_region(save_area
.entry
, i
, -1) # PyGtk bug
464 #save_area.entry.select_region(i, -1)
466 def got_response(widget
, response
):
467 if self
.save_in_progress
:
469 document
.save_cancelled()
471 rox
.report_exception()
473 if response
== g
.RESPONSE_CANCEL
:
475 elif response
== g
.RESPONSE_OK
:
476 self
.save_area
.save_to_file_in_entry()
477 elif response
== g
.RESPONSE_DELETE_EVENT
:
480 raise Exception('Unknown response!')
481 self
.connect('response', got_response
)
484 self
.connect('destroy', lambda w
: rox
.toplevel_unref())
486 def set_type(self
, type, icon
= None):
487 """See SaveArea's method of the same name."""
488 self
.save_area
.set_type(type, icon
)
490 def build_main_area(self
):
491 """Place self.save_area somewhere in self.vbox. Override this
492 for more complicated layouts."""
493 self
.vbox
.add(self
.save_area
)
495 def set_save_in_progress(self
, in_progress
):
496 """Called when saving starts and ends. Shade/unshade any widgets as
497 required. Make sure you call the default method too!
498 Not called if box is destroyed from a recursive mainloop inside
499 a save_to_* function."""
500 self
.set_response_sensitive(g
.RESPONSE_OK
, not in_progress
)
501 self
.save_in_progress
= in_progress
503 class StringSaver(SaveBox
, Saveable
):
504 """A very simple SaveBox which saves the string passed to its constructor."""
505 def __init__(self
, string
, name
):
506 """'string' is the string to save. 'name' is the default filename"""
507 SaveBox
.__init
__(self
, self
, name
, 'text/plain')
510 def save_to_stream(self
, stream
):
511 stream
.write(self
.string
)
513 class SaveFilter(Saveable
):
514 """This Saveable runs a process in the background to generate the
515 save data. Any python streams can be used as the input to and
516 output from the process.
518 The output from the subprocess is saved to the output stream (either
519 directly, for fileno() streams, or via another temporary file).
521 If the process returns a non-zero exit status or writes to stderr,
522 the save fails (messages written to stderr are displayed).
527 def set_stdin(self
, stream
):
528 """Use 'stream' as stdin for the process. If stream is not a
529 seekable fileno() stream then it is copied to a temporary file
530 at this point. If None, the child process will get /dev/null on
532 if stream
is not None:
533 if hasattr(stream
, 'fileno') and hasattr(stream
, 'seek'):
538 self
.stdin
= tempfile
.TemporaryFile()
539 shutil
.copyfileobj(stream
, self
.stdin
)
543 def save_to_stream(self
, stream
):
544 from processes
import Process
545 from cStringIO
import StringIO
549 # Get the FD for the output, creating a tmp file if needed
550 if hasattr(stream
, 'fileno'):
551 stdout_fileno
= stream
.fileno()
555 tmp
= tempfile
.TemporaryFile()
556 stdout_fileno
= tmp
.fileno()
558 # Get the FD for the input
560 stdin_fileno
= self
.stdin
.fileno()
563 stdin_fileno
= os
.open('/dev/null', os
.O_RDONLY
)
565 class FilterProcess(Process
):
566 def child_post_fork(self
):
567 if stdout_fileno
!= 1:
568 os
.dup2(stdout_fileno
, 1)
569 os
.close(stdout_fileno
)
570 if stdin_fileno
is not None and stdin_fileno
!= 0:
571 os
.dup2(stdin_fileno
, 0)
572 os
.close(stdin_fileno
)
573 def got_error_output(self
, data
):
575 def child_died(self
, status
):
580 self
.process
= FilterProcess()
588 print >> errors
, '\nProcess terminated at user request'
589 error
= errors
.getvalue().strip()
591 raise AbortSave(error
)
593 raise AbortSave('child_run() returned an error code, but no error message!')
595 # Data went to a temp file
597 stream
.write(tmp
.read())
600 """This is run in the child process. The default method runs 'self.command'
601 using os.system() and prints a message to stderr if the exit code is non-zero.
602 DO NOT call gtk functions here!
604 Be careful to escape shell special characters when inserting filenames!
606 command
= self
.command
607 if os
.system(command
):
608 print >>sys
.stderr
, "Command:\n%s\nreturned an error code" % command
609 os
._exit
(0) # Writing to stderr indicates error...
611 def save_cancelled(self
):
612 """Send SIGTERM to the child processes."""