From fdd9def9222f5dec61812a8159f5af263ba99b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 14 May 2020 22:24:36 +0200 Subject: [PATCH] dateMenu: Add "Events" section Events have a clear and obvious connection to the calendar, and similar to the Clocks and Weather sections there's a strong link to a particular application. Adding them as another section to the right-hand side of the calendar therefore presents a viable alternative to the old events section. https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1282 --- .../gnome-shell-sass/widgets/_calendar.scss | 26 +++ js/ui/dateMenu.js | 192 ++++++++++++++++++ 2 files changed, 218 insertions(+) diff --git a/data/theme/gnome-shell-sass/widgets/_calendar.scss b/data/theme/gnome-shell-sass/widgets/_calendar.scss index e99b8a82e..70d95193a 100644 --- a/data/theme/gnome-shell-sass/widgets/_calendar.scss +++ b/data/theme/gnome-shell-sass/widgets/_calendar.scss @@ -177,6 +177,32 @@ } } +/* Events */ +.events-button { + @include notification_bubble; + padding: $base_padding * 2; + + .events-box { + spacing: $base_spacing; + } + + .events-list { + spacing: 2 * $base_spacing; + } + + .events-title { + color: desaturate(darken($fg_color,40%), 10%); + font-weight: bold; + margin-bottom: $base_margin; + } + + .event-time { + color: darken($fg_color,20%); + font-feature-settings: "tnum"; + @include fontsize($base_font_size - 1); + } +} + /* World clocks */ .world-clocks-button { @include notification_bubble; diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index a98b5eec5..ce0a1f617 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -13,7 +13,11 @@ const System = imports.system; const { loadInterfaceXML } = imports.misc.fileUtils; +const NC_ = (context, str) => '%s\u0004%s'.format(context, str); +const T_ = Shell.util_translate_time_string; + const MAX_FORECASTS = 5; +const ELLIPSIS_CHAR = '\u2026'; const ClocksIntegrationIface = loadInterfaceXML('org.gnome.Shell.ClocksIntegration'); const ClocksProxy = Gio.DBusProxy.makeProxyWrapper(ClocksIntegrationIface); @@ -84,6 +88,188 @@ class TodayButton extends St.Button { } }); +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) { + const day = [date.getFullYear(), date.getMonth(), date.getDate()]; + this._startDate = new Date(...day); + this._endDate = new Date(...day, 23, 59, 59, 999); + + 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); + } + + _formatEventTime(event) { + const allDay = event.allDay || + (event.date <= this._startDate && event.end >= this._endDate); + + 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 { + let date = event.date >= this._startDate ? event.date : event.end; + title = Util.formatTime(date, { timeOnly: true }); + } + + const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; + if (event.date < this._startDate && !event.allDay) { + if (rtl) + title = '%s%s'.format(title, ELLIPSIS_CHAR); + else + title = '%s%s'.format(ELLIPSIS_CHAR, title); + } + if (event.end > this._endDate && !event.allDay) { + if (rtl) + title = '%s%s'.format(ELLIPSIS_CHAR, title); + else + title = '%s%s'.format(title, ELLIPSIS_CHAR); + } + 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() { @@ -632,6 +818,7 @@ class DateMenuButton extends PanelMenu.Button { this._calendar.connect('selected-date-changed', (_calendar, datetime) => { let date = _gDateTimeToDate(datetime); layout.frozen = !_isToday(date); + this._eventsItem.setDate(date); }); this.menu.connect('open-state-changed', (menu, isOpen) => { @@ -640,6 +827,7 @@ class DateMenuButton extends PanelMenu.Button { let now = new Date(); this._calendar.setDate(now); this._date.setDate(now); + this._eventsItem.setDate(now); } }); @@ -670,6 +858,9 @@ class DateMenuButton extends PanelMenu.Button { 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); @@ -695,6 +886,7 @@ class DateMenuButton extends PanelMenu.Button { this._eventSource.destroy(); this._calendar.setEventSource(eventSource); + this._eventsItem.setEventSource(eventSource); this._eventSource = eventSource; }