diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css index be92d5344..33c95cc6a 100644 --- a/data/theme/gnome-shell-high-contrast.css +++ b/data/theme/gnome-shell-high-contrast.css @@ -736,6 +736,7 @@ StScrollBar { .datemenu-today-button, .world-clocks-button, +.weather-button, .message-list-section-title { border-radius: 4px; padding: .4em; } @@ -749,11 +750,14 @@ StScrollBar { .datemenu-today-button:hover, .datemenu-today-button:focus, .world-clocks-button:hover, .world-clocks-button:focus, +.weather-button:hover, +.weather-button:focus, .message-list-section-title:hover, .message-list-section-title:focus { background-color: #0d0d0d; } .datemenu-today-button:active, .world-clocks-button:active, +.weather-button:active, .message-list-section-title:active { color: white; background-color: #215d9c; } @@ -762,6 +766,7 @@ StScrollBar { font-size: 1.5em; } .world-clocks-header, +.weather-header, .message-list-section-title { color: #999999; font-weight: bold; } @@ -769,6 +774,9 @@ StScrollBar { .world-clocks-grid { spacing-rows: 0.4em; } +.weather-box { + spacing: 0.4em; } + .calendar-month-label { color: #f2f2f2; font-weight: bold; diff --git a/data/theme/gnome-shell-sass b/data/theme/gnome-shell-sass index 50bbd0b50..2af71071a 160000 --- a/data/theme/gnome-shell-sass +++ b/data/theme/gnome-shell-sass @@ -1 +1 @@ -Subproject commit 50bbd0b50ff81d2a6d8441d79ac9e7be64517bb8 +Subproject commit 2af71071ae2f4e4e89851dee93befd5007d4d693 diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index f6d2f67c9..88ec11bc6 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -736,6 +736,7 @@ StScrollBar { .datemenu-today-button, .world-clocks-button, +.weather-button, .message-list-section-title { border-radius: 4px; padding: .4em; } @@ -749,11 +750,14 @@ StScrollBar { .datemenu-today-button:hover, .datemenu-today-button:focus, .world-clocks-button:hover, .world-clocks-button:focus, +.weather-button:hover, +.weather-button:focus, .message-list-section-title:hover, .message-list-section-title:focus { background-color: #454c4c; } .datemenu-today-button:active, .world-clocks-button:active, +.weather-button:active, .message-list-section-title:active { color: white; background-color: #215d9c; } @@ -762,6 +766,7 @@ StScrollBar { font-size: 1.5em; } .world-clocks-header, +.weather-header, .message-list-section-title { color: #8e8e80; font-weight: bold; } @@ -769,6 +774,9 @@ StScrollBar { .world-clocks-grid { spacing-rows: 0.4em; } +.weather-box { + spacing: 0.4em; } + .calendar-month-label { color: #e2e2df; font-weight: bold; diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index 9e49e2d9a..53806721e 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -8,6 +8,7 @@ const Gtk = imports.gi.Gtk; const GWeather = imports.gi.GWeather; const Lang = imports.lang; const Mainloop = imports.mainloop; +const Pango = imports.gi.Pango; const Cairo = imports.cairo; const Clutter = imports.gi.Clutter; const Shell = imports.gi.Shell; @@ -20,6 +21,7 @@ const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const PopupMenu = imports.ui.popupMenu; const Calendar = imports.ui.calendar; +const Weather = imports.misc.weather; function _isToday(date) { let now = new Date(); @@ -192,6 +194,133 @@ const WorldClocksSection = new Lang.Class({ } }); +const WeatherSection = new Lang.Class({ + Name: 'WeatherSection', + + _init: function() { + this._weatherClient = new Weather.WeatherClient(); + + this.actor = new St.Button({ style_class: 'weather-button', + x_fill: true, + can_focus: true }); + this.actor.connect('clicked', () => { + this._weatherClient.activateApp(); + + Main.overview.hide(); + Main.panel.closeCalendar(); + }); + this.actor.connect('notify::mapped', () => { + if (this.actor.mapped) + this._weatherClient.update(); + }); + + let box = new St.BoxLayout({ style_class: 'weather-box', + vertical: true }); + + this.actor.child = box; + + box.add_child(new St.Label({ style_class: 'weather-header', + x_align: Clutter.ActorAlign.START, + text: _("Weather") })); + + this._conditionsLabel = new St.Label({ style_class: 'weather-conditions', + x_align: Clutter.ActorAlign.START }); + this._conditionsLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this._conditionsLabel.clutter_text.line_wrap = true; + box.add_child(this._conditionsLabel); + + this._weatherClient.connect('changed', Lang.bind(this, this._sync)); + this._sync(); + }, + + _getSummary: function(info) { + let summary = info.get_conditions(); + if (summary == '-') + return info.get_sky(); + return summary; + }, + + _sameSummary: function(info1, info2) { + let [ok1, phenom1, qualifier1] = info1.get_value_conditions(); + let [ok2, phenom2, qualifier2] = info2.get_value_conditions(); + if (ok1 || ok2) + return ok1 == ok2 && phenom1 == phenom2 && qualifier1 == qualifier2; + + let [, sky1] = info1.get_value_sky(); + let [, sky2] = info2.get_value_sky(); + return sky1 == sky2; + }, + + _getSummaryText: function() { + let info = this._weatherClient.info; + let forecasts = info.get_forecast_list(); + if (forecasts.length == 0) // No forecasts, just current conditions + return '%s.'.format(this._getSummary(info)); + + let current = info; + let summaries = [this._getSummary(info)]; + for (let i = 0; i < forecasts.length; i++) { + let [ok, timestamp] = forecasts[i].get_value_update(); + if (!_isToday(new Date(timestamp * 1000))) + continue; // Ignore forecasts from other days + + if (this._sameSummary(current, forecasts[i])) + continue; // Ignore consecutive runs of equal summaries + + current = forecasts[i]; + if (summaries.push(this._getSummary(current)) == 3) + break; // Use a maximum of three summaries + } + + let fmt; + switch(summaries.length) { + /* Translators: %s is a weather condition like "Clear sky"; see + libgweather for the possible condition strings. If at all + possible, the sentence should match the grammatical case etc. of + the inserted conditions. */ + case 1: fmt = _("%s all day."); break; + + /* Translators: %s is a weather condition like "Clear sky"; see + libgweather for the possible condition strings. If at all + possible, the sentence should match the grammatical case etc. of + the inserted conditions. */ + case 2: fmt = _("%s, then %s later."); break; + + /* Translators: %s is a weather condition like "Clear sky"; see + libgweather for the possible condition strings. If at all + possible, the sentence should match the grammatical case etc. of + the inserted conditions. */ + case 3: fmt = _("%s, then %s, followed by %s later."); break; + } + return String.prototype.format.apply(fmt, summaries); + }, + + _getLabelText: function() { + if (this._weatherClient.loading) + return _("Loading…"); + + let info = this._weatherClient.info; + if (info.is_valid()) + return this._getSummaryText() + ' ' + + /* Translators: %s is a temperature with unit, e.g. "23℃" */ + _("Feels like %s.").format(info.get_apparent()); + + if (info.network_error()) + return _("Go online for weather information"); + + return _("Weather information is currently unavailable"); + }, + + _sync: function() { + this.actor.visible = this._weatherClient.available; + + if (!this.actor.visible) + return; + + this._conditionsLabel.text = this._getLabelText(); + } +}); + const MessagesIndicator = new Lang.Class({ Name: 'MessagesIndicator', @@ -394,6 +523,8 @@ const DateMenuButton = new Lang.Class({ this._clocksItem = new WorldClocksSection(); displaysBox.add(this._clocksItem.actor, { x_fill: true }); + this._weatherItem = new WeatherSection(); + displaysBox.add(this._weatherItem.actor, { x_fill: true }); // Done with hbox for calendar and event list