ca2e09fe8b
Since we eventually want to add a system for changing the top panel contents depending on the current state of the shell, let's use the "session mode" feature for this, and add a mechanism for updating the session mode at runtime. Add support for every key besides the two functional keys, and make all the components update automatically when the session mode is changed. Add a new lock-screen mode, and make the lock screen change to this when locked. https://bugzilla.gnome.org/show_bug.cgi?id=683156
245 lines
9.2 KiB
JavaScript
245 lines
9.2 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Lang = imports.lang;
|
|
const Gvc = imports.gi.Gvc;
|
|
const St = imports.gi.St;
|
|
|
|
const PanelMenu = imports.ui.panelMenu;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
|
|
const VOLUME_ADJUSTMENT_STEP = 0.05; /* Volume adjustment step in % */
|
|
|
|
const VOLUME_NOTIFY_ID = 1;
|
|
|
|
// Each Gvc.MixerControl is a connection to PulseAudio,
|
|
// so it's better to make it a singleton
|
|
let _mixerControl;
|
|
function getMixerControl() {
|
|
if (_mixerControl)
|
|
return _mixerControl;
|
|
|
|
_mixerControl = new Gvc.MixerControl({ name: 'GNOME Shell Volume Control' });
|
|
_mixerControl.open();
|
|
|
|
return _mixerControl;
|
|
}
|
|
|
|
const VolumeMenu = new Lang.Class({
|
|
Name: 'VolumeMenu',
|
|
Extends: PopupMenu.PopupMenuSection,
|
|
|
|
_init: function(control) {
|
|
this.parent();
|
|
|
|
this._control = control;
|
|
this._control.connect('state-changed', Lang.bind(this, this._onControlStateChanged));
|
|
this._control.connect('default-sink-changed', Lang.bind(this, this._readOutput));
|
|
this._control.connect('default-source-changed', Lang.bind(this, this._readInput));
|
|
this._control.connect('stream-added', Lang.bind(this, this._maybeShowInput));
|
|
this._control.connect('stream-removed', Lang.bind(this, this._maybeShowInput));
|
|
this._volumeMax = this._control.get_vol_max_norm();
|
|
|
|
this._output = null;
|
|
this._outputVolumeId = 0;
|
|
this._outputMutedId = 0;
|
|
/* Translators: This is the label for audio volume */
|
|
this._outputTitle = new PopupMenu.PopupMenuItem(_("Volume"), { reactive: false });
|
|
this._outputSlider = new PopupMenu.PopupSliderMenuItem(0);
|
|
this._outputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_output'));
|
|
this._outputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
|
|
this.addMenuItem(this._outputTitle);
|
|
this.addMenuItem(this._outputSlider);
|
|
|
|
this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
|
|
this._input = null;
|
|
this._inputVolumeId = 0;
|
|
this._inputMutedId = 0;
|
|
this._inputTitle = new PopupMenu.PopupMenuItem(_("Microphone"), { reactive: false });
|
|
this._inputSlider = new PopupMenu.PopupSliderMenuItem(0);
|
|
this._inputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_input'));
|
|
this._inputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
|
|
this.addMenuItem(this._inputTitle);
|
|
this.addMenuItem(this._inputSlider);
|
|
},
|
|
|
|
scroll: function(direction) {
|
|
let currentVolume = this._output.volume;
|
|
|
|
if (direction == Clutter.ScrollDirection.DOWN) {
|
|
let prev_muted = this._output.is_muted;
|
|
this._output.volume = Math.max(0, currentVolume - this._volumeMax * VOLUME_ADJUSTMENT_STEP);
|
|
if (this._output.volume < 1) {
|
|
this._output.volume = 0;
|
|
if (!prev_muted)
|
|
this._output.change_is_muted(true);
|
|
}
|
|
this._output.push_volume();
|
|
}
|
|
else if (direction == Clutter.ScrollDirection.UP) {
|
|
this._output.volume = Math.min(this._volumeMax, currentVolume + this._volumeMax * VOLUME_ADJUSTMENT_STEP);
|
|
this._output.change_is_muted(false);
|
|
this._output.push_volume();
|
|
}
|
|
|
|
this._notifyVolumeChange();
|
|
},
|
|
|
|
_onControlStateChanged: function() {
|
|
if (this._control.get_state() == Gvc.MixerControlState.READY) {
|
|
this._readOutput();
|
|
this._readInput();
|
|
} else {
|
|
this.emit('icon-changed', null);
|
|
}
|
|
},
|
|
|
|
_readOutput: function() {
|
|
if (this._outputVolumeId) {
|
|
this._output.disconnect(this._outputVolumeId);
|
|
this._output.disconnect(this._outputMutedId);
|
|
this._outputVolumeId = 0;
|
|
this._outputMutedId = 0;
|
|
}
|
|
this._output = this._control.get_default_sink();
|
|
if (this._output) {
|
|
this._outputMutedId = this._output.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_output'));
|
|
this._outputVolumeId = this._output.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_output'));
|
|
this._mutedChanged (null, null, '_output');
|
|
this._volumeChanged (null, null, '_output');
|
|
} else {
|
|
this._outputSlider.setValue(0);
|
|
this.emit('icon-changed', 'audio-volume-muted-symbolic');
|
|
}
|
|
},
|
|
|
|
_readInput: function() {
|
|
if (this._inputVolumeId) {
|
|
this._input.disconnect(this._inputVolumeId);
|
|
this._input.disconnect(this._inputMutedId);
|
|
this._inputVolumeId = 0;
|
|
this._inputMutedId = 0;
|
|
}
|
|
this._input = this._control.get_default_source();
|
|
if (this._input) {
|
|
this._inputMutedId = this._input.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_input'));
|
|
this._inputVolumeId = this._input.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_input'));
|
|
this._mutedChanged (null, null, '_input');
|
|
this._volumeChanged (null, null, '_input');
|
|
} else {
|
|
this._inputTitle.actor.hide();
|
|
this._inputSlider.actor.hide();
|
|
}
|
|
},
|
|
|
|
_maybeShowInput: function() {
|
|
// only show input widgets if any application is recording audio
|
|
let showInput = false;
|
|
let recordingApps = this._control.get_source_outputs();
|
|
if (this._input && recordingApps) {
|
|
for (let i = 0; i < recordingApps.length; i++) {
|
|
let outputStream = recordingApps[i];
|
|
let id = outputStream.get_application_id();
|
|
// but skip gnome-volume-control and pavucontrol
|
|
// (that appear as recording because they show the input level)
|
|
if (!id || (id != 'org.gnome.VolumeControl' && id != 'org.PulseAudio.pavucontrol')) {
|
|
showInput = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._inputTitle.actor.visible = showInput;
|
|
this._inputSlider.actor.visible = showInput;
|
|
},
|
|
|
|
_volumeToIcon: function(volume) {
|
|
if (volume <= 0) {
|
|
return 'audio-volume-muted-symbolic';
|
|
} else {
|
|
let n = Math.floor(3 * volume / this._volumeMax) + 1;
|
|
if (n < 2)
|
|
return 'audio-volume-low-symbolic';
|
|
if (n >= 3)
|
|
return 'audio-volume-high-symbolic';
|
|
return 'audio-volume-medium-symbolic';
|
|
}
|
|
},
|
|
|
|
_sliderChanged: function(slider, value, property) {
|
|
if (this[property] == null) {
|
|
log ('Volume slider changed for %s, but %s does not exist'.format(property, property));
|
|
return;
|
|
}
|
|
let volume = value * this._volumeMax;
|
|
let prev_muted = this[property].is_muted;
|
|
if (volume < 1) {
|
|
this[property].volume = 0;
|
|
if (!prev_muted)
|
|
this[property].change_is_muted(true);
|
|
} else {
|
|
this[property].volume = volume;
|
|
if (prev_muted)
|
|
this[property].change_is_muted(false);
|
|
}
|
|
this[property].push_volume();
|
|
},
|
|
|
|
_notifyVolumeChange: function() {
|
|
global.cancel_theme_sound(VOLUME_NOTIFY_ID);
|
|
global.play_theme_sound(VOLUME_NOTIFY_ID, 'audio-volume-change');
|
|
},
|
|
|
|
_mutedChanged: function(object, param_spec, property) {
|
|
let muted = this[property].is_muted;
|
|
let slider = this[property+'Slider'];
|
|
slider.setValue(muted ? 0 : (this[property].volume / this._volumeMax));
|
|
if (property == '_output') {
|
|
if (muted)
|
|
this.emit('icon-changed', 'audio-volume-muted-symbolic');
|
|
else
|
|
this.emit('icon-changed', this._volumeToIcon(this._output.volume));
|
|
}
|
|
},
|
|
|
|
_volumeChanged: function(object, param_spec, property) {
|
|
this[property+'Slider'].setValue(this[property].volume / this._volumeMax);
|
|
if (property == '_output' && !this._output.is_muted)
|
|
this.emit('icon-changed', this._volumeToIcon(this._output.volume));
|
|
}
|
|
});
|
|
|
|
const Indicator = new Lang.Class({
|
|
Name: 'VolumeIndicator',
|
|
Extends: PanelMenu.SystemStatusButton,
|
|
|
|
_init: function() {
|
|
this.parent('audio-volume-muted-symbolic', _("Volume"));
|
|
|
|
this._control = getMixerControl();
|
|
this._volumeMenu = new VolumeMenu(this._control);
|
|
this._volumeMenu.connect('icon-changed', Lang.bind(this, function(menu, icon) {
|
|
this._hasPulseAudio = (icon != null);
|
|
this.setIcon(icon);
|
|
this._syncVisibility();
|
|
}));
|
|
|
|
this.menu.addMenuItem(this._volumeMenu);
|
|
|
|
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
this.menu.addSettingsAction(_("Sound Settings"), 'gnome-sound-panel.desktop');
|
|
|
|
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
|
|
},
|
|
|
|
_syncVisibility: function() {
|
|
this.actor.visible = this._hasPulseAudio;
|
|
this.mainIcon.visible = this._hasPulseAudio;
|
|
},
|
|
|
|
_onScrollEvent: function(actor, event) {
|
|
this._volumeMenu.scroll(event.get_scroll_direction());
|
|
}
|
|
});
|