~ubuntu-branches/debian/sid/gnome-shell/sid

« back to all changes in this revision

Viewing changes to js/ui/components/autorunManager.js

  • Committer: Package Import Robot
  • Author(s): Emilio Pozuelo Monfort, Petr Salinger, Emilio Pozuelo Monfort
  • Date: 2013-10-13 17:47:35 UTC
  • mfrom: (1.2.17) (18.1.41 experimental)
  • Revision ID: package-import@ubuntu.com-20131013174735-2npsu0w5wk0e6vgb
Tags: 3.8.4-4
[ Petr Salinger ]
* Restrict dependency on gir1.2-nmgtk-1.0 to linux-any (Closes: #726099)

[ Emilio Pozuelo Monfort ]
* Upload to unstable.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
2
 
 
3
const Lang = imports.lang;
 
4
const Gio = imports.gi.Gio;
 
5
const St = imports.gi.St;
 
6
 
 
7
const GnomeSession = imports.misc.gnomeSession;
 
8
const Main = imports.ui.main;
 
9
const MessageTray = imports.ui.messageTray;
 
10
const ShellMountOperation = imports.ui.shellMountOperation;
 
11
 
 
12
// GSettings keys
 
13
const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
 
14
const SETTING_DISABLE_AUTORUN = 'autorun-never';
 
15
const SETTING_START_APP = 'autorun-x-content-start-app';
 
16
const SETTING_IGNORE = 'autorun-x-content-ignore';
 
17
const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
 
18
 
 
19
const AutorunSetting = {
 
20
    RUN: 0,
 
21
    IGNORE: 1,
 
22
    FILES: 2,
 
23
    ASK: 3
 
24
};
 
25
 
 
26
// misc utils
 
27
function shouldAutorunMount(mount, forTransient) {
 
28
    let root = mount.get_root();
 
29
    let volume = mount.get_volume();
 
30
 
 
31
    if (!volume || (!volume.allowAutorun && forTransient))
 
32
        return false;
 
33
 
 
34
    if (root.is_native() && isMountRootHidden(root))
 
35
        return false;
 
36
 
 
37
    return true;
 
38
}
 
39
 
 
40
function isMountRootHidden(root) {
 
41
    let path = root.get_path();
 
42
 
 
43
    // skip any mounts in hidden directory hierarchies
 
44
    return (path.indexOf('/.') != -1);
 
45
}
 
46
 
 
47
function isMountNonLocal(mount) {
 
48
    // If the mount doesn't have an associated volume, that means it's
 
49
    // an uninteresting filesystem. Most devices that we care about will
 
50
    // have a mount, like media players and USB sticks.
 
51
    let volume = mount.get_volume();
 
52
    if (volume == null)
 
53
        return true;
 
54
 
 
55
    return (volume.get_identifier("class") == "network");
 
56
}
 
57
 
 
58
function startAppForMount(app, mount) {
 
59
    let files = [];
 
60
    let root = mount.get_root();
 
61
    let retval = false;
 
62
 
 
63
    files.push(root);
 
64
 
 
65
    try {
 
66
        retval = app.launch(files, 
 
67
                            global.create_app_launch_context())
 
68
    } catch (e) {
 
69
        log('Unable to launch the application ' + app.get_name()
 
70
            + ': ' + e.toString());
 
71
    }
 
72
 
 
73
    return retval;
 
74
}
 
75
 
 
76
/******************************************/
 
77
 
 
78
const HotplugSnifferIface = <interface name="org.gnome.Shell.HotplugSniffer">
 
79
<method name="SniffURI">
 
80
    <arg type="s" direction="in" />
 
81
    <arg type="as" direction="out" />
 
82
</method>
 
83
</interface>;
 
84
 
 
85
const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
 
86
function HotplugSniffer() {
 
87
    return new HotplugSnifferProxy(Gio.DBus.session,
 
88
                                   'org.gnome.Shell.HotplugSniffer',
 
89
                                   '/org/gnome/Shell/HotplugSniffer');
 
90
}
 
91
 
 
92
const ContentTypeDiscoverer = new Lang.Class({
 
93
    Name: 'ContentTypeDiscoverer',
 
94
 
 
95
    _init: function(callback) {
 
96
        this._callback = callback;
 
97
        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
 
98
    },
 
99
 
 
100
    guessContentTypes: function(mount) {
 
101
        let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN);
 
102
        let shouldScan = autorunEnabled && !isMountNonLocal(mount);
 
103
 
 
104
        if (shouldScan) {
 
105
            // guess mount's content types using GIO
 
106
            mount.guess_content_type(false, null,
 
107
                                     Lang.bind(this,
 
108
                                               this._onContentTypeGuessed));
 
109
        } else {
 
110
            this._emitCallback(mount, []);
 
111
        }
 
112
    },
 
113
 
 
114
    _onContentTypeGuessed: function(mount, res) {
 
115
        let contentTypes = [];
 
116
 
 
117
        try {
 
118
            contentTypes = mount.guess_content_type_finish(res);
 
119
        } catch (e) {
 
120
            log('Unable to guess content types on added mount ' + mount.get_name()
 
121
                + ': ' + e.toString());
 
122
        }
 
123
 
 
124
        if (contentTypes.length) {
 
125
            this._emitCallback(mount, contentTypes);
 
126
        } else {
 
127
            let root = mount.get_root();
 
128
 
 
129
            let hotplugSniffer = new HotplugSniffer();
 
130
            hotplugSniffer.SniffURIRemote(root.get_uri(),
 
131
                 Lang.bind(this, function([contentTypes]) {
 
132
                     this._emitCallback(mount, contentTypes);
 
133
                 }));
 
134
        }
 
135
    },
 
136
 
 
137
    _emitCallback: function(mount, contentTypes) {
 
138
        if (!contentTypes)
 
139
            contentTypes = [];
 
140
 
 
141
        // we're not interested in win32 software content types here
 
142
        contentTypes = contentTypes.filter(function(type) {
 
143
            return (type != 'x-content/win32-software');
 
144
        });
 
145
 
 
146
        let apps = [];
 
147
        contentTypes.forEach(function(type) {
 
148
            let app = Gio.app_info_get_default_for_type(type, false);
 
149
 
 
150
            if (app)
 
151
                apps.push(app);
 
152
        });
 
153
 
 
154
        if (apps.length == 0)
 
155
            apps.push(Gio.app_info_get_default_for_type('inode/directory', false));
 
156
 
 
157
        this._callback(mount, apps, contentTypes);
 
158
    }
 
159
});
 
