61c5f259ec
We currently show the world clocks section unconditionally, even when the session mode disallows launching the Clocks app to configure the displayed clocks. This does not make sense, so hide the entire section when the session mode disallows settings. https://bugzilla.gnome.org/show_bug.cgi?id=745494
442 lines
16 KiB
JavaScript
442 lines
16 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const GnomeDesktop = imports.gi.GnomeDesktop;
|
|
const GObject = imports.gi.GObject;
|
|
const Gtk = imports.gi.Gtk;
|
|
const GWeather = imports.gi.GWeather;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Cairo = imports.cairo;
|
|
const Clutter = imports.gi.Clutter;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Atk = imports.gi.Atk;
|
|
|
|
const Params = imports.misc.params;
|
|
const Util = imports.misc.util;
|
|
const Main = imports.ui.main;
|
|
const PanelMenu = imports.ui.panelMenu;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
const Calendar = imports.ui.calendar;
|
|
|
|
function _isToday(date) {
|
|
let now = new Date();
|
|
return now.getYear() == date.getYear() &&
|
|
now.getMonth() == date.getMonth() &&
|
|
now.getDate() == date.getDate();
|
|
}
|
|
|
|
const TodayButton = new Lang.Class({
|
|
Name: 'TodayButton',
|
|
|
|
_init: function(calendar) {
|
|
// Having the ability to go to the current date if the user is already
|
|
// on the current date can be confusing. So don't make the button reactive
|
|
// until the selected date changes.
|
|
this.actor = new St.Button({ style_class: 'datemenu-today-button',
|
|
x_align: St.Align.START,
|
|
can_focus: true,
|
|
reactive: false
|
|
});
|
|
this.actor.connect('clicked', Lang.bind(this,
|
|
function() {
|
|
this._calendar.setDate(new Date(), false);
|
|
}));
|
|
|
|
let hbox = new St.BoxLayout({ vertical: true });
|
|
this.actor.add_actor(hbox);
|
|
|
|
this._dayLabel = new St.Label({ style_class: 'day-label',
|
|
x_align: Clutter.ActorAlign.START });
|
|
hbox.add_actor(this._dayLabel);
|
|
|
|
this._dateLabel = new St.Label({ style_class: 'date-label' });
|
|
hbox.add_actor(this._dateLabel);
|
|
|
|
this._calendar = calendar;
|
|
this._calendar.connect('selected-date-changed', Lang.bind(this,
|
|
function(calendar, date) {
|
|
// Make the button reactive only if the selected date is not the
|
|
// current date.
|
|
this.actor.reactive = !_isToday(date)
|
|
}));
|
|
},
|
|
|
|
setDate: function(date) {
|
|
this._dayLabel.set_text(date.toLocaleFormat('%A'));
|
|
|
|
/* Translators: This is the date format to use when the calendar popup is
|
|
* shown - it is shown just below the time in the shell (e.g. "Tue 9:29 AM").
|
|
*/
|
|
let dateFormat = Shell.util_translate_time_string (N_("%B %e %Y"));
|
|
this._dateLabel.set_text(date.toLocaleFormat(dateFormat));
|
|
|
|
/* Translators: This is the accessible name of the date button shown
|
|
* below the time in the shell; it should combine the weekday and the
|
|
* date, e.g. "Tuesday February 17 2015".
|
|
*/
|
|
let dateFormat = Shell.util_translate_time_string (N_("%A %B %e %Y"));
|
|
this.actor.accessible_name = date.toLocaleFormat(dateFormat);
|
|
}
|
|
});
|
|
|
|
const WorldClocksSection = new Lang.Class({
|
|
Name: 'WorldClocksSection',
|
|
|
|
_init: function() {
|
|
this._clock = new GnomeDesktop.WallClock();
|
|
this._settings = null;
|
|
this._clockNotifyId = 0;
|
|
this._changedId = 0;
|
|
|
|
this._locations = [];
|
|
|
|
this.actor = new St.Button({ style_class: 'world-clocks-button',
|
|
x_fill: true,
|
|
can_focus: true });
|
|
this.actor.connect('clicked', Lang.bind(this,
|
|
function() {
|
|
let app = this._getClockApp();
|
|
app.activate();
|
|
|
|
Main.overview.hide();
|
|
Main.panel.closeCalendar();
|
|
}));
|
|
|
|
let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
|
|
this._grid = new St.Widget({ style_class: 'world-clocks-grid',
|
|
layout_manager: layout });
|
|
layout.hookup_style(this._grid);
|
|
|
|
this.actor.child = this._grid;
|
|
|
|
Shell.AppSystem.get_default().connect('installed-changed',
|
|
Lang.bind(this, this._sync));
|
|
this._sync();
|
|
},
|
|
|
|
_getClockApp: function() {
|
|
return Shell.AppSystem.get_default().lookup_app('org.gnome.clocks.desktop');
|
|
},
|
|
|
|
_sync: function() {
|
|
this.actor.visible = (this._getClockApp() != null);
|
|
|
|
if (this.actor.visible) {
|
|
if (!this._settings) {
|
|
this._settings = new Gio.Settings({ schema_id: 'org.gnome.clocks' });
|
|
this._changedId =
|
|
this._settings.connect('changed::world-clocks',
|
|
Lang.bind(this, this._clocksChanged));
|
|
this._clocksChanged();
|
|
}
|
|
} else {
|
|
if (this._settings)
|
|
this._settings.disconnect(this._changedId);
|
|
this._settings = null;
|
|
this._changedId = 0;
|
|
}
|
|
},
|
|
|
|
_clocksChanged: function() {
|
|
this._grid.destroy_all_children();
|
|
this._locations = [];
|
|
|
|
let world = GWeather.Location.get_world();
|
|
let clocks = this._settings.get_value('world-clocks').deep_unpack();
|
|
for (let i = 0; i < clocks.length; i++) {
|
|
let l = world.deserialize(clocks[i].location);
|
|
this._locations.push({ location: l });
|
|
}
|
|
|
|
this._locations.sort(function(a, b) {
|
|
return a.location.get_timezone().get_offset() -
|
|
b.location.get_timezone().get_offset();
|
|
});
|
|
|
|
let layout = this._grid.layout_manager;
|
|
let title = (this._locations.length == 0) ? _("Add world clocks…")
|
|
: _("World Clocks");
|
|
let header = new St.Label({ style_class: 'world-clocks-header',
|
|
x_align: Clutter.ActorAlign.START,
|
|
text: title });
|
|
layout.attach(header, 0, 0, 2, 1);
|
|
|
|
for (let i = 0; i < this._locations.length; i++) {
|
|
let l = this._locations[i].location;
|
|
|
|
let label = new St.Label({ style_class: 'world-clocks-city',
|
|
text: l.get_city_name(),
|
|
x_align: Clutter.ActorAlign.START,
|
|
x_expand: true });
|
|
|
|
let time = new St.Label({ style_class: 'world-clocks-time',
|
|
x_align: Clutter.ActorAlign.END,
|
|
x_expand: true });
|
|
|
|
if (this._grid.text_direction == Clutter.TextDirection.RTL) {
|
|
layout.attach(time, 0, i + 1, 1, 1);
|
|
layout.attach(label, 1, i + 1, 1, 1);
|
|
} else {
|
|
layout.attach(label, 0, i + 1, 1, 1);
|
|
layout.attach(time, 1, i + 1, 1, 1);
|
|
}
|
|
|
|
this._locations[i].actor = time;
|
|
}
|
|
|
|
if (this._grid.get_n_children() > 1) {
|
|
if (!this._clockNotifyId)
|
|
this._clockNotifyId =
|
|
this._clock.connect('notify::clock', Lang.bind(this, this._updateLabels));
|
|
this._updateLabels();
|
|
} else {
|
|
if (this._clockNotifyId)
|
|
this._clock.disconnect(this._clockNotifyId);
|
|
this._clockNotifyId = 0;
|
|
}
|
|
},
|
|
|
|
_updateLabels: function() {
|
|
let desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
|
|
let clockFormat = desktopSettings.get_string('clock-format');
|
|
let hasAmPm = new Date().toLocaleFormat('%p') != '';
|
|
|
|
let format;
|
|
if (clockFormat == '24h' || !hasAmPm)
|
|
/* Translators: Time in 24h format */
|
|
format = N_("%H\u2236%M");
|
|
else
|
|
/* Translators: Time in 12h format */
|
|
format = N_("%l\u2236%M %p");
|
|
|
|
for (let i = 0; i < this._locations.length; i++) {
|
|
let l = this._locations[i];
|
|
let tz = GLib.TimeZone.new(l.location.get_timezone().get_tzid());
|
|
let now = GLib.DateTime.new_now(tz);
|
|
l.actor.text = now.format(format);
|
|
}
|
|
}
|
|
});
|
|
|
|
const MessagesIndicator = new Lang.Class({
|
|
Name: 'MessagesIndicator',
|
|
|
|
_init: function() {
|
|
this.actor = new St.Label({ text: '⚫', visible: false, y_expand: true,
|
|
y_align: Clutter.ActorAlign.CENTER });
|
|
|
|
this._sources = [];
|
|
|
|
Main.messageTray.connect('source-added', Lang.bind(this, this._onSourceAdded));
|
|
Main.messageTray.connect('source-removed', Lang.bind(this, this._onSourceRemoved));
|
|
Main.messageTray.connect('queue-changed', Lang.bind(this, this._updateCount));
|
|
|
|
let sources = Main.messageTray.getSources();
|
|
sources.forEach(Lang.bind(this, function(source) { this._onSourceAdded(null, source); }));
|
|
},
|
|
|
|
_onSourceAdded: function(tray, source) {
|
|
source.connect('count-updated', Lang.bind(this, this._updateCount));
|
|
this._sources.push(source);
|
|
this._updateCount();
|
|
},
|
|
|
|
_onSourceRemoved: function(tray, source) {
|
|
this._sources.splice(this._sources.indexOf(source), 1);
|
|
this._updateCount();
|
|
},
|
|
|
|
_updateCount: function() {
|
|
let count = 0;
|
|
this._sources.forEach(Lang.bind(this,
|
|
function(source) {
|
|
count += source.unseenCount;
|
|
}));
|
|
count -= Main.messageTray.queueCount;
|
|
|
|
this.actor.visible = (count > 0);
|
|
}
|
|
});
|
|
|
|
const IndicatorPad = new Lang.Class({
|
|
Name: 'IndicatorPad',
|
|
Extends: St.Widget,
|
|
|
|
_init: function(actor) {
|
|
this._source = actor;
|
|
this._source.connect('notify::visible',
|
|
Lang.bind(this, this.queue_relayout));
|
|
this.parent();
|
|
},
|
|
|
|
vfunc_get_preferred_width: function(container, forHeight) {
|
|
if (this._source.visible)
|
|
return this._source.get_preferred_width(forHeight);
|
|
return [0, 0];
|
|
},
|
|
|
|
vfunc_get_preferred_height: function(container, forWidth) {
|
|
if (this._source.visible)
|
|
return this._source.get_preferred_height(forWidth);
|
|
return [0, 0];
|
|
}
|
|
});
|
|
|
|
const FreezableBinLayout = new Lang.Class({
|
|
Name: 'FreezableBinLayout',
|
|
Extends: Clutter.BinLayout,
|
|
|
|
_init: function() {
|
|
this.parent();
|
|
|
|
this._frozen = false;
|
|
this._savedWidth = [NaN, NaN];
|
|
this._savedHeight = [NaN, NaN];
|
|
},
|
|
|
|
set frozen(v) {
|
|
if (this._frozen == v)
|
|
return;
|
|
|
|
this._frozen = v;
|
|
if (!this._frozen)
|
|
this.layout_changed();
|
|
},
|
|
|
|
vfunc_get_preferred_width: function(container, forHeight) {
|
|
if (!this._frozen || this._savedWidth.some(isNaN))
|
|
this._savedWidth = this.parent(container, forHeight);
|
|
return this._savedWidth;
|
|
},
|
|
|
|
vfunc_get_preferred_height: function(container, forWidth) {
|
|
if (!this._frozen || this._savedHeight.some(isNaN))
|
|
this._savedHeight = this.parent(container, forWidth);
|
|
return this._savedHeight;
|
|
}
|
|
});
|
|
|
|
const DateMenuButton = new Lang.Class({
|
|
Name: 'DateMenuButton',
|
|
Extends: PanelMenu.Button,
|
|
|
|
_init: function() {
|
|
let item;
|
|
let hbox;
|
|
let vbox;
|
|
|
|
let menuAlignment = 0.5;
|
|
if (Clutter.get_default_text_direction() == Clutter.TextDirection.RTL)
|
|
menuAlignment = 1.0 - menuAlignment;
|
|
this.parent(menuAlignment);
|
|
|
|
this._clockDisplay = new St.Label({ y_align: Clutter.ActorAlign.CENTER });
|
|
this._indicator = new MessagesIndicator();
|
|
|
|
let box = new St.BoxLayout();
|
|
box.add_actor(new IndicatorPad(this._indicator.actor));
|
|
box.add_actor(this._clockDisplay);
|
|
box.add_actor(this._indicator.actor);
|
|
|
|
this.actor.label_actor = this._clockDisplay;
|
|
this.actor.add_actor(box);
|
|
this.actor.add_style_class_name ('clock-display');
|
|
|
|
|
|
let layout = new FreezableBinLayout();
|
|
let bin = new St.Widget({ layout_manager: layout });
|
|
this.menu.box.add_child(bin);
|
|
|
|
hbox = new St.BoxLayout({ name: 'calendarArea' });
|
|
bin.add_actor(hbox);
|
|
|
|
this._calendar = new Calendar.Calendar();
|
|
this._calendar.connect('selected-date-changed',
|
|
Lang.bind(this, function(calendar, date) {
|
|
layout.frozen = !_isToday(date);
|
|
this._messageList.setDate(date);
|
|
}));
|
|
|
|
this.menu.connect('open-state-changed', Lang.bind(this, function(menu, isOpen) {
|
|
// Whenever the menu is opened, select today
|
|
if (isOpen) {
|
|
let now = new Date();
|
|
this._calendar.setDate(now);
|
|
this._date.setDate(now);
|
|
this._messageList.setDate(now);
|
|
}
|
|
// Block notification banners while the menu is open
|
|
Main.messageTray.bannerBlocked = isOpen;
|
|
}));
|
|
|
|
// Fill up the first column
|
|
this._messageList = new Calendar.MessageList();
|
|
hbox.add(this._messageList.actor, { expand: true, y_fill: false, y_align: St.Align.START });
|
|
|
|
// Fill up the second column
|
|
vbox = new St.BoxLayout({ style_class: 'datemenu-calendar-column',
|
|
vertical: true });
|
|
hbox.add(vbox);
|
|
|
|
this._date = new TodayButton(this._calendar);
|
|
vbox.add_actor(this._date.actor);
|
|
|
|
vbox.add(this._calendar.actor);
|
|
|
|
this._displaysSection = new St.ScrollView({ style_class: 'datemenu-displays-section vfade',
|
|
x_expand: true, x_fill: true,
|
|
overlay_scrollbars: true });
|
|
this._displaysSection.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
|
|
vbox.add_actor(this._displaysSection);
|
|
|
|
let displaysBox = new St.BoxLayout({ vertical: true,
|
|
style_class: 'datemenu-displays-box' });
|
|
this._displaysSection.add_actor(displaysBox);
|
|
|
|
this._clocksItem = new WorldClocksSection();
|
|
displaysBox.add(this._clocksItem.actor, { x_fill: true });
|
|
|
|
|
|
// Done with hbox for calendar and event list
|
|
|
|
this._clock = new GnomeDesktop.WallClock();
|
|
this._clock.bind_property('clock', this._clockDisplay, 'text', GObject.BindingFlags.SYNC_CREATE);
|
|
|
|
Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
|
|
this._sessionUpdated();
|
|
},
|
|
|
|
_getEventSource: function() {
|
|
return new Calendar.DBusEventSource();
|
|
},
|
|
|
|
_setEventSource: function(eventSource) {
|
|
if (this._eventSource)
|
|
this._eventSource.destroy();
|
|
|
|
this._calendar.setEventSource(eventSource);
|
|
this._messageList.setEventSource(eventSource);
|
|
|
|
this._eventSource = eventSource;
|
|
},
|
|
|
|
_sessionUpdated: function() {
|
|
let eventSource;
|
|
let showEvents = Main.sessionMode.showCalendarEvents;
|
|
if (showEvents) {
|
|
eventSource = this._getEventSource();
|
|
} else {
|
|
eventSource = new Calendar.EmptyEventSource();
|
|
}
|
|
this._setEventSource(eventSource);
|
|
|
|
// Displays are not actually expected to launch Settings when activated
|
|
// but the corresponding app (clocks, weather); however we can consider
|
|
// that display-specific settings, so re-use "allowSettings" here ...
|
|
this._displaysSection.visible = Main.sessionMode.allowSettings;
|
|
}
|
|
});
|