[gnome-shell] Add a calendar pop-down to the clock



commit 04e28cd7c4f70d4e404bb1ab78c9c7a70c181246
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Wed Sep 30 10:02:08 2009 -0400

    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 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index bb184c9..ae59609 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 0000000..633af04
--- /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: "&lt;", 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: "&gt;", 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 2fbc1d8..92a1bf8 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 0000000..9336c95
--- /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();



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]