160
 
 
161
const AutorunManager = new Lang.Class({
 
162
    Name: 'AutorunManager',
 
163
 
 
164
    _init: function() {
 
165
        this._session = new GnomeSession.SessionManager();
 
166
        this._volumeMonitor = Gio.VolumeMonitor.get();
 
167
 
 
168
        this._transDispatcher = new AutorunTransientDispatcher(this);
 
169
    },
 
170
 
 
171
    _ensureResidentSource: function() {
 
172
        if (this._residentSource)
 
173
            return;
 
174
 
 
175
        this._residentSource = new AutorunResidentSource(this);
 
176
        let destroyId = this._residentSource.connect('destroy', Lang.bind(this, function() {
 
177
            this._residentSource.disconnect(destroyId);
 
178
            this._residentSource = null;
 
179
        }));
 
180
    },
 
181
 
 
182
    enable: function() {
 
183
        this._scanMounts();
 
184
 
 
185
        this._mountAddedId = this._volumeMonitor.connect('mount-added', Lang.bind(this, this._onMountAdded));
 
186
        this._mountRemovedId = this._volumeMonitor.connect('mount-removed', Lang.bind(this, this._onMountRemoved));
 
187
    },
 
188
 
 
189
    disable: function() {
 
190
        if (this._residentSource)
 
191
            this._residentSource.destroy();
 
192
        this._volumeMonitor.disconnect(this._mountAddedId);
 
193
        this._volumeMonitor.disconnect(this._mountRemovedId);
 
194
    },
 
195
 
 
196
    _processMount: function(mount, hotplug) {
 
197
        let discoverer = new ContentTypeDiscoverer(Lang.bind(this, function(mount, apps, contentTypes) {
 
198
            this._ensureResidentSource();
 
199
            this._residentSource.addMount(mount, apps);
 
200
 
 
201
            if (hotplug)
 
202
                this._transDispatcher.addMount(mount, apps, contentTypes);
 
203
        }));
 
204
        discoverer.guessContentTypes(mount);
 
205
    },
 
206
 
 
207
    _scanMounts: function() {
 
208
        let mounts = this._volumeMonitor.get_mounts();
 
209
        mounts.forEach(Lang.bind(this, function(mount) {
 
210
            this._processMount(mount, false);
 
211
        }));
 
212
    },
 
213
 
 
214
    _onMountAdded: function(monitor, mount) {
 
215
        // don't do anything if our session is not the currently
 
216
        // active one
 
217
        if (!this._session.SessionIsActive)
 
218
            return;
 
219
 
 
220
        this._processMount(mount, true);
 
221
    },
 
222
 
 
223
    _onMountRemoved: function(monitor, mount) {
 
224
        this._transDispatcher.removeMount(mount);
 
225
        if (this._residentSource)
 
226
            this._residentSource.removeMount(mount);
 
227
    },
 
228
 
 
229
    ejectMount: function(mount) {
 
230
        let mountOp = new ShellMountOperation.ShellMountOperation(mount);
 
231
 
 
232
        // first, see if we have a drive
 
233
        let drive = mount.get_drive();
 
234
        let volume = mount.get_volume();
 
235
 
 
236
        if (drive &&
 
237
            drive.get_start_stop_type() == Gio.DriveStartStopType.SHUTDOWN &&
 
238
            drive.can_stop()) {
 
239
            drive.stop(0, mountOp.mountOp, null,
 
240
                       Lang.bind(this, this._onStop));
 
241
        } else {
 
242
            if (mount.can_eject()) {
 
243
                mount.eject_with_operation(0, mountOp.mountOp, null,
 
244
                                           Lang.bind(this, this._onEject));
 
245
            } else if (volume && volume.can_eject()) {
 
246
                volume.eject_with_operation(0, mountOp.mountOp, null,
 
247
                                            Lang.bind(this, this._onEject));
 
248
            } else if (drive && drive.can_eject()) {
 
249
                drive.eject_with_operation(0, mountOp.mountOp, null,
 
250
                                           Lang.bind(this, this._onEject));
 
251
            } else if (mount.can_unmount()) {
 
252
                mount.unmount_with_operation(0, mountOp.mountOp, null,
 
253
                                             Lang.bind(this, this._onUnmount));
 
254
            }
 
255
        }
 
256
    },
 
257
 
 
258
    _onUnmount: function(mount, res) {
 
259
        try {
 
260
            mount.unmount_with_operation_finish(res);
 
261
        } catch (e) {
 
262
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
 
263
                log('Unable to eject the mount ' + mount.get_name() 
 
264
                    + ': ' + e.toString());
 
265
        }
 
266
    },
 
267
 
 
268
    _onEject: function(source, res) {
 
269
        try {
 
270
            source.eject_with_operation_finish(res);
 
271
        } catch (e) {
 
272
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
 
273
                log('Unable to eject the drive ' + source.get_name()
 
274
                    + ': ' + e.toString());
 
275
        }
 
276
    },
 
277
 
 
278
    _onStop: function(drive, res) {
 
279
        try {
 
280
            drive.stop_finish(res);
 
281
        } catch (e) {
 
282
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
 
283
                log('Unable to stop the drive ' + drive.get_name() 
 
284
                    + ': ' + e.toString());
 
285
        }
 
286
    },
 
287
});
 
