summaryrefslogtreecommitdiffstats
path: root/js/ui/dateMenu.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/dateMenu.js')
-rw-r--r--js/ui/dateMenu.js980
1 files changed, 980 insertions, 0 deletions
diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js
new file mode 100644
index 0000000..2c44f0c
--- /dev/null
+++ b/js/ui/dateMenu.js
@@ -0,0 +1,980 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported DateMenuButton */
+
+const {
+ Clutter, Gio, GLib, GnomeDesktop,
+ GObject, GWeather, Pango, Shell, St,
+} = imports.gi;
+
+const Util = imports.misc.util;
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const Calendar = imports.ui.calendar;
+const Weather = imports.misc.weather;
+const System = imports.system;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const NC_ = (context, str) => `${context}\u0004${str}`;
+const T_ = Shell.util_translate_time_string;
+
+const MAX_FORECASTS = 5;
+const EN_CHAR = '\u2013';
+
+const ClocksIntegrationIface = loadInterfaceXML('org.gnome.Shell.ClocksIntegration');
+const ClocksProxy = Gio.DBusProxy.makeProxyWrapper(ClocksIntegrationIface);
+
+function _isToday(date) {
+ let now = new Date();
+ return now.getYear() == date.getYear() &&
+ now.getMonth() == date.getMonth() &&
+ now.getDate() == date.getDate();
+}
+
+function _gDateTimeToDate(datetime) {
+ return new Date(datetime.to_unix() * 1000 + datetime.get_microsecond() / 1000);
+}
+
+var TodayButton = GObject.registerClass(
+class TodayButton extends St.Button {
+ _init(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.
+ super._init({
+ style_class: 'datemenu-today-button',
+ x_expand: true,
+ can_focus: true,
+ reactive: false,
+ });
+
+ let hbox = new St.BoxLayout({ vertical: true });
+ this.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', (_calendar, datetime) => {
+ // Make the button reactive only if the selected date is not the
+ // current date.
+ this.reactive = !_isToday(_gDateTimeToDate(datetime));
+ });
+ }
+
+ vfunc_clicked() {
+ this._calendar.setDate(new Date(), false);
+ }
+
+ setDate(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 top bar (e.g.,
+ * "Tue 9:29 AM"). The string itself should become a full date, e.g.,
+ * "February 17 2015".
+ */
+ let dateFormat = Shell.util_translate_time_string(N_("%B %-d %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".
+ */
+ dateFormat = Shell.util_translate_time_string(N_("%A %B %e %Y"));
+ this.accessible_name = date.toLocaleFormat(dateFormat);
+ }
+});
+
+var EventsSection = GObject.registerClass(
+class EventsSection extends St.Button {
+ _init() {
+ super._init({
+ style_class: 'events-button',
+ can_focus: true,
+ x_expand: true,
+ child: new St.BoxLayout({
+ style_class: 'events-box',
+ vertical: true,
+ x_expand: true,
+ }),
+ });
+
+ this._startDate = null;
+ this._endDate = null;
+
+ this._eventSource = null;
+ this._calendarApp = null;
+
+ this._title = new St.Label({
+ style_class: 'events-title',
+ });
+ this.child.add_child(this._title);
+
+ this._eventsList = new St.BoxLayout({
+ style_class: 'events-list',
+ vertical: true,
+ x_expand: true,
+ });
+ this.child.add_child(this._eventsList);
+
+ this._appSys = Shell.AppSystem.get_default();
+ this._appSys.connect('installed-changed',
+ this._appInstalledChanged.bind(this));
+ this._appInstalledChanged();
+ }
+
+ setDate(date) {
+ this._startDate =
+ new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ this._endDate =
+ new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
+
+ this._updateTitle();
+ this._reloadEvents();
+ }
+
+ setEventSource(eventSource) {
+ if (!(eventSource instanceof Calendar.EventSourceBase))
+ throw new Error('Event source is not valid type');
+
+ this._eventSource = eventSource;
+ this._eventSource.connect('changed', this._reloadEvents.bind(this));
+ this._eventSource.connect('notify::has-calendars',
+ this._sync.bind(this));
+ this._sync();
+ }
+
+ _updateTitle() {
+ /* Translators: Shown on calendar heading when selected day occurs on current year */
+ const sameYearFormat = T_(NC_('calendar heading', '%B %-d'));
+
+ /* Translators: Shown on calendar heading when selected day occurs on different year */
+ const otherYearFormat = T_(NC_('calendar heading', '%B %-d %Y'));
+
+ const timeSpanDay = GLib.TIME_SPAN_DAY / 1000;
+ const now = new Date();
+
+ if (this._startDate <= now && now < this._endDate)
+ this._title.text = _('Today');
+ else if (this._endDate <= now && now - this._endDate < timeSpanDay)
+ this._title.text = _('Yesterday');
+ else if (this._startDate > now && this._startDate - now <= timeSpanDay)
+ this._title.text = _('Tomorrow');
+ else if (this._startDate.getFullYear() === now.getFullYear())
+ this._title.text = this._startDate.toLocaleFormat(sameYearFormat);
+ else
+ this._title.text = this._startDate.toLocaleFormat(otherYearFormat);
+ }
+
+ _isAtMidnight(eventTime) {
+ return eventTime.getHours() === 0 && eventTime.getMinutes() === 0 && eventTime.getSeconds() === 0;
+ }
+
+ _formatEventTime(event) {
+ const eventStart = event.date;
+ let eventEnd = event.end;
+
+ const allDay =
+ eventStart.getTime() === this._startDate.getTime() && eventEnd.getTime() === this._endDate.getTime();
+
+ const startsBeforeToday = eventStart < this._startDate;
+ const endsAfterToday = eventEnd > this._endDate;
+
+ const startTimeOnly = Util.formatTime(eventStart, { timeOnly: true });
+ const endTimeOnly = Util.formatTime(eventEnd, { timeOnly: true });
+
+ const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+
+ let title;
+ if (allDay) {
+ /* Translators: Shown in calendar event list for all day events
+ * Keep it short, best if you can use less then 10 characters
+ */
+ title = C_('event list time', 'All Day');
+ } else if (startsBeforeToday || endsAfterToday) {
+ const now = new Date();
+ const thisYear = now.getFullYear();
+
+ const startsAtMidnight = this._isAtMidnight(eventStart);
+ const endsAtMidnight = this._isAtMidnight(eventEnd);
+
+ const startYear = eventStart.getFullYear();
+
+ if (endsAtMidnight) {
+ eventEnd = new Date(eventEnd);
+ eventEnd.setDate(eventEnd.getDate() - 1);
+ }
+
+ const endYear = eventEnd.getFullYear();
+
+ let format;
+ if (startYear === thisYear && thisYear === endYear)
+ /* Translators: Shown in calendar event list as the start/end of events
+ * that only show day and month
+ */
+ format = T_(N_('%m/%d'));
+ else
+ format = '%x';
+
+ const startDateOnly = eventStart.toLocaleFormat(format);
+ const endDateOnly = eventEnd.toLocaleFormat(format);
+
+ if (startsAtMidnight && endsAtMidnight)
+ title = `${rtl ? endDateOnly : startDateOnly} ${EN_CHAR} ${rtl ? startDateOnly : endDateOnly}`;
+ else if (rtl)
+ title = `${endTimeOnly} ${endDateOnly} ${EN_CHAR} ${startTimeOnly} ${startDateOnly}`;
+ else
+ title = `${startDateOnly} ${startTimeOnly} ${EN_CHAR} ${endDateOnly} ${endTimeOnly}`;
+ } else if (eventStart === eventEnd) {
+ title = startTimeOnly;
+ } else {
+ title = `${rtl ? endTimeOnly : startTimeOnly} ${EN_CHAR} ${rtl ? startTimeOnly : endTimeOnly}`;
+ }
+
+ return title;
+ }
+
+ _reloadEvents() {
+ if (this._eventSource.isLoading || this._reloading)
+ return;
+
+ this._reloading = true;
+
+ [...this._eventsList].forEach(c => c.destroy());
+
+ const events =
+ this._eventSource.getEvents(this._startDate, this._endDate);
+
+ for (let event of events) {
+ const box = new St.BoxLayout({
+ style_class: 'event-box',
+ vertical: true,
+ });
+ box.add(new St.Label({
+ text: event.summary,
+ style_class: 'event-summary',
+ }));
+ box.add(new St.Label({
+ text: this._formatEventTime(event),
+ style_class: 'event-time',
+ }));
+ this._eventsList.add_child(box);
+ }
+
+ if (this._eventsList.get_n_children() === 0) {
+ const placeholder = new St.Label({
+ text: _('No Events'),
+ style_class: 'event-placeholder',
+ });
+ this._eventsList.add_child(placeholder);
+ }
+
+ this._reloading = false;
+ this._sync();
+ }
+
+ vfunc_clicked() {
+ Main.overview.hide();
+ Main.panel.closeCalendar();
+
+ let appInfo = this._calendarApp;
+ if (appInfo.get_id() === 'org.gnome.Evolution.desktop') {
+ const app = this._appSys.lookup_app('evolution-calendar.desktop');
+ if (app)
+ appInfo = app.app_info;
+ }
+ appInfo.launch([], global.create_app_launch_context(0, -1));
+ }
+
+ _appInstalledChanged() {
+ const apps = Gio.AppInfo.get_recommended_for_type('text/calendar');
+ if (apps && (apps.length > 0)) {
+ const app = Gio.AppInfo.get_default_for_type('text/calendar', false);
+ const defaultInRecommended = apps.some(a => a.equal(app));
+ this._calendarApp = defaultInRecommended ? app : apps[0];
+ } else {
+ this._calendarApp = null;
+ }
+
+ return this._sync();
+ }
+
+ _sync() {
+ this.visible = this._eventSource && this._eventSource.hasCalendars;
+ this.reactive = this._calendarApp !== null;
+ }
+});
+
+var WorldClocksSection = GObject.registerClass(
+class WorldClocksSection extends St.Button {
+ _init() {
+ super._init({
+ style_class: 'world-clocks-button',
+ can_focus: true,
+ x_expand: true,
+ });
+ this._clock = new GnomeDesktop.WallClock();
+ this._clockNotifyId = 0;
+ this._tzNotifyId = 0;
+
+ this._locations = [];
+
+ let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
+ this._grid = new St.Widget({
+ style_class: 'world-clocks-grid',
+ x_expand: true,
+ layout_manager: layout,
+ });
+ layout.hookup_style(this._grid);
+
+ this.child = this._grid;
+
+ this._clocksApp = null;
+ this._clocksProxy = new ClocksProxy(
+ Gio.DBus.session,
+ 'org.gnome.clocks',
+ '/org/gnome/clocks',
+ this._onProxyReady.bind(this),
+ null /* cancellable */,
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES);
+
+ this._settings = new Gio.Settings({
+ schema_id: 'org.gnome.shell.world-clocks',
+ });
+ this._settings.connect('changed', this._clocksChanged.bind(this));
+ this._clocksChanged();
+
+ this._appSystem = Shell.AppSystem.get_default();
+ this._appSystem.connect('installed-changed',
+ this._sync.bind(this));
+ this._sync();
+ }
+
+ vfunc_clicked() {
+ if (this._clocksApp)
+ this._clocksApp.activate();
+
+ Main.overview.hide();
+ Main.panel.closeCalendar();
+ }
+
+ _sync() {
+ this._clocksApp = this._appSystem.lookup_app('org.gnome.clocks.desktop');
+ this.visible = this._clocksApp != null;
+ }
+
+ _clocksChanged() {
+ this._grid.destroy_all_children();
+ this._locations = [];
+
+ let world = GWeather.Location.get_world();
+ let clocks = this._settings.get_value('locations').deepUnpack();
+ for (let i = 0; i < clocks.length; i++) {
+ let l = world.deserialize(clocks[i]);
+ if (l && l.get_timezone() != null)
+ this._locations.push({ location: l });
+ }
+
+ const unixtime = GLib.DateTime.new_now_local().to_unix();
+ this._locations.sort((a, b) => {
+ const tzA = a.location.get_timezone();
+ const tzB = b.location.get_timezone();
+ const intA = tzA.find_interval(GLib.TimeType.STANDARD, unixtime);
+ const intB = tzB.find_interval(GLib.TimeType.STANDARD, unixtime);
+ return tzA.get_offset(intA) - tzB.get_offset(intB);
+ });
+
+ let layout = this._grid.layout_manager;
+ let title = this._locations.length == 0
+ ? _("Add world clocks…")
+ : _("World Clocks");
+ const header = new St.Label({
+ style_class: 'world-clocks-header',
+ x_align: Clutter.ActorAlign.START,
+ text: title,
+ });
+ if (this._grid.text_direction === Clutter.TextDirection.RTL)
+ layout.attach(header, 2, 0, 1, 1);
+ else
+ layout.attach(header, 0, 0, 2, 1);
+ this.label_actor = header;
+
+ for (let i = 0; i < this._locations.length; i++) {
+ let l = this._locations[i].location;
+
+ let name = l.get_city_name() || l.get_name();
+ const label = new St.Label({
+ style_class: 'world-clocks-city',
+ text: name,
+ x_align: Clutter.ActorAlign.START,
+ y_align: Clutter.ActorAlign.CENTER,
+ x_expand: true,
+ });
+
+ let time = new St.Label({ style_class: 'world-clocks-time' });
+
+ const tz = new St.Label({
+ style_class: 'world-clocks-timezone',
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ time.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ tz.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+
+ if (this._grid.text_direction == Clutter.TextDirection.RTL) {
+ layout.attach(tz, 0, i + 1, 1, 1);
+ layout.attach(time, 1, i + 1, 1, 1);
+ layout.attach(label, 2, i + 1, 1, 1);
+ } else {
+ layout.attach(label, 0, i + 1, 1, 1);
+ layout.attach(time, 1, i + 1, 1, 1);
+ layout.attach(tz, 2, i + 1, 1, 1);
+ }
+
+ this._locations[i].timeLabel = time;
+ this._locations[i].tzLabel = tz;
+ }
+
+ if (this._grid.get_n_children() > 1) {
+ if (!this._clockNotifyId) {
+ this._clockNotifyId =
+ this._clock.connect('notify::clock', this._updateTimeLabels.bind(this));
+ }
+ if (!this._tzNotifyId) {
+ this._tzNotifyId =
+ this._clock.connect('notify::timezone', this._updateTimezoneLabels.bind(this));
+ }
+ this._updateTimeLabels();
+ this._updateTimezoneLabels();
+ } else {
+ if (this._clockNotifyId)
+ this._clock.disconnect(this._clockNotifyId);
+ this._clockNotifyId = 0;
+
+ if (this._tzNotifyId)
+ this._clock.disconnect(this._tzNotifyId);
+ this._tzNotifyId = 0;
+ }
+ }
+
+ _getTimezoneOffsetAtLocation(location) {
+ const tz = location.get_timezone();
+ const localOffset = GLib.DateTime.new_now_local().get_utc_offset();
+ const utcOffset = GLib.DateTime.new_now(tz).get_utc_offset();
+ const offsetCurrentTz = utcOffset - localOffset;
+ const offsetHours =
+ Math.floor(Math.abs(offsetCurrentTz) / GLib.TIME_SPAN_HOUR);
+ const offsetMinutes =
+ (Math.abs(offsetCurrentTz) % GLib.TIME_SPAN_HOUR) /
+ GLib.TIME_SPAN_MINUTE;
+
+ const prefix = offsetCurrentTz >= 0 ? '+' : '-';
+ const text = offsetMinutes === 0
+ ? `${prefix}${offsetHours}`
+ : `${prefix}${offsetHours}\u2236${offsetMinutes}`;
+ return text;
+ }
+
+ _updateTimeLabels() {
+ for (let i = 0; i < this._locations.length; i++) {
+ let l = this._locations[i];
+ const now = GLib.DateTime.new_now(l.location.get_timezone());
+ l.timeLabel.text = Util.formatTime(now, { timeOnly: true });
+ }
+ }
+
+ _updateTimezoneLabels() {
+ for (let i = 0; i < this._locations.length; i++) {
+ let l = this._locations[i];
+ l.tzLabel.text = this._getTimezoneOffsetAtLocation(l.location);
+ }
+ }
+
+ _onProxyReady(proxy, error) {
+ if (error) {
+ log(`Failed to create GNOME Clocks proxy: ${error}`);
+ return;
+ }
+
+ this._clocksProxy.connect('g-properties-changed',
+ this._onClocksPropertiesChanged.bind(this));
+ this._onClocksPropertiesChanged();
+ }
+
+ _onClocksPropertiesChanged() {
+ if (this._clocksProxy.g_name_owner == null)
+ return;
+
+ this._settings.set_value('locations',
+ new GLib.Variant('av', this._clocksProxy.Locations));
+ }
+});
+
+var WeatherSection = GObject.registerClass(
+class WeatherSection extends St.Button {
+ _init() {
+ super._init({
+ style_class: 'weather-button',
+ can_focus: true,
+ x_expand: true,
+ });
+
+ this._weatherClient = new Weather.WeatherClient();
+
+ let box = new St.BoxLayout({
+ style_class: 'weather-box',
+ vertical: true,
+ x_expand: true,
+ });
+
+ this.child = box;
+
+ let titleBox = new St.BoxLayout({ style_class: 'weather-header-box' });
+ this._titleLabel = new St.Label({
+ style_class: 'weather-header',
+ x_align: Clutter.ActorAlign.START,
+ x_expand: true,
+ y_align: Clutter.ActorAlign.END,
+ });
+ titleBox.add_child(this._titleLabel);
+ box.add_child(titleBox);
+
+ this._titleLocation = new St.Label({
+ style_class: 'weather-header location',
+ x_align: Clutter.ActorAlign.END,
+ y_align: Clutter.ActorAlign.END,
+ });
+ titleBox.add_child(this._titleLocation);
+
+ let layout = new Clutter.GridLayout({ orientation: Clutter.Orientation.VERTICAL });
+ this._forecastGrid = new St.Widget({
+ style_class: 'weather-grid',
+ layout_manager: layout,
+ });
+ layout.hookup_style(this._forecastGrid);
+ box.add_child(this._forecastGrid);
+
+ this._weatherClient.connect('changed', this._sync.bind(this));
+ this._sync();
+ }
+
+ vfunc_map() {
+ this._weatherClient.update();
+ super.vfunc_map();
+ }
+
+ vfunc_clicked() {
+ this._weatherClient.activateApp();
+
+ Main.overview.hide();
+ Main.panel.closeCalendar();
+ }
+
+ _getInfos() {
+ let forecasts = this._weatherClient.info.get_forecast_list();
+
+ let now = GLib.DateTime.new_now_local();
+ let current = GLib.DateTime.new_from_unix_local(0);
+ let infos = [];
+ for (let i = 0; i < forecasts.length; i++) {
+ const [valid, timestamp] = forecasts[i].get_value_update();
+ if (!valid || timestamp === 0)
+ continue; // 0 means 'never updated'
+
+ const datetime = GLib.DateTime.new_from_unix_local(timestamp);
+ if (now.difference(datetime) > 0)
+ continue; // Ignore earlier forecasts
+
+ if (datetime.difference(current) < GLib.TIME_SPAN_HOUR)
+ continue; // Enforce a minimum interval of 1h
+
+ if (infos.push(forecasts[i]) == MAX_FORECASTS)
+ break; // Use a maximum of five forecasts
+
+ current = datetime;
+ }
+ return infos;
+ }
+
+ _addForecasts() {
+ let layout = this._forecastGrid.layout_manager;
+
+ let infos = this._getInfos();
+ if (this._forecastGrid.text_direction == Clutter.TextDirection.RTL)
+ infos.reverse();
+
+ let col = 0;
+ infos.forEach(fc => {
+ const [valid_, timestamp] = fc.get_value_update();
+ let timeStr = Util.formatTime(new Date(timestamp * 1000), {
+ timeOnly: true,
+ ampm: false,
+ });
+ const [, tempValue] = fc.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
+ const tempPrefix = Math.round(tempValue) >= 0 ? ' ' : '';
+
+ let time = new St.Label({
+ style_class: 'weather-forecast-time',
+ text: timeStr,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ let icon = new St.Icon({
+ style_class: 'weather-forecast-icon',
+ icon_name: fc.get_symbolic_icon_name(),
+ x_align: Clutter.ActorAlign.CENTER,
+ x_expand: true,
+ });
+ let temp = new St.Label({
+ style_class: 'weather-forecast-temp',
+ text: `${tempPrefix}${Math.round(tempValue)}°`,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+
+ temp.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+ time.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+
+ layout.attach(time, col, 0, 1, 1);
+ layout.attach(icon, col, 1, 1, 1);
+ layout.attach(temp, col, 2, 1, 1);
+ col++;
+ });
+ }
+
+ _setStatusLabel(text) {
+ let layout = this._forecastGrid.layout_manager;
+ let label = new St.Label({ text });
+ layout.attach(label, 0, 0, 1, 1);
+ }
+
+ _findBestLocationName(loc) {
+ const locName = loc.get_name();
+
+ if (loc.get_level() === GWeather.LocationLevel.CITY ||
+ !loc.has_coords())
+ return locName;
+
+ const world = GWeather.Location.get_world();
+ const city = world.find_nearest_city(...loc.get_coords());
+ const cityName = city.get_name();
+
+ return locName.includes(cityName) ? cityName : locName;
+ }
+
+ _updateForecasts() {
+ this._forecastGrid.destroy_all_children();
+
+ if (!this._weatherClient.hasLocation)
+ return;
+
+ const { info } = this._weatherClient;
+ this._titleLocation.text = this._findBestLocationName(info.location);
+
+ if (this._weatherClient.loading) {
+ this._setStatusLabel(_("Loading…"));
+ return;
+ }
+
+ if (info.is_valid()) {
+ this._addForecasts();
+ return;
+ }
+
+ if (info.network_error())
+ this._setStatusLabel(_("Go online for weather information"));
+ else
+ this._setStatusLabel(_("Weather information is currently unavailable"));
+ }
+
+ _sync() {
+ this.visible = this._weatherClient.available;
+
+ if (!this.visible)
+ return;
+
+ if (this._weatherClient.hasLocation)
+ this._titleLabel.text = _('Weather');
+ else
+ this._titleLabel.text = _('Select weather location…');
+
+ this._forecastGrid.visible = this._weatherClient.hasLocation;
+ this._titleLocation.visible = this._weatherClient.hasLocation;
+
+ this._updateForecasts();
+ }
+});
+
+var MessagesIndicator = GObject.registerClass(
+class MessagesIndicator extends St.Icon {
+ _init() {
+ super._init({
+ icon_size: 16,
+ visible: false,
+ y_expand: true,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+
+ this._sources = [];
+ this._count = 0;
+
+ this._settings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.notifications',
+ });
+ this._settings.connect('changed::show-banners', this._sync.bind(this));
+
+ Main.messageTray.connect('source-added', this._onSourceAdded.bind(this));
+ Main.messageTray.connect('source-removed', this._onSourceRemoved.bind(this));
+ Main.messageTray.connect('queue-changed', this._updateCount.bind(this));
+
+ let sources = Main.messageTray.getSources();
+ sources.forEach(source => this._onSourceAdded(null, source));
+
+ this._sync();
+
+ this.connect('destroy', () => {
+ this._settings.run_dispose();
+ this._settings = null;
+ });
+ }
+
+ _onSourceAdded(tray, source) {
+ source.connect('notify::count', this._updateCount.bind(this));
+ this._sources.push(source);
+ this._updateCount();
+ }
+
+ _onSourceRemoved(tray, source) {
+ this._sources.splice(this._sources.indexOf(source), 1);
+ this._updateCount();
+ }
+
+ _updateCount() {
+ let count = 0;
+ this._sources.forEach(source => (count += source.unseenCount));
+ this._count = count - Main.messageTray.queueCount;
+
+ this._sync();
+ }
+
+ _sync() {
+ let doNotDisturb = !this._settings.get_boolean('show-banners');
+ this.icon_name = doNotDisturb
+ ? 'notifications-disabled-symbolic'
+ : 'message-indicator-symbolic';
+ this.visible = doNotDisturb || this._count > 0;
+ }
+});
+
+var FreezableBinLayout = GObject.registerClass(
+class FreezableBinLayout extends Clutter.BinLayout {
+ _init() {
+ super._init();
+
+ 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(container, forHeight) {
+ if (!this._frozen || this._savedWidth.some(isNaN))
+ return super.vfunc_get_preferred_width(container, forHeight);
+ return this._savedWidth;
+ }
+
+ vfunc_get_preferred_height(container, forWidth) {
+ if (!this._frozen || this._savedHeight.some(isNaN))
+ return super.vfunc_get_preferred_height(container, forWidth);
+ return this._savedHeight;
+ }
+
+ vfunc_allocate(container, allocation) {
+ super.vfunc_allocate(container, allocation);
+
+ let [width, height] = allocation.get_size();
+ this._savedWidth = [width, width];
+ this._savedHeight = [height, height];
+ }
+});
+
+var CalendarColumnLayout = GObject.registerClass(
+class CalendarColumnLayout extends Clutter.BoxLayout {
+ _init(actors) {
+ super._init({ orientation: Clutter.Orientation.VERTICAL });
+ this._colActors = actors;
+ }
+
+ vfunc_get_preferred_width(container, forHeight) {
+ const actors =
+ this._colActors.filter(a => a.get_parent() === container);
+ if (actors.length === 0)
+ return super.vfunc_get_preferred_width(container, forHeight);
+ return actors.reduce(([minAcc, natAcc], child) => {
+ const [min, nat] = child.get_preferred_width(forHeight);
+ return [Math.max(minAcc, min), Math.max(natAcc, nat)];
+ }, [0, 0]);
+ }
+});
+
+var DateMenuButton = GObject.registerClass(
+class DateMenuButton extends PanelMenu.Button {
+ _init() {
+ let hbox;
+
+ super._init(0.5);
+
+ this._clockDisplay = new St.Label({ style_class: 'clock' });
+ this._clockDisplay.clutter_text.y_align = Clutter.ActorAlign.CENTER;
+ this._clockDisplay.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+
+ this._indicator = new MessagesIndicator();
+
+ const indicatorPad = new St.Widget();
+ this._indicator.bind_property('visible',
+ indicatorPad, 'visible',
+ GObject.BindingFlags.SYNC_CREATE);
+ indicatorPad.add_constraint(new Clutter.BindConstraint({
+ source: this._indicator,
+ coordinate: Clutter.BindCoordinate.SIZE,
+ }));
+
+ let box = new St.BoxLayout({ style_class: 'clock-display-box' });
+ box.add_actor(indicatorPad);
+ box.add_actor(this._clockDisplay);
+ box.add_actor(this._indicator);
+
+ this.label_actor = this._clockDisplay;
+ this.add_actor(box);
+ this.add_style_class_name('clock-display');
+
+ let layout = new FreezableBinLayout();
+ let bin = new St.Widget({ layout_manager: layout });
+ // For some minimal compatibility with PopupMenuItem
+ bin._delegate = this;
+ 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', (_calendar, datetime) => {
+ let date = _gDateTimeToDate(datetime);
+ layout.frozen = !_isToday(date);
+ this._eventsItem.setDate(date);
+ });
+ this._date = new TodayButton(this._calendar);
+
+ this.menu.connect('open-state-changed', (menu, isOpen) => {
+ // Whenever the menu is opened, select today
+ if (isOpen) {
+ let now = new Date();
+ this._calendar.setDate(now);
+ this._date.setDate(now);
+ this._eventsItem.setDate(now);
+ }
+ });
+
+ // Fill up the first column
+ this._messageList = new Calendar.CalendarMessageList();
+ hbox.add_child(this._messageList);
+
+ // Fill up the second column
+ const boxLayout = new CalendarColumnLayout([this._calendar, this._date]);
+ const vbox = new St.Widget({
+ style_class: 'datemenu-calendar-column',
+ layout_manager: boxLayout,
+ });
+ boxLayout.hookup_style(vbox);
+ hbox.add(vbox);
+
+ vbox.add_actor(this._date);
+ vbox.add_actor(this._calendar);
+
+ this._displaysSection = new St.ScrollView({
+ style_class: 'datemenu-displays-section vfade',
+ x_expand: true,
+ overlay_scrollbars: true,
+ });
+ this._displaysSection.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL);
+ vbox.add_actor(this._displaysSection);
+
+ const displaysBox = new St.BoxLayout({
+ vertical: true,
+ x_expand: true,
+ style_class: 'datemenu-displays-box',
+ });
+ this._displaysSection.add_actor(displaysBox);
+
+ this._eventsItem = new EventsSection();
+ displaysBox.add_child(this._eventsItem);
+
+ this._clocksItem = new WorldClocksSection();
+ displaysBox.add_child(this._clocksItem);
+
+ this._weatherItem = new WeatherSection();
+ displaysBox.add_child(this._weatherItem);
+
+ // 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);
+ this._clock.connect('notify::timezone', this._updateTimeZone.bind(this));
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+ }
+
+ _getEventSource() {
+ return new Calendar.DBusEventSource();
+ }
+
+ _setEventSource(eventSource) {
+ if (this._eventSource)
+ this._eventSource.destroy();
+
+ this._calendar.setEventSource(eventSource);
+ this._eventsItem.setEventSource(eventSource);
+
+ this._eventSource = eventSource;
+ }
+
+ _updateTimeZone() {
+ // SpiderMonkey caches the time zone so we must explicitly clear it
+ // before we can update the calendar, see
+ // https://bugzilla.gnome.org/show_bug.cgi?id=678507
+ System.clearDateCaches();
+
+ this._calendar.updateTimeZone();
+ }
+
+ _sessionUpdated() {
+ 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;
+ }
+});