880b240ecb
The message indicator conveys that the message list contains unseen messages that will not be shown as banner. So its visibility depends on two factors: the number of unseen messages, and the number of messages waiting in the queue to be shown as banner. As we currently only update the visibility on changes to the former, the indicator is not always accurate - for instance sources notify count changes before passing on a notification to the message tray for display. To fix, add a signal to the message tray to notify when the queue changes and use it to update the indicator's visibility. https://bugzilla.gnome.org/show_bug.cgi?id=744880
435 lines
15 KiB
JavaScript
435 lines
15 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,
|
|
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.can_focus = 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);
|
|
}
|
|
// 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);
|
|
|
|
let scroll = new St.ScrollView({ style_class: 'vfade',
|
|
x_expand: true, x_fill: true,
|
|
overlay_scrollbars: true });
|
|
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
|
|
vbox.add_actor(scroll);
|
|
|
|
let displaysBox = new St.BoxLayout({ vertical: true,
|
|
style_class: 'datemenu-displays-box' });
|
|
scroll.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);
|
|
}
|
|
});
|