288
 
 
289
const AutorunResidentSource = new Lang.Class({
 
290
    Name: 'AutorunResidentSource',
 
291
    Extends: MessageTray.Source,
 
292
 
 
293
    _init: function(manager) {
 
294
        this.parent(_("Removable Devices"), 'media-removable');
 
295
        this.resident = true;
 
296
 
 
297
        this._mounts = [];
 
298
 
 
299
        this._manager = manager;
 
300
        this._notification = new AutorunResidentNotification(this._manager, this);
 
301
    },
 
302
 
 
303
    _createPolicy: function() {
 
304
        return new MessageTray.NotificationPolicy({ showInLockScreen: false });
 
305
    },
 
306
 
 
307
    buildRightClickMenu: function() {
 
308
        return null;
 
309
    },
 
310
 
 
311
    addMount: function(mount, apps) {
 
312
        if (!shouldAutorunMount(mount, false))
 
313
            return;
 
314
 
 
315
        let filtered = this._mounts.filter(function (element) {
 
316
            return (element.mount == mount);
 
317
        });
 
318
 
 
319
        if (filtered.length != 0)
 
320
            return;
 
321
 
 
322
        let element = { mount: mount, apps: apps };
 
323
        this._mounts.push(element);
 
324
        this._redisplay();
 
325
    },
 
326
 
 
327
    removeMount: function(mount) {
 
328
        this._mounts =
 
329
            this._mounts.filter(function (element) {
 
330
                return (element.mount != mount);
 
331
            });
 
332
 
 
333
        this._redisplay();
 
334
    },
 
335
 
 
336
    _redisplay: function() {
 
337
        if (this._mounts.length == 0) {
 
338
            this._notification.destroy();
 
339
            this.destroy();
 
340
 
 
341
            return;
 
342
        }
 
343
 
 
344
        this._notification.updateForMounts(this._mounts);
 
345
 
 
346
        // add ourselves as a source, and push the notification
 
347
        if (!Main.messageTray.contains(this)) {
 
348
            Main.messageTray.add(this);
 
349
            this.pushNotification(this._notification);
 
350
        }
 
351
    }
 
352
});
 
