From 04e28cd7c4f70d4e404bb1ab78c9c7a70c181246 Mon Sep 17 00:00:00 2001 From: "Owen W. Taylor" Date: Wed, 30 Sep 2009 10:02:08 -0400 Subject: [PATCH] Add a calendar pop-down to the clock js/ui/calendar.js: Generic calendar widget tests/interactive/calendar.js: Basic test of the calendar js/ui/panel.js: Add a pop-down from the clock that shows a calendar widget. The pop-down is not menu-like to allow the user to interact with an application while looking at the calendar. gnome-shell.css: Add theming for calendar, calendar popup, and for buttons on the panel https://bugzilla.gnome.org/show_bug.cgi?id=596432 --- data/theme/gnome-shell.css | 57 +++++++++++++ js/ui/calendar.js | 154 ++++++++++++++++++++++++++++++++++ js/ui/panel.js | 68 ++++++++++++++- tests/interactive/calendar.js | 31 +++++++ 4 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 js/ui/calendar.js create mode 100644 tests/interactive/calendar.js diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index bb184c92a..ae5960962 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -61,6 +61,19 @@ StScrollBar StButton#vhandle:hover border-image: url("scroll-vhandle.png") 5; } +/* Panel */ + +.panel-button { + padding: 4px 12px 3px; + border-radius: 5px; + font: 16px sans-serif; + color: white; +} + +.panel-button:active, .panel-button:checked { + background-color: #314a6c; +} + /* LookingGlass */ #LookingGlassDialog @@ -95,3 +108,47 @@ StScrollBar StButton#vhandle:hover padding: 4px; spacing: 4px; } + +/* Calendar popup */ + +#calendarPopup { + border-radius: 5px; + background: rgba(0,0,0,0.9); + border: 1px solid rgba(128,128,128,0.45); + color: white; + padding: 10px; +} + +.calendar { + spacing-rows: 5px; + spacing-columns: 3px; +} + +.calendar-change-month { + padding: 2px; +} + +.calendar-change-month:hover { + background: #314a6c; + border-radius: 5px; +} + +.calendar-change-month:active { + background: #213050; + border-radius: 5px; +} + +.calendar-day { + padding: 1px 2px; +} + +.calendar-today { + font-weight: bold; + background: #ffffff; + color: black; + border-radius: 5px; +} + +.calendar-other-month-day { + color: #cccccc; +} diff --git a/js/ui/calendar.js b/js/ui/calendar.js new file mode 100644 index 000000000..633af0471 --- /dev/null +++ b/js/ui/calendar.js @@ -0,0 +1,154 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Lang = imports.lang; +const St = imports.gi.St; + +const Gettext_gtk20 = imports.gettext.domain('gtk20'); + +const MSECS_IN_DAY = 24 * 60 * 60 * 1000; + +function _sameDay(dateA, dateB) { + return (dateA.getDate() == dateB.getDate() && + dateA.getMonth() == dateB.getMonth() && + dateA.getYear() == dateB.getYear()); +} + +function Calendar() { + this._init(); +}; + +Calendar.prototype = { + _init: function() { + // FIXME: This is actually the fallback method for GTK+ for the week start; + // GTK+ by preference uses nl_langinfo (NL_TIME_FIRST_WEEKDAY). We probably + // should add a C function so we can do the full handling. + this._weekStart = NaN; + let weekStartString = Gettext_gtk20.gettext("calendar:week_start:0"); + if (weekStartString.indexOf("calendar:week_start:") == 0) { + this._weekStart = parseInt(weekStartString.substring(20)); + } + + if (isNaN(this._weekStart) || this._weekStart < 0 || this._weekStart > 6) { + log("Translation of 'calendar:week_start:0' in GTK+ is not correct"); + this.weekStart = 0; + } + + // Find the ordering for month/year in the calendar heading + switch (Gettext_gtk20.gettext("calendar:MY")) { + case "calendar:MY": + this._headerFormat = "%B %Y"; + break; + case "calendar:YM": + this._headerFormat = "%Y %B"; + break; + default: + log("Translation of 'calendar:MY' in GTK+ is not correct"); + this._headerFormat = "%B %Y"; + break; + } + + // Start off with the current date + this.date = new Date(); + + this.actor = new St.Table({ homogeneous: false, + style_class: "calendar" }); + + // Top line of the calendar '<| September 2009 |>' + this._topBox = new St.BoxLayout(); + this.actor.add(this._topBox, + { row: 0, col: 0, col_span: 7 }); + + let back = new St.Button({ label: "<", style_class: 'calendar-change-month' }); + this._topBox.add(back); + back.connect("clicked", Lang.bind(this, this._prevMonth)); + + this._dateLabel = new St.Label(); + this._topBox.add(this._dateLabel, { expand: true, x_fill: false, x_align: St.Align.MIDDLE }); + + let forward = new St.Button({ label: ">", style_class: 'calendar-change-month' }); + this._topBox.add(forward); + forward.connect("clicked", Lang.bind(this, this._nextMonth)); + + // We need to figure out the abbreviated localized names for the days of the week; + // we do this by just getting the next 7 days starting from right now and then putting + // them in the right cell in the table. It doesn't matter if we add them in order + let iter = new Date(this.date); + iter.setSeconds(0); // Leap second protection. Hah! + for (let i = 0; i < 7; i++) { + this.actor.add(new St.Label({ text: iter.toLocaleFormat("%a") }), + { row: 1, + col: (7 + iter.getDay() - this._weekStart) % 7, + x_fill: false, x_align: 1.0 }); + iter.setTime(iter.getTime() + MSECS_IN_DAY); + } + + this._update(); + }, + + // Sets the calendar to show a specific date + setDate: function(date) { + if (!_sameDay(date, this.date)) { + this.date = date; + this._update(); + } + }, + + _prevMonth: function() { + if (this.date.getMonth() == 0) { + this.date.setMonth(11); + this.date.setFullYear(this.date.getFullYear() - 1); + } else { + this.date.setMonth(this.date.getMonth() - 1); + } + this._update(); + }, + + _nextMonth: function() { + if (this.date.getMonth() == 11) { + this.date.setMonth(0); + this.date.setFullYear(this.date.getFullYear() + 1); + } else { + this.date.setMonth(this.date.getMonth() + 1); + } + this._update(); + }, + + _update: function() { + this._dateLabel.text = this.date.toLocaleFormat("%B %Y"); + + // Remove everything but the topBox and the weekday labels + let children = this.actor.get_children(); + for (let i = 8; i < children.length; i++) + children[i].destroy(); + + // Start at the beginning of the week before the start of the month + let iter = new Date(this.date); + iter.setDate(1); + iter.setSeconds(0); + iter.setTime(iter.getTime() - (iter.getDay() - this._weekStart) * MSECS_IN_DAY); + + let now = new Date(); + + let row = 2; + while (true) { + let label = new St.Label({ text: iter.getDate().toString() }); + if (_sameDay(now, iter)) + label.style_class = "calendar-day calendar-today"; + else if (iter.getMonth() != this.date.getMonth()) + label.style_class = "calendar-day calendar-other-month-day"; + else + label.style_class = "calendar-day"; + this.actor.add(label, + { row: row, col: (7 + iter.getDay() - this._weekStart) % 7, + x_fill: false, x_align: 1.0 }); + + iter.setTime(iter.getTime() + MSECS_IN_DAY); + if (iter.getDay() == this._weekStart) { + // We stop on the first "first day of the week" after the month we are displaying + if (iter.getMonth() > this.date.getMonth() || iter.getYear() > this.date.getYear()) + break; + row++; + } + } + } +}; diff --git a/js/ui/panel.js b/js/ui/panel.js index 2fbc1d89c..92a1bf804 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -7,12 +7,14 @@ const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; +const St = imports.gi.St; const Tweener = imports.ui.tweener; const Signals = imports.signals; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const Button = imports.ui.button; +const Calendar = imports.ui.calendar; const Main = imports.ui.main; const PANEL_HEIGHT = 26; @@ -313,10 +315,17 @@ Panel.prototype = { /* center */ + let clockButton = new St.Button({ style_class: "panel-button", + toggle_mode: true }); + this._centerBox.append(clockButton, Big.BoxPackFlags.NONE); + clockButton.connect('clicked', Lang.bind(this, this._toggleCalendar)); + this._clock = new Clutter.Text({ font_name: DEFAULT_FONT, color: PANEL_FOREGROUND_COLOR, text: "" }); - this._centerBox.append(this._clock, Big.BoxPackFlags.NONE); + clockButton.add_actor(this._clock); + + this._calendarPopup = null; /* right */ @@ -454,6 +463,16 @@ Panel.prototype = { return false; }, + _toggleCalendar: function(clockButton) { + if (clockButton.checked) { + if (this._calendarPopup == null) + this._calendarPopup = new CalendarPopup(); + this._calendarPopup.show(); + } else { + this._calendarPopup.hide(); + } + }, + _onHotCornerEntered : function() { if (!this._hotCornerEntered) { this._hotCornerEntered = true; @@ -485,3 +504,50 @@ Panel.prototype = { return false; } }; + +function CalendarPopup() { + this._init(); +} + +CalendarPopup.prototype = { + _init: function() { + let panelActor = Main.panel.actor; + + this.actor = new St.BoxLayout({ name: 'calendarPopup' }); + + this.calendar = new Calendar.Calendar(); + this.actor.add(this.calendar.actor); + + Main.chrome.actor.add_actor(this.actor); + Main.chrome.addInputRegionActor(this.actor); + this.actor.y = (panelActor.y + panelActor.height - this.actor.height); + }, + + show: function() { + let panelActor = Main.panel.actor; + + // Reset the calendar to today's date + this.calendar.setDate(new Date()); + + this.actor.x = Math.round((panelActor.x + panelActor.width - this.actor.width) / 2); + this.actor.lower(panelActor); + this.actor.show(); + Tweener.addTween(this.actor, + { y: panelActor.y + panelActor.height, + time: 0.2, + transition: "easeOutQuad" + }); + }, + + hide: function() { + let panelActor = Main.panel.actor; + + Tweener.addTween(this.actor, + { y: panelActor.y + panelActor.height - this.actor.height, + time: 0.2, + transition: "easeOutQuad", + onComplete: function() { this.actor.hide(); }, + onCompleteScope: this + }); + } +}; diff --git a/tests/interactive/calendar.js b/tests/interactive/calendar.js new file mode 100644 index 000000000..9336c95c9 --- /dev/null +++ b/tests/interactive/calendar.js @@ -0,0 +1,31 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; +const St = imports.gi.St; + +const Calendar =imports.ui.calendar; +const UI = imports.testcommon.ui; + +const Gettext_gtk20 = imports.gettext.domain('gtk20'); + +UI.init(); +let stage = Clutter.Stage.get_default(); +stage.width = stage.height = 400; +stage.show(); + +let vbox = new St.BoxLayout({ vertical: true, + width: stage.width, + height: stage.height, + style: 'padding: 10px; spacing: 10px; font: 15px sans-serif;' }); +stage.add_actor(vbox); + +let calendar = new Calendar.Calendar(); +vbox.add(calendar.actor, + { expand: true, + x_fill: false, x_align: St.Align.MIDDLE, + y_fill: false, y_align: St.Align.START }); + +stage.show(); +Clutter.main(); +stage.destroy();