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
This commit is contained in:
parent
2cc41c6726
commit
04e28cd7c4
@ -61,6 +61,19 @@ StScrollBar StButton#vhandle:hover
|
|||||||
border-image: url("scroll-vhandle.png") 5;
|
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 */
|
/* LookingGlass */
|
||||||
|
|
||||||
#LookingGlassDialog
|
#LookingGlassDialog
|
||||||
@ -95,3 +108,47 @@ StScrollBar StButton#vhandle:hover
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
spacing: 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;
|
||||||
|
}
|
||||||
|
154
js/ui/calendar.js
Normal file
154
js/ui/calendar.js
Normal file
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -7,12 +7,14 @@ const Lang = imports.lang;
|
|||||||
const Mainloop = imports.mainloop;
|
const Mainloop = imports.mainloop;
|
||||||
const Meta = imports.gi.Meta;
|
const Meta = imports.gi.Meta;
|
||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
|
const St = imports.gi.St;
|
||||||
const Tweener = imports.ui.tweener;
|
const Tweener = imports.ui.tweener;
|
||||||
const Signals = imports.signals;
|
const Signals = imports.signals;
|
||||||
const Gettext = imports.gettext.domain('gnome-shell');
|
const Gettext = imports.gettext.domain('gnome-shell');
|
||||||
const _ = Gettext.gettext;
|
const _ = Gettext.gettext;
|
||||||
|
|
||||||
const Button = imports.ui.button;
|
const Button = imports.ui.button;
|
||||||
|
const Calendar = imports.ui.calendar;
|
||||||
const Main = imports.ui.main;
|
const Main = imports.ui.main;
|
||||||
|
|
||||||
const PANEL_HEIGHT = 26;
|
const PANEL_HEIGHT = 26;
|
||||||
@ -313,10 +315,17 @@ Panel.prototype = {
|
|||||||
|
|
||||||
/* center */
|
/* 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,
|
this._clock = new Clutter.Text({ font_name: DEFAULT_FONT,
|
||||||
color: PANEL_FOREGROUND_COLOR,
|
color: PANEL_FOREGROUND_COLOR,
|
||||||
text: "" });
|
text: "" });
|
||||||
this._centerBox.append(this._clock, Big.BoxPackFlags.NONE);
|
clockButton.add_actor(this._clock);
|
||||||
|
|
||||||
|
this._calendarPopup = null;
|
||||||
|
|
||||||
/* right */
|
/* right */
|
||||||
|
|
||||||
@ -454,6 +463,16 @@ Panel.prototype = {
|
|||||||
return false;
|
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() {
|
_onHotCornerEntered : function() {
|
||||||
if (!this._hotCornerEntered) {
|
if (!this._hotCornerEntered) {
|
||||||
this._hotCornerEntered = true;
|
this._hotCornerEntered = true;
|
||||||
@ -485,3 +504,50 @@ Panel.prototype = {
|
|||||||
return false;
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
31
tests/interactive/calendar.js
Normal file
31
tests/interactive/calendar.js
Normal file
@ -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();
|
Loading…
Reference in New Issue
Block a user