353
 
 
354
const AutorunResidentNotification = new Lang.Class({
 
355
    Name: 'AutorunResidentNotification',
 
356
    Extends: MessageTray.Notification,
 
357
 
 
358
    _init: function(manager, source) {
 
359
        this.parent(source, source.title, null, { customContent: true });
 
360
 
 
361
        // set the notification as resident
 
362
        this.setResident(true);
 
363
 
 
364
        this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
 
365
                                           vertical: true });
 
366
        this._manager = manager;
 
367
 
 
368
        this.addActor(this._layout,
 
369
                      { x_expand: true,
 
370
                        x_fill: true });
 
371
    },
 
372
 
 
373
    updateForMounts: function(mounts) {
 
374
        // remove all the layout content
 
375
        this._layout.destroy_all_children();
 
376
 
 
377
        for (let idx = 0; idx < mounts.length; idx++) {
 
378
            let element = mounts[idx];
 
379
 
 
380
            let actor = this._itemForMount(element.mount, element.apps);
 
381
            this._layout.add(actor, { x_fill: true,
 
382
                                      expand: true });
 
383
        }
 
384
    },
 
385
 
 
386
    _itemForMount: function(mount, apps) {
 
387
        let item = new St.BoxLayout();
 
388
 
 
389
        // prepare the mount button content
 
390
        let mountLayout = new St.BoxLayout();
 
391
 
 
392
        let mountIcon = new St.Icon({ gicon: mount.get_icon(),
 
393
                                      style_class: 'hotplug-resident-mount-icon' });
 
394
        mountLayout.add_actor(mountIcon);
 
395
 
 
396
        let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
 
397
        let mountLabel =
 
398
            new St.Label({ text: mount.get_name(),
 
399
                           style_class: 'hotplug-resident-mount-label',
 
400
                           track_hover: true,
 
401
                           reactive: true });
 
402
        labelBin.add_actor(mountLabel);
 
403
        mountLayout.add_actor(labelBin);
 
404
 
 
405
        let mountButton = new St.Button({ child: mountLayout,
 
406
                                          x_align: St.Align.START,
 
407
                                          x_fill: true,
 
408
                                          style_class: 'hotplug-resident-mount',
 
409
                                          button_mask: St.ButtonMask.ONE });
 
410
        item.add(mountButton, { x_align: St.Align.START,
 
411
                                expand: true });
 
412
 
 
413
        let ejectIcon = 
 
414
            new St.Icon({ icon_name: 'media-eject-symbolic',
 
415
                          style_class: 'hotplug-resident-eject-icon' });
 
416
 
 
417
        let ejectButton =
 
418
            new St.Button({ style_class: 'hotplug-resident-eject-button',
 
419
                            button_mask: St.ButtonMask.ONE,
 
420
                            child: ejectIcon });
 
421
        item.add(ejectButton, { x_align: St.Align.END });
 
422
 
 
423
        // now connect signals
 
424
        mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
 
425
            startAppForMount(apps[0], mount);
 
426
        }));
 
