diff --git a/configure.ac b/configure.ac index a9533aa4d..6da1d197f 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,7 @@ GJS_MIN_VERSION=0.7 MUTTER_MIN_VERSION=2.91.0 GTK_MIN_VERSION=2.91.0 GIO_MIN_VERSION=2.25.9 +LIBICAL_MIN_VERSION=0.43 # Collect more than 20 libraries for a prize! PKG_CHECK_MODULES(MUTTER_PLUGIN, gio-2.0 >= $GIO_MIN_VERSION @@ -70,6 +71,7 @@ PKG_CHECK_MODULES(MUTTER_PLUGIN, gio-2.0 >= $GIO_MIN_VERSION gtk+-3.0 >= $GTK_MIN_VERSION mutter-plugins >= $MUTTER_MIN_VERSION gjs-gi-1.0 >= $GJS_MIN_VERSION + libical >= $LIBICAL_MIN_VERSION libgnome-menu $recorder_modules gconf-2.0 gdk-x11-3.0 clutter-x11-1.0 >= $CLUTTER_MIN_VERSION diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index e8b26daec..1348370bb 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -843,6 +843,10 @@ StTooltip { text-align: center; } +.calendar-day-base:active { + background: #666; +} + .calendar-day-heading { color: #666666; } @@ -876,6 +880,60 @@ StTooltip { color: #333333; } +.events-header { + height: 40px; +} + +.events-day-header { + padding-left: 20px; + padding-right: 40px; + font-weight: bold; + font-size: 14px; + color: rgba(153, 153, 153, 1.0); +} + +.events-day-time { + font-size: 14px; + font-weight: bold; + color: #fff; +} + +.events-day-task { + font-weight: bold; + font-size: 14px; + color: rgba(153, 153, 153, 1.0); +} + +.events-day-name-box { + width: 50px; +} + +.events-time-box { + width: 70px; +} + +.events-event-box { + width: 300px; +} + +.events-no-events { + font-weight: bold; + padding-left: 40px; + padding-right: 40px; + font-size: 14px; + color: rgba(153, 153, 153, 1.0); +} + +.open-calendar { + padding-bottom: 12px; + padding-left: 12px; + height: 40px; +} + +.open-calendar:hover { + background: #666; +} + /* Message Tray */ #message-tray { background-gradient-direction: vertical; diff --git a/js/ui/calendar.js b/js/ui/calendar.js index 1ead975ef..7aeb5ede4 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -4,6 +4,7 @@ const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const Lang = imports.lang; const St = imports.gi.St; +const Signals = imports.signals; const Pango = imports.gi.Pango; const Gettext_gtk20 = imports.gettext.domain('gtk20'); const Gettext = imports.gettext.domain('gnome-shell'); @@ -263,6 +264,12 @@ Calendar.prototype = { this._update(); }, + clearButtonsState: function() { + for (let i = 0; i < this._dayButtons.length; i++) { + this._dayButtons[i].remove_style_pseudo_class('active'); + } + }, + _update: function() { this._dateLabel.text = this.date.toLocaleFormat(this._headerFormat); @@ -282,8 +289,21 @@ Calendar.prototype = { let now = new Date(); let row = 2; + let dayButtons = []; + this._dayButtons = dayButtons; while (true) { - let label = new St.Label({ text: iter.getDate().toString() }); + let button = new St.Button({ label: iter.getDate().toString() }); + + dayButtons.push(button); + + let iterStr = iter.toUTCString(); + button.connect('clicked', Lang.bind(this, function() { + this.emit('activate', new Date(iterStr)); + for (let i = 0; i < dayButtons.length; i++) { + dayButtons[i].remove_style_pseudo_class('active'); + } + button.add_style_pseudo_class('active'); + })); let style_class; style_class = 'calendar-day-base calendar-day'; @@ -297,10 +317,10 @@ Calendar.prototype = { else if (iter.getMonth() != this.date.getMonth()) style_class += ' calendar-other-month-day'; - label.style_class = style_class; + button.style_class = style_class; let offsetCols = this._useWeekdate ? 1 : 0; - this.actor.add(label, + this.actor.add(button, { row: row, col: offsetCols + (7 + iter.getDay() - this._weekStart) % 7 }); if (this._useWeekdate && iter.getDay() == 4) { @@ -320,3 +340,198 @@ Calendar.prototype = { } } }; + +Signals.addSignalMethods(Calendar.prototype); + +function EvolutionEventsSource() { + this._init(); +} + +EvolutionEventsSource.prototype = { + _init: function() { + try { + this.EDataServer = imports.gi.EDataServer; + this.ECalendar = imports.gi.ECalendar; + } catch (e) {log(e);} + }, + + _ISODateString: function(d) { + function pad(n) { + return n < 10 ? '0' + n : n; + } + return '' + d.getUTCFullYear()// + '-' + + pad(d.getUTCMonth() + 1)// + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours())// + ':' + + pad(d.getUTCMinutes())// + ':' + + pad(d.getUTCSeconds()) + 'Z' + }, + + _getStartTime: function(str) { + let start = /DTSTART:\d{8}T\d{6}/.exec(str); + if (!start) + throw new Error("Bad data"); + start = start[0].substr(8); + let year = start.substr(0, 4), month = start.substr(4, 2), day = start.substr(6, 2); + let hour = start.substr(9, 2), minute = start.substr(12, 2); + return new Date(year, month - 1, day, hour, minute, 0, 0); + }, + + _getSummary: function(str) { + let match = /\nSUMMARY:.*(?=\r\n)/.exec(str); + if (!match) + return ""; + return match[0].substr(9); + }, + + getForInterval: function(begin, end, callback) { + if (!this.EDataServer) { + callback([]); + return; + } + let res = []; + let wait = 0; + let list = this.EDataServer.SourceList.new_for_gconf_default("/apps/evolution/calendar/sources"); + + let groups = list.peek_groups(); + + let query = '(occur-in-time-range? (make-time "' + this._ISODateString(begin) + '") (make-time "' + this._ISODateString(end) + '"))'; + + function decrimentWait() { + wait--; + if (wait == 0) { + res.sort(function (a, b) { + if (a.time == b.time) + return 0; + if (a.time > b.time) + return 1; + return -1; + }); + callback(res); + } + } + + for (let i = 0; i < groups.length; i++) { + let sources = groups[i].peek_sources(); + for (let k = 0; k < sources.length; k++) { + let cal = this.ECalendar.Cal.new(sources[k], this.ECalendar.CalSourceType.EVENT); + if (!cal) + continue; + let calOpenedExId = cal.connect('cal-opened-ex', Lang.bind(this, function(s, error) { + if (error) { + decrimentWait; + return; + } + let [success, view] = cal.get_query(query); + let viewObjectsAddedId = view.connect('objects-added', Lang.bind(this, function(o, list) { + for (let j = 0; j < list.length; j++) { + let event = global.icalcomponent_to_str(list[j]); + let start = this._getStartTime(event); + res.push({ time: start, title: this._getSummary(event) }); + } + })); + let viewCompleteId = view.connect('view-complete', function() { + cal.disconnect(calOpenedExId); + view.disconnect(viewObjectsAddedId); + view.disconnect(viewCompleteId); + decrimentWait(); + }); + view.start(); + })); + cal.open_async(false); + wait++; + } + } + + if (wait == 0) { + callback([]); + } + } +}; + +function EventsList() { + this._init(); +} + +EventsList.prototype = { + _init: function() { + this.actor = new St.BoxLayout({ vertical: true }); + this.evolutionTasks = new EvolutionEventsSource(); + }, + + _addPeriod: function(header, begin, end, isDay) { + this.actor.add(new St.Label({ style_class: 'events-day-header', + text: header })); + let box = new St.BoxLayout(); + let dayNameBox = new St.BoxLayout({ vertical: true, style_class: 'events-day-name-box' }); + let timeBox = new St.BoxLayout({ vertical: true, style_class: 'events-time-box' }); + let eventTitleBox = new St.BoxLayout({ vertical: true, style_class: 'events-event-box' }); + box.add(dayNameBox); + box.add(timeBox); + box.add(eventTitleBox); + this.actor.add(box); + + this.evolutionTasks.getForInterval(begin, end, Lang.bind(this, function(tasks) { + if (!tasks.length) { + eventTitleBox.add(new St.Label({ style_class: 'events-no-events', text: _("No events") })); + return; + } + + for (let i = 0; i < tasks.length; i++) { + let time = tasks[i].time.getHours() + ':'; + if (tasks[i].time.getMinutes() < 10) + time += '0'; + time += tasks[i].time.getMinutes(); + timeBox.add(new St.Label({ style_class: 'events-day-time', text: time })); + eventTitleBox.add(new St.Label({ style_class: 'events-day-task', text: tasks[i].title }), { expand: false }); + if (!isDay) + continue; + dayNameBox.add(new St.Label({ text: tasks[i].time.toLocaleFormat("%a") })); + } + })); + }, + + showDay: function(day) { + this.actor.destroy_children(); + + this.actor.add(new St.Bin({ style_class: 'events-header' })); + + let dayBegin = new Date(day.getTime()); + let dayEnd = new Date(day.getTime()); + dayBegin.setHours(0); + dayBegin.setMinutes(1); + dayEnd.setHours(23); + dayEnd.setMinutes(59); + this._addPeriod(day.toLocaleDateString(), dayBegin, dayEnd, false); + }, + + update: function() { + this.actor.destroy_children(); + + this.actor.add(new St.Bin({ style_class: 'events-header' })); + + let dayBegin = new Date(); + let dayEnd = new Date(); + dayBegin.setHours(0); + dayBegin.setMinutes(1); + dayEnd.setHours(23); + dayEnd.setMinutes(59); + this._addPeriod(_("Today"), dayBegin, dayEnd, false); + + dayBegin.setDate(dayBegin.getDate() + 1); + dayEnd.setDate(dayEnd.getDate() + 1); + this._addPeriod(_("Tomorrow"), dayBegin, dayEnd, false); + + if (dayEnd.getDay() == 6 || dayEnd.getDay() == 0) { + dayBegin.setDate(dayEnd.getDate() + 1); + dayEnd.setDate(dayBegin.getDate() + 6 - dayBegin.getDay()); + + this._addPeriod(_("Next week"), dayBegin, dayEnd, true); + return; + } + let d = 6 - dayEnd.getDay() - 1; + dayBegin.setDate(dayBegin.getDate() + 1); + dayEnd.setDate(dayEnd.getDate() + 1 + d); + this._addPeriod(_("This week"), dayBegin, dayEnd, true); + } +}; diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index 77d7bba30..0e6a644ec 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -67,6 +67,15 @@ DateMenuButton.prototype = { this._calendar = new Calendar.Calendar(); this.menu._box.add(this._calendar.actor); + this._taskList = new Calendar.EventsList(); + this.menu.connect('opening', Lang.bind(this, function() { + this._calendar.clearButtonsState(); + this._taskList.update(); + })); + this._calendar.connect('activate', Lang.bind(this, function(obj, day) { + this._taskList.showDay(day); + })); + item = new PopupMenu.PopupSeparatorMenuItem(); this.menu.addMenuItem(item); @@ -89,7 +98,15 @@ DateMenuButton.prototype = { hbox = new St.BoxLayout(); hbox.add(orig_menu_box); hbox.add(this._vertSep); - hbox.add(new St.Label({text: "foo0"})); + + let calendarButton = new St.Button({ label: _("Open Calendar"), + style_class: 'open-calendar', + x_align: St.Align.START }); + let box = new St.BoxLayout({ vertical: true }); + box.add(this._taskList.actor, { expand: true, x_fill: true, y_fill: true }); + box.add(calendarButton); + + hbox.add(box); this.menu._boxPointer.bin.set_child(hbox); this.menu._box = hbox; diff --git a/src/shell-global.c b/src/shell-global.c index 1e42fc9f0..1a2018d97 100644 --- a/src/shell-global.c +++ b/src/shell-global.c @@ -25,6 +25,7 @@ #include #include #include +#include #ifdef HAVE_SYS_RESOURCE_H #include #endif @@ -446,6 +447,17 @@ shell_global_set_stage_input_mode (ShellGlobal *global, } } +/** + * shell_global_icalcomponent_to_str: + * + * Wrap icalcomponent_as_ical_string_r + */ +char* +shell_global_icalcomponent_to_str (long icalcomp) +{ + return icalcomponent_as_ical_string_r ((icalcomponent*)icalcomp); +} + /** * shell_global_set_cursor: * @global: A #ShellGlobal diff --git a/src/shell-global.h b/src/shell-global.h index 5cf5e1373..368b79a28 100644 --- a/src/shell-global.h +++ b/src/shell-global.h @@ -101,6 +101,7 @@ void shell_global_get_pointer (ShellGlobal *global, ClutterModifierType *mods); GSettings *shell_global_get_settings (ShellGlobal *global); +char* shell_global_icalcomponent_to_str (long icalcomp); ClutterModifierType shell_get_event_state (ClutterEvent *event);