You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
13 KiB

/**
* MenuItem module
*
* @author Deminder <tremminder@gmail.com>
* @copyright 2021
* @license GNU General Public License v3.0
*/
/* exported ShutdownTimer, init, uninit, MODES */
const { GObject, St, Gio, Clutter } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const { Convenience, InfoFetcher, ScheduleInfo } = Me.imports.lib;
const {
logDebug,
modeLabel,
MODES,
WAKE_MODES,
durationString,
longDurationString,
guiIdle,
} = Convenience;
// menu items
const PopupMenu = imports.ui.popupMenu;
const Slider = imports.ui.slider;
// translations
const Gettext = imports.gettext.domain('ShutdownTimer');
const _ = Gettext.gettext;
const _n = Gettext.ngettext;
const C_ = Gettext.pgettext;
let ACTIONS;
let settings;
var ShutdownTimer = GObject.registerClass(
class ShutdownTimer extends PopupMenu.PopupSubMenuMenuItem {
_init() {
super._init('', true);
// track external shutdown and wake schedule
this.infoFetcher = new InfoFetcher.InfoFetcher(
this._externalScheduleInfoTick.bind(this)
);
this.checkRunning = false;
this.externalScheduleInfo = new ScheduleInfo.ScheduleInfo({
external: true,
});
this.externalWakeInfo = new ScheduleInfo.ScheduleInfo({
external: false,
mode: 'wake',
});
this.internalScheduleInfo = new ScheduleInfo.ScheduleInfo({
external: false,
deadline: settings.get_int('shutdown-timestamp-value'),
mode: settings.get_string('shutdown-mode-value'),
});
// submenu in status area menu with slider and toggle button
this.sliderItems = {};
this.sliders = {};
['shutdown', 'wake'].forEach(prefix => {
const [item, slider] = _createSliderItem(prefix);
this.sliderItems[prefix] = item;
this.sliders[prefix] = slider;
this._onShowSliderChanged(prefix);
});
this.switcher = new PopupMenu.PopupSwitchMenuItem('', false);
_connect(this.switcher, [['toggled', this._onToggle.bind(this)]]);
this.switcherSettingsButton = new St.Button({
reactive: true,
can_focus: true,
track_hover: true,
accessible_name: _('Settings'),
style_class: 'system-menu-action settings-button',
});
this.switcherSettingsButton.child = new St.Icon({
icon_name: 'emblem-system-symbolic',
style_class: 'popup-menu-icon',
});
_connect(this.switcherSettingsButton, [
[
'clicked',
async () => {
try {
const r = ExtensionUtils.openPrefs();
if (r) {
await r;
}
} catch {
logDebug('failed to open preferences!');
}
},
],
]);
this.switcher.add_child(this.switcherSettingsButton);
this._onShowSettingsButtonChanged();
this._updateSwitchLabel();
this.icon.icon_name = 'preferences-system-time-symbolic';
this.menu.addMenuItem(this.switcher);
// make switcher toggle without popup menu closing
this.switcher.activate = __ => {
if (this.switcher._switch.mapped) {
this.switcher.toggle();
}
};
this.menu.addMenuItem(this.sliderItems['shutdown']);
this.modeItems = MODES.map(mode => {
const modeItem = new PopupMenu.PopupMenuItem(modeLabel(mode));
_connect(modeItem, [
[
'activate',
() => {
this._startMode(mode);
},
],
]);
this.menu.addMenuItem(modeItem);
return [mode, modeItem];
});
this.wakeItems = [
new PopupMenu.PopupSeparatorMenuItem(),
this.sliderItems['wake'],
...WAKE_MODES.map(mode => {
const modeItem = new PopupMenu.PopupMenuItem(modeLabel(mode));
if (mode === 'wake') {
this.wakeModeItem = modeItem;
}
_connect(modeItem, [
[
'activate',
() => ACTIONS.wakeAction(mode, _getSliderMinutes('wake')),
],
]);
return modeItem;
}),
];
this._updateWakeModeItem();
this.wakeItems.forEach(item => {
this.menu.addMenuItem(item);
});
this._updateShownWakeItems();
this._updateShownModeItems();
this._updateSelectedModeItems();
this._onInternalShutdownTimestampChanged();
// handlers for changed values in settings
this.settingsHandlerIds = [
['shutdown-max-timer-value', this._updateSwitchLabel.bind(this)],
['nonlinear-shutdown-slider-value', this._updateSwitchLabel.bind(this)],
['wake-max-timer-value', this._updateWakeModeItem.bind(this)],
['nonlinear-wake-slider-value', this._updateWakeModeItem.bind(this)],
[
'shutdown-slider-value',
() => {
this._updateSlider('shutdown');
this._updateSwitchLabel();
},
],
[
'wake-slider-value',
() => {
this._updateSlider('wake');
this._updateWakeModeItem();
},
],
['root-mode-value', this._onRootModeChanged.bind(this)],
['show-settings-value', this._onShowSettingsButtonChanged.bind(this)],
[
'show-shutdown-slider-value',
() => this._onShowSliderChanged('shutdown'),
],
['show-wake-slider-value', () => this._onShowSliderChanged('wake')],
['show-wake-items-value', this._updateShownWakeItems.bind(this)],
['show-shutdown-mode-value', this._updateShownModeItems.bind(this)],
['shutdown-mode-value', this._onModeChange.bind(this)],
[
'shutdown-timestamp-value',
this._onInternalShutdownTimestampChanged.bind(this),
],
].map(([label, func]) => settings.connect(`changed::${label}`, func));
}
_onRootModeChanged() {
Promise.all([
ACTIONS.maybeStopRootModeProtection(this.internalScheduleInfo),
ACTIONS.maybeStartRootModeProtection(this.internalScheduleInfo),
]).then(() => {
this._updateSwitchLabel();
});
}
_onModeChange() {
// redo Root-mode protection
ACTIONS.maybeStopRootModeProtection(this.internalScheduleInfo, true)
.then(() => {
this._updateCurrentMode();
logDebug(`Shutdown mode: ${this.internalScheduleInfo.mode}`);
guiIdle(this._updateSelectedModeItems.bind(this));
})
.then(() =>
ACTIONS.maybeStartRootModeProtection(this.internalScheduleInfo)
);
}
_updateCurrentMode() {
this.internalScheduleInfo = this.internalScheduleInfo.copy({
mode: settings.get_string('shutdown-mode-value'),
});
ACTIONS.onShutdownScheduleChange(this.internalScheduleInfo);
this.updateShutdownInfo();
}
_onInternalShutdownTimestampChanged() {
this.internalScheduleInfo = this.internalScheduleInfo.copy({
deadline: settings.get_int('shutdown-timestamp-value'),
});
ACTIONS.onShutdownScheduleChange(this.internalScheduleInfo);
this.switcher.setToggleState(this.internalScheduleInfo.scheduled);
this.updateShutdownInfo();
}
/* Schedule Info updates */
_externalScheduleInfoTick(info, wakeInfo) {
this.externalScheduleInfo = this.externalScheduleInfo.copy({
...info,
});
this.externalWakeInfo = this.externalWakeInfo.copy({ ...wakeInfo });
guiIdle(this.updateShutdownInfo.bind(this));
}
_updateShownModeItems() {
const activeModes = settings
.get_string('show-shutdown-mode-value')
.split(',')
.map(s => s.trim().toLowerCase())
.filter(s => MODES.includes(s));
this.modeItems.forEach(([mode, item]) => {
const position = activeModes.indexOf(mode);
if (position > -1) {
this.menu.moveMenuItem(item, position + 2);
}
item.visible = position > -1;
});
}
updateShutdownInfo() {
let shutdownLabel;
if (this.internalScheduleInfo.scheduled && this.checkRunning) {
const secPassed = Math.max(0, -this.internalScheduleInfo.secondsLeft);
shutdownLabel = _('Check %s for %s').format(
this.internalScheduleInfo.modeText,
durationString(secPassed)
);
} else {
const info = this.externalScheduleInfo.isMoreUrgendThan(
this.internalScheduleInfo
)
? this.externalScheduleInfo
: this.internalScheduleInfo;
shutdownLabel = info.label;
}
this.label.text =
[shutdownLabel, this.externalWakeInfo.label]
.filter(v => !!v)
.join('\n') || _('Shutdown Timer');
}
_updateSelectedModeItems() {
this.modeItems.forEach(([mode, item]) => {
item.setOrnament(
mode === this.internalScheduleInfo.mode
? PopupMenu.Ornament.DOT
: PopupMenu.Ornament.NONE
);
});
}
// update timer value if slider has changed
_updateSlider(prefix) {
this.sliders[prefix].value =
settings.get_double(`${prefix}-slider-value`) / 100.0;
}
_updateSwitchLabel() {
const minutes = Math.abs(_getSliderMinutes('shutdown'));
const timeStr = longDurationString(
minutes,
h => _n('%s hr', '%s hrs', h),
m => _n('%s min', '%s mins', m)
);
this.switcher.label.text = settings.get_boolean('root-mode-value')
? _('%s (protect)').format(timeStr)
: timeStr;
}
_updateWakeModeItem() {
const minutes = Math.abs(_getSliderMinutes('wake'));
this.wakeModeItem.label.text = C_('WakeButtonText', '%s %s').format(
modeLabel('wake'),
longDurationString(
minutes,
h => _n('%s hour', '%s hours', h),
m => _n('%s minute', '%s minutes', m)
)
);
}
_onShowSettingsButtonChanged() {
this.switcherSettingsButton.visible = settings.get_boolean(
'show-settings-value'
);
}
_updateShownWakeItems() {
this.wakeItems.forEach(item => {
item.visible = settings.get_boolean('show-wake-items-value');
});
this._onShowSliderChanged('wake');
}
_onShowSliderChanged(settingsPrefix) {
this.sliderItems[settingsPrefix].visible =
(settingsPrefix !== 'wake' ||
settings.get_boolean('show-wake-items-value')) &&
settings.get_boolean(`show-${settingsPrefix}-slider-value`);
}
_startMode(mode) {
settings.set_string('shutdown-mode-value', mode);
ACTIONS.startSchedule(
_getSliderMinutes('shutdown'),
_getSliderMinutes('wake')
);
}
// toggle button starts/stops shutdown timer
_onToggle() {
if (this.switcher.state) {
// start shutdown timer
ACTIONS.startSchedule(
_getSliderMinutes('shutdown'),
_getSliderMinutes('wake')
);
} else {
// stop shutdown timer
ACTIONS.stopSchedule();
}
}
destroy() {
this.infoFetcher.stop();
this.settingsHandlerIds.forEach(handlerId => {
settings.disconnect(handlerId);
});
this.settingsHandlerIds = [];
super.destroy();
}
}
);
/**
*
* @param settingsObj
* @param actions
*/
function init(settingsObj, actions) {
settings = settingsObj;
ACTIONS = actions;
}
/**
*
*/
function uninit() {
settings = null;
ACTIONS = null;
}
/**
*
* @param prefix
*/
function _getSliderMinutes(prefix) {
let sliderValue = settings.get_double(`${prefix}-slider-value`) / 100.0;
const rampUp = settings.get_double(`nonlinear-${prefix}-slider-value`);
const ramp = x => Math.expm1(rampUp * x) / Math.expm1(rampUp);
return Math.floor(
(rampUp === 0 ? sliderValue : ramp(sliderValue)) *
settings.get_int(`${prefix}-max-timer-value`)
);
}
/**
*
* @param item
* @param connections
*/
function _connect(item, connections) {
const handlerIds = connections.map(([label, func]) =>
item.connect(label, func)
);
const destroyId = item.connect('destroy', () => {
handlerIds.concat(destroyId).forEach(handlerId => {
item.disconnect(handlerId);
});
});
}
/**
*
* @param settingsPrefix
*/
function _createSliderItem(settingsPrefix) {
const sliderValue =
settings.get_double(`${settingsPrefix}-slider-value`) / 100.0;
const item = new PopupMenu.PopupBaseMenuItem({ activate: false });
const sliderIcon = new St.Icon({
icon_name:
settingsPrefix === 'wake' ? 'alarm-symbolic' : 'system-shutdown-symbolic',
style_class: 'popup-menu-icon',
});
item.add(sliderIcon);
const slider = new Slider.Slider(sliderValue);
_connect(slider, [
[
'notify::value',
() => {
settings.set_double(
`${settingsPrefix}-slider-value`,
slider.value * 100
);
},
],
]);
item.add_child(slider);
return [item, slider];
}