427
 
 
428
        ejectButton.connect('clicked', Lang.bind(this, function() {
 
429
            this._manager.ejectMount(mount);
 
430
        }));
 
431
 
 
432
        return item;
 
433
    },
 
434
});
 
435
 
 
436
const AutorunTransientDispatcher = new Lang.Class({
 
437
    Name: 'AutorunTransientDispatcher',
 
438
 
 
439
    _init: function(manager) {
 
440
        this._manager = manager;
 
441
        this._sources = [];
 
442
        this._settings = new Gio.Settings({ schema: SETTINGS_SCHEMA });
 
443
    },
 
444
 
 
445
    _getAutorunSettingForType: function(contentType) {
 
446
        let runApp = this._settings.get_strv(SETTING_START_APP);
 
447
        if (runApp.indexOf(contentType) != -1)
 
448
            return AutorunSetting.RUN;
 
449
 
 
450
        let ignore = this._settings.get_strv(SETTING_IGNORE);
 
451
        if (ignore.indexOf(contentType) != -1)
 
452
            return AutorunSetting.IGNORE;
 
453
 
 
454
        let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
 
455
        if (openFiles.indexOf(contentType) != -1)
 
456
            return AutorunSetting.FILES;
 
457
 
 
458
        return AutorunSetting.ASK;
 
459
    },
 
460
 
 
461
    _getSourceForMount: function(mount) {
 
462
        let filtered =
 
463
            this._sources.filter(function (source) {
 
464
                return (source.mount == mount);
 
465
            });
 
466
 
 
467
        // we always make sure not to add two sources for the same
 
468
        // mount in addMount(), so it's safe to assume filtered.length
 
469
        // is always either 1 or 0.
 
470
        if (filtered.length == 1)
 
471
            return filtered[0];
 
472
 
 
473
        return null;
 
474
    },
 
475
 
 
476
    _addSource: function(mount, apps) {
 
477
        // if we already have a source showing for this 
 
478
        // mount, return
 
479
        if (this._getSourceForMount(mount))
 
480
            return;
 
481
     
 
482
        // add a new source
 
483
        this._sources.push(new AutorunTransientSource(this._manager, mount, apps));
 
484
    },
 
485
 
 
486
    addMount: function(mount, apps, contentTypes) {
 
487
        // if autorun is disabled globally, return
 
488
        if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
 
489
            return;
 
490
 
 
491
        // if the mount doesn't want to be autorun, return
 
492
        if (!shouldAutorunMount(mount, true))
 
493
            return;
 
494
 
 
495
        let setting = this._getAutorunSettingForType(contentTypes[0]);
 
496
 
 
497
        // check at the settings for the first content type
 
498
        // to see whether we should ask
 
499
        if (setting == AutorunSetting.IGNORE)
 
500
            return; // return right away
 
501
 
 
502
        let success = false;
 
503
        let app = null;
 
504
 
 
505
        if (setting == AutorunSetting.RUN) {
 
506
            app = Gio.app_info_get_default_for_type(contentTypes[0], false);
 
507
        } else if (setting == AutorunSetting.FILES) {
 
508
            app = Gio.app_info_get_default_for_type('inode/directory', false);
 
509
        }
 
510
 
 
511
        if (app)
 
512
            success = startAppForMount(app, mount);
 
513
 
 
514
        // we fallback here also in case the settings did not specify 'ask',
 
515
        // but we failed launching the default app or the default file manager
 
516
        if (!success)
 
517
            this._addSource(mount, apps);
 
518
    },
 
519
 
 
520
    removeMount: function(mount) {
 
521
        let source = this._getSourceForMount(mount);
 
522
        
 
523
        // if we aren't tracking this mount, don't do anything
 
524
        if (!source)
 
525
            return;
 
526
 
 
527
        // destroy the notification source
 
528
        source.destroy();
 
529
    }
 
530
});
 
