1 /*
2  * Copyright (c) 2005-2006 Jean-François Wauthy (pollux@xfce.org)
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  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU Library General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17  */
18 
19 #ifdef	HAVE_CONFIG_H
20 #include <config.h>
21 #endif /* !HAVE_CONFIG_H */
22 
23 #include <libxfce4ui/libxfce4ui.h>
24 
25 #include <libburn.h>
26 
27 #include "xfburn-global.h"
28 #include "xfburn-utils.h"
29 #include "xfburn-progress-dialog.h"
30 #include "xfburn-device-box.h"
31 #include "xfburn-device-list.h"
32 #include "xfburn-udev-manager.h"
33 #include "xfburn-main.h"
34 
35 #include "xfburn-blank-dialog.h"
36 
37 #define XFBURN_BLANK_DIALOG_GET_PRIVATE(obj) (xfburn_blank_dialog_get_instance_private (obj))
38 
39 #define XFBURN_BLANK_DIALOG_EJECT_DEFAULT TRUE
40 
41 enum {
42   PROP_0,
43   PROP_EJECT,
44 };
45 
46 typedef struct
47 {
48   GtkWidget *device_box;
49   GtkWidget *combo_type;
50   GtkWidget *button_blank;
51 
52   GtkWidget *check_eject;
53   gboolean eject;
54 } XfburnBlankDialogPrivate;
55 
56 /* FIXME: the 128MB comes from cdrskin, but why? Is this really complete? */
57 #define XFBURN_FORMAT_COMPLETE_SIZE 128*1024*1024
58 
59 typedef enum {
60   XFBURN_BLANK_FAST,        /* erase w/ fast flag */
61   XFBURN_BLANK_COMPLETE,    /* erase, no flag */
62   XFBURN_FORMAT_FAST,       /* DVD+RW sequential (0x13) to overwritable (0x14), zero size */
63   XFBURN_FORMAT_COMPLETE,   /* DVD+RW sequential (0x13) to overwritable (0x14), 128MB, flag=1*/
64   XFBURN_DEFORMAT_FAST,     /* same as fast blank */
65   XFBURN_DEFORMAT_COMPLETE, /* same as complete blank */
66   XFBURN_BLANK_MODE_LAST,
67 } XfburnBlankMode;
68 
69 static char * blank_mode_names[] = {
70     N_("Quick Blank"),
71     N_("Full Blank (slow)"),
72     N_("Quick Format"),
73     N_("Full Format"),
74     N_("Quick Deformat"),
75     N_("Full Deformat (slow)"),
76   };
77 
78 enum {
79   BLANK_COMBO_NAME_COLUMN,
80   BLANK_COMBO_MODE_COLUMN,
81   BLANK_COMBO_N_COLUMNS,
82 };
83 
84 typedef struct {
85   GtkWidget *dialog_progress;
86   XfburnDevice *device;
87   XfburnBlankMode blank_mode;
88   gboolean eject;
89 } ThreadBlankParams;
90 
91 /* internal prototypes */
92 
93 static void xfburn_blank_dialog_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec);
94 static void xfburn_blank_dialog_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec);
95 
96 static gboolean is_valid_blank_mode (XfburnDevice *device, XfburnBlankMode mode);
97 static void fill_combo_mode (XfburnBlankDialog *dialog);
98 //static GList * get_valid_blank_modes (XfburnDevice *device);
99 static XfburnBlankMode get_selected_mode (XfburnBlankDialogPrivate *priv);
100 static gboolean thread_blank_perform_blank (ThreadBlankParams * params, struct burn_drive_info *drive_info);
101 static void* thread_blank (ThreadBlankParams * params);
102 static void xfburn_blank_dialog_response_cb (XfburnBlankDialog * dialog, gint response_id, gpointer user_data);
103 static void cb_volume_changed (GtkWidget *device_box, gboolean device_changed, XfburnDevice *device, XfburnBlankDialog * dialog);
104 
105 static XfceTitledDialogClass *parent_class = NULL;
106 
107 G_DEFINE_TYPE_WITH_PRIVATE(XfburnBlankDialog, xfburn_blank_dialog, XFCE_TYPE_TITLED_DIALOG);
108 
109 static void
xfburn_blank_dialog_class_init(XfburnBlankDialogClass * klass)110 xfburn_blank_dialog_class_init (XfburnBlankDialogClass * klass)
111 {
112   GObjectClass *object_class = G_OBJECT_CLASS (klass);
113 
114   parent_class = g_type_class_peek_parent (klass);
115   object_class->set_property = xfburn_blank_dialog_set_property;
116   object_class->get_property = xfburn_blank_dialog_get_property;
117 
118   g_object_class_install_property (object_class, PROP_EJECT,
119                                    g_param_spec_boolean ("eject", _("Eject the disc"),
120                                                         _("Default value for eject checkbox"), XFBURN_BLANK_DIALOG_EJECT_DEFAULT, G_PARAM_READWRITE));
121 }
122 
123 static void
xfburn_blank_dialog_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)124 xfburn_blank_dialog_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
125 {
126   XfburnBlankDialogPrivate *priv = XFBURN_BLANK_DIALOG_GET_PRIVATE (XFBURN_BLANK_DIALOG (object));
127 
128   switch (prop_id) {
129     case PROP_EJECT:
130       g_value_set_boolean (value, priv->eject);
131       break;
132     default:
133       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
134       break;
135   }
136 }
137 
138 static void
xfburn_blank_dialog_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)139 xfburn_blank_dialog_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
140 {
141   XfburnBlankDialogPrivate *priv = XFBURN_BLANK_DIALOG_GET_PRIVATE (XFBURN_BLANK_DIALOG (object));
142 
143   switch (prop_id) {
144     case PROP_EJECT:
145       priv->eject = g_value_get_boolean (value);
146       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->check_eject), priv->eject);
147       break;
148     default:
149       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
150       break;
151   }
152 }
153 
154 static void
xfburn_blank_dialog_init(XfburnBlankDialog * obj)155 xfburn_blank_dialog_init (XfburnBlankDialog * obj)
156 {
157   XfburnBlankDialogPrivate *priv = XFBURN_BLANK_DIALOG_GET_PRIVATE (obj);
158   GtkBox *box = GTK_BOX (gtk_dialog_get_content_area((GTK_DIALOG (obj))));
159   GdkPixbuf *icon = NULL;
160   GtkWidget *frame;
161   GtkWidget *vbox;
162   GtkWidget *button;
163   gint x,y;
164 
165   GtkListStore *store = NULL;
166   GtkCellRenderer *cell;
167 
168   gtk_window_set_title (GTK_WINDOW (obj), _("Blank Disc"));
169   gtk_window_set_destroy_with_parent (GTK_WINDOW (obj), TRUE);
170 
171   gtk_icon_size_lookup (GTK_ICON_SIZE_DIALOG, &x, &y);
172   icon = gtk_icon_theme_load_icon ( gtk_icon_theme_get_default(), "stock_xfburn-blank-cdrw", x, GTK_ICON_LOOKUP_GENERIC_FALLBACK, NULL);
173 
174   gtk_window_set_icon (GTK_WINDOW (obj), icon);
175   g_object_unref (icon);
176 
177   /* devices list */
178   priv->device_box = xfburn_device_box_new (SHOW_CDRW_WRITERS | BLANK_MODE);
179   g_signal_connect (G_OBJECT (priv->device_box), "volume-changed", G_CALLBACK (cb_volume_changed), obj);
180   gtk_widget_show (priv->device_box);
181 
182   frame = xfce_gtk_frame_box_new_with_content (_("Burning device"), priv->device_box);
183   gtk_widget_show (frame);
184   gtk_box_pack_start (box, frame, FALSE, FALSE, BORDER);
185 
186   /* blank mode */
187   store = gtk_list_store_new (BLANK_COMBO_N_COLUMNS, G_TYPE_STRING, G_TYPE_INT);
188   priv->combo_type = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store));
189   g_object_unref (store);
190   cell = gtk_cell_renderer_text_new ();
191   gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (priv->combo_type), cell, TRUE);
192   gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (priv->combo_type), cell, "text", BLANK_COMBO_NAME_COLUMN, NULL);
193   gtk_widget_show (priv->combo_type);
194 
195   frame = xfce_gtk_frame_box_new_with_content (_("Blank mode"), priv->combo_type);
196   gtk_widget_show (frame);
197   gtk_box_pack_start (box, frame, FALSE, FALSE, BORDER);
198 
199   /* options */
200   vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
201   gtk_widget_show (vbox);
202 
203   frame = xfce_gtk_frame_box_new_with_content (_("Options"), vbox);
204   gtk_widget_show (frame);
205   gtk_box_pack_start (box, frame, FALSE, FALSE, BORDER);
206 
207   priv->check_eject = gtk_check_button_new_with_mnemonic (_("E_ject disk"));
208   gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->check_eject), XFBURN_BLANK_DIALOG_EJECT_DEFAULT);
209   gtk_widget_show (priv->check_eject);
210   gtk_box_pack_start (GTK_BOX (vbox), priv->check_eject, FALSE, FALSE, BORDER);
211 
212   /* action buttons */
213   button = gtk_button_new_with_mnemonic (_("_Cancel"));
214   gtk_widget_show (button);
215   gtk_dialog_add_action_widget (GTK_DIALOG (obj), button, GTK_RESPONSE_CANCEL);
216 
217   button = xfce_gtk_button_new_mixed ("stock_xfburn-blank-cdrw", _("_Blank"));
218   gtk_widget_show (button);
219   gtk_dialog_add_action_widget (GTK_DIALOG (obj), button, GTK_RESPONSE_OK);
220   gtk_widget_set_can_default (button, TRUE);
221   gtk_widget_grab_focus (button);
222   gtk_widget_grab_default (button);
223   priv->button_blank = button;
224 
225   g_signal_connect (G_OBJECT (obj), "response", G_CALLBACK (xfburn_blank_dialog_response_cb), obj);
226   fill_combo_mode (obj);
227 }
228 
fill_combo_mode(XfburnBlankDialog * dialog)229 static void fill_combo_mode (XfburnBlankDialog *dialog)
230 {
231   XfburnBlankDialogPrivate *priv = XFBURN_BLANK_DIALOG_GET_PRIVATE (dialog);
232   XfburnBlankMode mode = XFBURN_BLANK_FAST;
233   GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (priv->combo_type));
234   int n = 0;
235 
236   gtk_list_store_clear (GTK_LIST_STORE (model));
237 
238   while (mode < XFBURN_BLANK_MODE_LAST) {
239     if (is_valid_blank_mode (NULL, mode)) {
240       GtkTreeIter iter;
241 
242       gtk_list_store_append (GTK_LIST_STORE (model), &iter);
243       gtk_list_store_set (GTK_LIST_STORE (model), &iter, BLANK_COMBO_NAME_COLUMN, _(blank_mode_names[mode]), BLANK_COMBO_MODE_COLUMN, mode, -1);
244       n++;
245     }
246     mode++;
247   }
248   gtk_combo_box_set_active (GTK_COMBO_BOX (priv->combo_type), 0);
249   gtk_widget_set_sensitive (priv->button_blank, n > 0);
250 }
251 
is_valid_blank_mode(XfburnDevice * device,XfburnBlankMode mode)252 static gboolean is_valid_blank_mode (XfburnDevice *device, XfburnBlankMode mode)
253 {
254   int profile_no = 0;
255   gboolean erasable = FALSE;
256   enum burn_disc_status disc_state;
257 
258   XfburnDeviceList *devlist = xfburn_device_list_new ();
259 
260   g_object_get (G_OBJECT (xfburn_device_list_get_current_device (devlist)), "profile-no", &profile_no, "erasable", &erasable, "disc-status", &disc_state, NULL);
261   g_object_unref (devlist);
262 
263   if (profile_no == 0x13) {
264     /* in 0x14 no blanking is needed, we can only deformat */
265     if (mode == XFBURN_DEFORMAT_FAST || mode == XFBURN_DEFORMAT_COMPLETE)
266       return TRUE;
267     else
268       return FALSE;
269   }
270 
271   if (profile_no == 0x14 && (mode == XFBURN_FORMAT_FAST || mode == XFBURN_FORMAT_COMPLETE))
272       return TRUE;
273 
274   if (erasable && (disc_state != BURN_DISC_BLANK) && (mode == XFBURN_BLANK_FAST || mode == XFBURN_BLANK_COMPLETE))
275     return TRUE;
276 
277   return FALSE;
278 }
279 
280 /*
281 static GList * get_valid_blank_modes (XfburnDevice *device)
282 {
283   XfburnBlankMode mode = XFBURN_BLANK_FAST;
284   GList *modes = NULL;
285 
286   while (mode < XFBURN_BLANK_MODE_LAST) {
287     if (is_valid_blank_mode (device, mode))
288       modes = g_list_append (modes, GINT_TO_POINTER (mode));
289     mode++;
290   }
291 
292   return modes;
293 }
294 */
295 
296 static gboolean
thread_blank_perform_blank(ThreadBlankParams * params,struct burn_drive_info * drive_info)297 thread_blank_perform_blank (ThreadBlankParams * params, struct burn_drive_info *drive_info)
298 {
299   GtkWidget *dialog_progress = params->dialog_progress;
300 
301   struct burn_drive *drive;
302   enum burn_disc_status disc_state;
303   enum burn_drive_status drive_state;
304   struct burn_progress progress;
305 
306   int ret;
307   gboolean error = FALSE;
308   int error_code;
309   char msg_text[BURN_MSGS_MESSAGE_LEN];
310   int os_errno;
311   char severity[80];
312   const char *final_status_text;
313   XfburnProgressDialogStatus final_status;
314   gchar *final_message = NULL;
315 
316   drive = drive_info->drive;
317 
318   while (burn_drive_get_status (drive, NULL) != BURN_DRIVE_IDLE) {
319     usleep (1001);
320   }
321 
322   while ( (disc_state = burn_disc_get_status (drive)) == BURN_DISC_UNREADY)
323     usleep (1001);
324 
325   switch (disc_state) {
326   case BURN_DISC_BLANK:
327     if (params->blank_mode == XFBURN_BLANK_FAST || params->blank_mode == XFBURN_BLANK_COMPLETE) {
328       /* blanking can only be performed on blank discs, format and deformat are allowed to be blank ones */
329       xfburn_progress_dialog_burning_failed (XFBURN_PROGRESS_DIALOG (dialog_progress), _("The inserted disc is already blank."));
330       return FALSE;
331     } // fall through
332   case BURN_DISC_FULL:
333   case BURN_DISC_APPENDABLE:
334     /* these ones we can blank */
335     xfburn_progress_dialog_set_status_with_text (XFBURN_PROGRESS_DIALOG (dialog_progress), XFBURN_PROGRESS_DIALOG_STATUS_RUNNING, _("Ready"));
336     break;
337   case BURN_DISC_EMPTY:
338     xfburn_progress_dialog_burning_failed (XFBURN_PROGRESS_DIALOG (dialog_progress), _("No disc detected in the drive."));
339     return FALSE;
340   default:
341     //xfburn_progress_dialog_burning_failed (XFBURN_PROGRESS_DIALOG (dialog_progress), _("Cannot recognize drive and disc state."));
342     //return FALSE;
343     break;
344   }
345 
346   if (!burn_disc_erasable (drive)) {
347     xfburn_progress_dialog_burning_failed (XFBURN_PROGRESS_DIALOG (dialog_progress), _("Disc is not erasable."));
348     return FALSE;
349   }
350 
351   /* set us up to receive fatal errors */
352   ret = burn_msgs_set_severities ("ALL", "NEVER", "libburn");
353 
354   if (ret <= 0)
355     g_warning ("Failed to set libburn message severities, burn errors might not get detected!");
356 
357   switch (params->blank_mode) {
358     case XFBURN_BLANK_FAST:
359       //DBG ("blank_fast");
360       burn_disc_erase(drive, 1);
361       break;
362     case XFBURN_BLANK_COMPLETE:
363       //DBG ("blank_complete");
364       burn_disc_erase(drive, 0);
365       break;
366     case XFBURN_FORMAT_FAST:
367       //DBG ("format_fast");
368       burn_disc_format(drive, 0, 0);
369       break;
370     case XFBURN_FORMAT_COMPLETE:
371       //DBG ("format_complete");
372       burn_disc_format(drive, XFBURN_FORMAT_COMPLETE_SIZE, 1);
373       break;
374     case XFBURN_DEFORMAT_FAST:
375       //DBG ("deformat_fast");
376       burn_disc_erase(drive, 1);
377       break;
378     case XFBURN_DEFORMAT_COMPLETE:
379       //DBG ("deformat_complete");
380       burn_disc_erase(drive, 0);
381       break;
382     default:
383       g_error ("Invalid blank mode %d, this is a bug.", params->blank_mode);
384   }
385   sleep(1);
386 
387   xfburn_progress_dialog_set_status_with_text (XFBURN_PROGRESS_DIALOG (dialog_progress), XFBURN_PROGRESS_DIALOG_STATUS_RUNNING, _("Blanking disc..."));
388 
389   while ((drive_state = burn_drive_get_status (drive, &progress)) != BURN_DRIVE_IDLE) {
390     //DBG ("drive_state = %d", drive_state);
391     if(progress.sectors>0 && progress.sector>=0) {
392       gdouble percent = 1.0 + ((gdouble) progress.sector+1.0) / ((gdouble) progress.sectors) * 98.0;
393 
394       xfburn_progress_dialog_set_progress_bar_fraction (XFBURN_PROGRESS_DIALOG (dialog_progress), percent);
395     }
396     usleep(500000);
397   }
398 
399   /* check the libburn message queue for errors */
400   while ((ret = burn_msgs_obtain ("FAILURE", &error_code, msg_text, &os_errno, severity)) == 1) {
401     g_warning ("[%s] %d: %s (%d)", severity, error_code, msg_text, os_errno);
402     error = TRUE;
403   }
404 #ifdef DEBUG
405   while ((ret = burn_msgs_obtain ("ALL", &error_code, msg_text, &os_errno, severity)) == 1) {
406     g_warning ("[%s] %d: %s (%d)", severity, error_code, msg_text, os_errno);
407   }
408 #endif
409 
410   if (ret < 0)
411     g_warning ("Fatal error while trying to retrieve libburn message!");
412 
413   if (G_LIKELY (!error)) {
414     final_message = g_strdup_printf (_("Done"));
415     final_status = XFBURN_PROGRESS_DIALOG_STATUS_COMPLETED;
416   } else {
417     final_status_text  = _("Failure");
418     final_status = XFBURN_PROGRESS_DIALOG_STATUS_FAILED;
419     final_message = g_strdup_printf ("%s: %s", final_status_text, msg_text);
420   }
421 
422   xfburn_progress_dialog_set_status_with_text (XFBURN_PROGRESS_DIALOG (dialog_progress), final_status, final_message);
423   g_free (final_message);
424 
425   return TRUE;
426 }
427 
428 static void*
thread_blank(ThreadBlankParams * params)429 thread_blank (ThreadBlankParams * params)
430 {
431   struct burn_drive_info *drive_info = NULL;
432 
433   if (!xfburn_device_grab (params->device, &drive_info)) {
434     xfburn_progress_dialog_burning_failed (XFBURN_PROGRESS_DIALOG (params->dialog_progress), _("Unable to grab the drive."));
435   } else {
436     thread_blank_perform_blank (params, drive_info);
437     xfburn_device_release (drive_info, params->eject);
438   }
439 
440   g_free (params);
441 
442 #ifdef HAVE_GUDEV
443   gdk_threads_enter ();
444   DBG ("blanking done!");
445   xfburn_udev_manager_send_volume_changed ();
446   gdk_threads_leave ();
447 #endif
448   return NULL;
449 }
450 
451 static XfburnBlankMode
get_selected_mode(XfburnBlankDialogPrivate * priv)452 get_selected_mode (XfburnBlankDialogPrivate *priv)
453 {
454   GtkTreeModel *model;
455   GtkTreeIter iter;
456   XfburnBlankMode blank_mode;
457   gboolean ret;
458 
459   model = gtk_combo_box_get_model (GTK_COMBO_BOX (priv->combo_type));
460   ret = gtk_combo_box_get_active_iter (GTK_COMBO_BOX (priv->combo_type), &iter);
461   if (ret) {
462     gtk_tree_model_get (model, &iter, BLANK_COMBO_MODE_COLUMN, &blank_mode, -1);
463   } else {
464     g_warning("No blank mode selected, using default");
465     blank_mode = XFBURN_BLANK_FAST;
466   }
467   return blank_mode;
468 }
469 
470 static void
xfburn_blank_dialog_response_cb(XfburnBlankDialog * dialog,gint response_id,gpointer user_data)471 xfburn_blank_dialog_response_cb (XfburnBlankDialog * dialog, gint response_id, gpointer user_data)
472 {
473   if (response_id == GTK_RESPONSE_OK) {
474     XfburnBlankDialogPrivate *priv = XFBURN_BLANK_DIALOG_GET_PRIVATE (dialog);
475     XfburnDevice *device;
476 
477     GtkWidget *dialog_progress;
478     ThreadBlankParams *params = NULL;
479 
480     device = xfburn_device_box_get_selected_device (XFBURN_DEVICE_BOX (priv->device_box));
481 
482 
483     dialog_progress = xfburn_progress_dialog_new (GTK_WINDOW (dialog));
484     g_object_set (dialog_progress, "animate", TRUE, NULL);
485 
486     gtk_widget_hide (GTK_WIDGET (dialog));
487 
488     gtk_widget_show (dialog_progress);
489 
490     params = g_new0 (ThreadBlankParams, 1);
491     params->dialog_progress = dialog_progress;
492     params->device = device;
493     params->blank_mode = get_selected_mode (priv);
494     params->eject = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->check_eject));
495     g_thread_new ("xfburn_blank", (GThreadFunc) thread_blank, params);
496   } else {
497     xfburn_main_leave_window ();
498   }
499 }
500 
501 static void
cb_volume_changed(GtkWidget * device_box,gboolean device_changed,XfburnDevice * device,XfburnBlankDialog * dialog)502 cb_volume_changed (GtkWidget *device_box, gboolean device_changed, XfburnDevice *device, XfburnBlankDialog * dialog)
503 {
504   fill_combo_mode (dialog);
505 }
506 
507 
508 /* public */
509 GtkWidget *
xfburn_blank_dialog_new(void)510 xfburn_blank_dialog_new (void)
511 {
512   GtkWidget *obj;
513 
514   obj = GTK_WIDGET (g_object_new (XFBURN_TYPE_BLANK_DIALOG, NULL));
515   cb_volume_changed (NULL, TRUE, NULL, XFBURN_BLANK_DIALOG (obj));
516 
517   xfburn_main_enter_window ();
518 
519   return obj;
520 }
521 
522 GtkWidget *
xfburn_blank_dialog_new_eject(gboolean eject)523 xfburn_blank_dialog_new_eject (gboolean eject)
524 {
525   GtkWidget *obj;
526 
527   obj = xfburn_blank_dialog_new ();
528 
529   g_object_set (G_OBJECT (obj), "eject", eject, NULL);
530 
531   return obj;
532 }
533