531
 
 
532
const AutorunTransientSource = new Lang.Class({
 
533
    Name: 'AutorunTransientSource',
 
534
    Extends: MessageTray.Source,
 
535
 
 
536
    _init: function(manager, mount, apps) {
 
537
        this._manager = manager;
 
538
        this.mount = mount;
 
539
        this.apps = apps;
 
540
 
 
541
        this.parent(mount.get_name());
 
542
 
 
543
        this._notification = new AutorunTransientNotification(this._manager, this);
 
544
 
 
545
        // add ourselves as a source, and popup the notification
 
546
        Main.messageTray.add(this);
 
547
        this.notify(this._notification);
 
548
    },
 
549
 
 
550
    getIcon: function() {
 
551
        return this.mount.get_icon();
 
552
    }
 
553
});
 
554
 
 
555
const AutorunTransientNotification = new Lang.Class({
 
556
    Name: 'AutorunTransientNotification',
 
557
    Extends: MessageTray.Notification,
 
558
 
 
559
    _init: function(manager, source) {
 
560
        this.parent(source, source.title, null, { customContent: true });
 
561
 
 
562
        this._manager = manager;
 
563
        this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
 
564
                                       vertical: true });
 
565
        this.addActor(this._box);
 
566
 
 
567
        this._mount = source.mount;
 
568
 
 
569
        source.apps.forEach(Lang.bind(this, function (app) {
 
570
            let actor = this._buttonForApp(app);
 
571
 
 
572
            if (actor)
 
573
                this._box.add(actor, { x_fill: true,
 
574
                                       x_align: St.Align.START });
 
575
        }));
 
576
 
 
577
        this._box.add(this._buttonForEject(), { x_fill: true,
 
578
                                                x_align: St.Align.START });
 
579
 
 
580
        // set the notification to transient and urgent, so that it
 
581
        // expands out
 
582
        this.setTransient(true);
 
583
        this.setUrgency(MessageTray.Urgency.CRITICAL);
 
584
    },
 
585
 
 
586
    _buttonForApp: function(app) {
 
587
        let box = new St.BoxLayout();
 
588
        let icon = new St.Icon({ gicon: app.get_icon(),
 
589
                                 style_class: 'hotplug-notification-item-icon' });
 
590
        box.add(icon);
 
591
 
 
592
        let label = new St.Bin({ y_align: St.Align.MIDDLE,
 
593
                                 child: new St.Label
 
594
                                 ({ text: _("Open with %s").format(app.get_name()) })
 
595
                               });
 
596
        box.add(label);
 
597
 
 
598
        let button = new St.Button({ child: box,
 
599
                                     x_fill: true,
 
600
                                     x_align: St.Align.START,
 
601
                                     button_mask: St.ButtonMask.ONE,
 
602
                                     style_class: 'hotplug-notification-item' });
 
603
 
 
604
        button.connect('clicked', Lang.bind(this, function() {
 
605
            startAppForMount(app, this._mount);
 
606
            this.destroy();
 
607
        }));
 
608
 
 
609
        return button;
 
610
    },
 
611
 
 
612
    _buttonForEject: function() {
 
613
        let box = new St.BoxLayout();
 
614
        let icon = new St.Icon({ icon_name: 'media-eject-symbolic',
 
615
                                 style_class: 'hotplug-notification-item-icon' });
 
616
        box.add(icon);
 
617
 
 
618
        let label = new St.Bin({ y_align: St.Align.MIDDLE,
 
619
                                 child: new St.Label
 
620
                                 ({ text: _("Eject") })
 
621
                               });
 
622
        box.add(label);
 
623
 
 
624
        let button = new St.Button({ child: box,
 
625
                                     x_fill: true,
 
626
                                     x_align: St.Align.START,
 
627
                                     button_mask: St.ButtonMask.ONE,
 
628
                                     style_class: 'hotplug-notification-item' });
 
629
 
 
630
        button.connect('clicked', Lang.bind(this, function() {
 
631
            this._manager.ejectMount(this._mount);
 
632
        }));
 
633
 
 
634
        return button;
 
635
    }
 
636
});
 
637
 
 
638
const Component = AutorunManager;