diff --git a/configure.ac b/configure.ac index d19bce0a6..2cab70d09 100644 --- a/configure.ac +++ b/configure.ac @@ -66,6 +66,10 @@ GJS_MIN_VERSION=0.7.8 MUTTER_MIN_VERSION=2.91.4 GTK_MIN_VERSION=2.91.7 GIO_MIN_VERSION=2.25.9 +LIBECAL_REQUIRED=1.6.0 +LIBEDATASERVER_REQUIRED=1.2.0 +LIBEDATASERVERUI_REQUIRED=1.2.0 + # Collect more than 20 libraries for a prize! PKG_CHECK_MODULES(MUTTER_PLUGIN, gio-2.0 >= $GIO_MIN_VERSION @@ -113,6 +117,10 @@ PKG_CHECK_EXISTS([gnome-bluetooth-1.0 >= 2.90.0], AC_SUBST([HAVE_BLUETOOTH],[0]) AC_MSG_RESULT([no])]) +PKG_CHECK_MODULES(LIBECAL, libecal-1.2 >= $LIBECAL_REQUIRED libedataserver-1.2 >= $LIBEDATASERVER_REQUIRED libedataserverui-1.2 >= $LIBEDATASERVERUI_REQUIRED) +AC_SUBST(LIBECAL_CFLAGS) +AC_SUBST(LIBECAL_LIBS) + MUTTER_BIN_DIR=`$PKG_CONFIG --variable=exec_prefix mutter-plugins`/bin # FIXME: metacity-plugins.pc should point directly to its .gir file MUTTER_LIB_DIR=`$PKG_CONFIG --variable=libdir mutter-plugins` diff --git a/data/Makefile.am b/data/Makefile.am index 62eed7c88..2de2ff976 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -25,6 +25,8 @@ dist_images_DATA = \ themedir = $(pkgdatadir)/theme dist_theme_DATA = \ theme/add-workspace.svg \ + theme/calendar-arrow-left.svg \ + theme/calendar-arrow-right.svg \ theme/close-window.svg \ theme/close.svg \ theme/corner-ripple.png \ diff --git a/data/theme/calendar-arrow-left.svg b/data/theme/calendar-arrow-left.svg new file mode 100644 index 000000000..d5d97b3c3 --- /dev/null +++ b/data/theme/calendar-arrow-left.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/calendar-arrow-right.svg b/data/theme/calendar-arrow-right.svg new file mode 100644 index 000000000..545da7ec5 --- /dev/null +++ b/data/theme/calendar-arrow-right.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index d6616aa14..73d2b5c9b 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -674,6 +674,17 @@ StTooltip StLabel { /* Calendar popup */ +#calendarArea { + /* this is the width of the entire popup */ + width: 600px; +} + +.calendar-vertical-separator { + -stipple-width: 1px; + -stipple-color: #505050; + width: 1.5em; +} + #calendarPopup { border-radius: 5px; background: rgba(0,0,0,0.9); @@ -686,37 +697,155 @@ StTooltip StLabel { } .calendar { - spacing-rows: 5px; - spacing-columns: 3px; + padding: .4em 1.75em; + spacing-rows: 0px; + spacing-columns: 0px; } -.calendar-change-month { +.calendar-month-label { + color: #666666; + font-size: 10px; padding: 2px; } -.calendar-change-month:hover { - background: #314a6c; - border-radius: 5px; +.calendar-change-month-back { + width: 20px; + height: 20px; + background-image: url("calendar-arrow-left.svg"); + border-radius: 4px; +} +.calendar-change-month-back:hover { + background-color: #999999; +} +.calendar-change-month-back:active { + background-color: #aaaaaa; } -.calendar-change-month:active { - background: #213050; - border-radius: 5px; +.calendar-change-month-forward { + width: 20px; + height: 20px; + background-image: url("calendar-arrow-right.svg"); + border-radius: 4px; +} +.calendar-change-month-forward:hover { + background-color: #999999; +} +.calendar-change-month-forward:active { + background-color: #aaaaaa; } +.datemenu-date-label { + padding: .4em 1.75em; + font-size: 16px; + color: #ffffff; +} + +.calendar-day-base { + font-size: 10px; + text-align: center; + width: 24px; + height: 24px; +} + +.calendar-day-base:hover { + background: #777777; +} + +.calendar-day-base:active { + background: #555555; +} + +.calendar-day-heading { + color: #666666; +} + +.calendar-week-number { + color: #666666; + font-weight: bold; +} + +/* Hack used in lieu of border-collapse - see calendar.js */ .calendar-day { - padding: 1px 2px; + border: 1px solid #333333; + color: #cccccc; + border-top-width: 0; + border-left-width: 0; +} +.calendar-day-top { + border-top-width: 1px; +} +.calendar-day-left { + border-left-width: 1px; +} + +.calendar-work-day { +} + +.calendar-nonwork-day { + background-color: rgba(128, 128, 128, .1); } .calendar-today { + color: #ffffff; font-weight: bold; - background: #ffffff; - color: black; - border-radius: 5px; + background-gradient-direction: vertical; + background-gradient-start: #3c3c3c; + background-gradient-end: #131313; } .calendar-other-month-day { - color: #cccccc; + color: #333333; +} + +.calendar-day-with-events { + font-weight: bold; +} + +.events-header-vbox { + spacing: 10px; +} + +.events-header { + height: 40px; +} + +.events-header-hbox { + spacing: 8px; + padding: 0.3em; +} + +.events-day-header { + font-size: 14px; + color: rgba(153, 153, 153, 1.0); +} + +.events-day-dayname { + font-size: 12px; + color: rgba(153, 153, 153, 1.0); + text-align: left; +} + +.events-day-time { + font-size: 12px; + font-weight: bold; + color: #fff; + text-align: right; +} + +.events-day-task { + font-size: 12px; + color: rgba(153, 153, 153, 1.0); +} + +.events-day-name-box { + width: 20px; +} + +.events-time-box { + width: 70px; +} + +.events-event-box { } .url-highlighter { @@ -895,10 +1024,6 @@ StTooltip StLabel { padding-left: 4px; } -.calendar-calendarweek { - color: #666666; -} - /* App Switcher */ #altTabPopup { padding: 8px; diff --git a/js/Makefile.am b/js/Makefile.am index 3a7a2da06..21683af19 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -19,6 +19,7 @@ nobase_dist_js_DATA = \ ui/chrome.js \ ui/ctrlAltTab.js \ ui/dash.js \ + ui/dateMenu.js \ ui/dnd.js \ ui/docDisplay.js \ ui/endSessionDialog.js \ diff --git a/js/ui/calendar.js b/js/ui/calendar.js index 8f58f69f7..236c3ec67 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -4,19 +4,78 @@ 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_gtk30 = imports.gettext.domain('gtk30'); +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; +const Mainloop = imports.mainloop; +const Shell = imports.gi.Shell; const MSECS_IN_DAY = 24 * 60 * 60 * 1000; const WEEKDATE_HEADER_WIDTH_DIGITS = 3; const SHOW_WEEKDATE_KEY = 'show-weekdate'; +// in org.gnome.desktop.interface +const CLOCK_FORMAT_KEY = 'clock-format'; + function _sameDay(dateA, dateB) { return (dateA.getDate() == dateB.getDate() && dateA.getMonth() == dateB.getMonth() && dateA.getYear() == dateB.getYear()); } +function _sameYear(dateA, dateB) { + return (dateA.getYear() == dateB.getYear()); +} + +/* TODO: maybe needs config - right now we assume that Saturday and + * Sunday are non-work days (not true in e.g. Israel, it's Sunday and + * Monday there) + */ +function _isWorkDay(date) { + return date.getDay() != 0 && date.getDay() != 6; +} + +function _getBeginningOfDay(date) { + let ret = new Date(date.getTime()); + ret.setHours(0); + ret.setMinutes(0); + ret.setSeconds(0); + ret.setMilliseconds(0); + return ret; +} + +function _getEndOfDay(date) { + let ret = new Date(date.getTime()); + ret.setHours(23); + ret.setMinutes(59); + ret.setSeconds(59); + ret.setMilliseconds(999); + return ret; +} + +function _formatEventTime(event, clockFormat) { + let ret; + if (event.allDay) { + /* Translators: Shown in calendar event list for all day events */ + ret = _("All Day"); + } else { + switch (clockFormat) { + case '24h': + ret = event.date.toLocaleFormat('%H:%M'); + break; + + default: + /* explicit fall-through */ + case '12h': + ret = event.date.toLocaleFormat('%l:%M %p'); + break; + } + } + return ret; +} + function _getCalendarWeekForDate(date) { // Based on the algorithms found here: // http://en.wikipedia.org/wiki/Talk:ISO_week_date @@ -43,12 +102,259 @@ function _getDigitWidth(actor){ return width; } -function Calendar() { +function _getCalendarDayAbbreviation(dayNumber) { + let abbreviations = [ + /* Translators: Calendar grid abbreviation for Sunday. + * + * NOTE: These abbreviations are always shown together and in + * order, e.g. "S M T W T F S". + */ + _("S"), + /* Translators: Calendar grid abbreviation for Monday */ + _("M"), + /* Translators: Calendar grid abbreviation for Tuesday */ + _("T"), + /* Translators: Calendar grid abbreviation for Wednesday */ + _("W"), + /* Translators: Calendar grid abbreviation for Thursday */ + _("T"), + /* Translators: Calendar grid abbreviation for Friday */ + _("F"), + /* Translators: Calendar grid abbreviation for Saturday */ + _("S") + ]; + return abbreviations[dayNumber]; +} + +function _getEventDayAbbreviation(dayNumber) { + let abbreviations = [ + /* Translators: Event list abbreviation for Sunday. + * + * NOTE: These abbreviations are normally not shown together + * so they need to be unique (e.g. Tuesday and Thursday cannot + * both be 'T'). + */ + _("Su"), + /* Translators: Event list abbreviation for Monday */ + _("M"), + /* Translators: Event list abbreviation for Tuesday */ + _("T"), + /* Translators: Event list abbreviation for Wednesday */ + _("W"), + /* Translators: Event list abbreviation for Thursday */ + _("Th"), + /* Translators: Event list abbreviation for Friday */ + _("F"), + /* Translators: Event list abbreviation for Saturday */ + _("S") + ]; + return abbreviations[dayNumber]; +} + +// Abstraction for an appointment/event in a calendar + +function CalendarEvent(date, summary, allDay) { + this._init(date, summary, allDay); +} + +CalendarEvent.prototype = { + _init: function(date, summary, allDay) { + this.date = date; + this.summary = summary; + this.allDay = allDay; + } +}; + +// Interface for appointments/events - e.g. the contents of a calendar +// + +// First, an implementation with no events +function EmptyEventSource() { this._init(); } -Calendar.prototype = { +EmptyEventSource.prototype = { _init: function() { + }, + + requestRange: function(begin, end) { + }, + + getEvents: function(begin, end) { + let result = []; + return result; + }, + + hasEvents: function(day) { + return false; + } +}; +Signals.addSignalMethods(EmptyEventSource.prototype); + +// Second, wrap native Evolution event source +function EvolutionEventSource() { + this._init(); +} + +EvolutionEventSource.prototype = { + _init: function() { + this._native = new Shell.EvolutionEventSource(); + this._native.connect('changed', Lang.bind(this, function() { + this.emit('changed'); + })); + }, + + requestRange: function(begin, end) { + this._native.request_range(begin.getTime(), end.getTime()); + }, + + getEvents: function(begin, end) { + let result = []; + let nativeEvents = this._native.get_events(begin.getTime(), end.getTime()); + for (let n = 0; n < nativeEvents.length; n++) { + let nativeEvent = nativeEvents[n]; + result.push(new CalendarEvent(new Date(nativeEvent.msec_begin), nativeEvent.summary, nativeEvent.all_day)); + } + return result; + }, + + hasEvents: function(day) { + let dayBegin = _getBeginningOfDay(day); + let dayEnd = _getEndOfDay(day); + + let events = this.getEvents(dayBegin, dayEnd); + + if (events.length == 0) + return false; + + return true; + } +}; +Signals.addSignalMethods(EvolutionEventSource.prototype); + +// Finally, an implementation with fake events +function FakeEventSource() { + this._init(); +} + +FakeEventSource.prototype = { + _init: function() { + + this._fakeEvents = []; + + // Generate fake events + // + let midnightToday = _getBeginningOfDay(new Date()); + let summary = ''; + + // '10-oclock pow-wow' is an event occuring IN THE PAST every four days at 10am + for (let n = 0; n < 10; n++) { + let t = new Date(midnightToday.getTime() - n * 4 * 86400 * 1000); + t.setHours(10); + summary = '10-oclock pow-wow (n=' + n + ')'; + this._fakeEvents.push(new CalendarEvent(t, summary, false)); + } + + // '11-oclock thing' is an event occuring every three days at 11am + for (let n = 0; n < 10; n++) { + let t = new Date(midnightToday.getTime() + n * 3 * 86400 * 1000); + t.setHours(11); + summary = '11-oclock thing (n=' + n + ')'; + this._fakeEvents.push(new CalendarEvent(t, summary, false)); + } + + // 'Weekly Meeting' is an event occuring every seven days at 1:45pm (two days displaced) + for (let n = 0; n < 5; n++) { + let t = new Date(midnightToday.getTime() + (n * 7 + 2) * 86400 * 1000); + t.setHours(13); + t.setMinutes(45); + summary = 'Weekly Meeting (n=' + n + ')'; + this._fakeEvents.push(new CalendarEvent(t, summary, false)); + } + + // 'Fun All Day' is an all-day event occuring every fortnight (three days displayed) + for (let n = 0; n < 10; n++) { + let t = new Date(midnightToday.getTime() + (n * 14 + 3) * 86400 * 1000); + summary = 'Fun All Day (n=' + n + ')'; + this._fakeEvents.push(new CalendarEvent(t, summary, true)); + } + + // 'Get Married' is an event that actually reflects reality (Dec 4, 2010) :-) + this._fakeEvents.push(new CalendarEvent(new Date(2010, 11, 4, 16, 0), 'Get Married', false)); + + // ditto for 'NE Patriots vs NY Jets' + this._fakeEvents.push(new CalendarEvent(new Date(2010, 11, 6, 20, 30), 'NE Patriots vs NY Jets', false)); + + // An event for tomorrow @6:30pm that is added/removed every five + // seconds (to check that the ::changed signal works) + let transientEventDate = new Date(midnightToday.getTime() + 86400 * 1000); + transientEventDate.setHours(18); + transientEventDate.setMinutes(30); + transientEventDate.setSeconds(0); + Mainloop.timeout_add(5000, Lang.bind(this, this._updateTransientEvent)); + this._includeTransientEvent = false; + this._transientEvent = new CalendarEvent(transientEventDate, 'A Transient Event', false); + this._transientEventCounter = 1; + }, + + _updateTransientEvent: function() { + this._includeTransientEvent = !this._includeTransientEvent; + this._transientEventCounter = this._transientEventCounter + 1; + this._transientEvent.summary = 'A Transient Event (' + this._transientEventCounter + ')'; + this.emit('changed'); + Mainloop.timeout_add(5000, Lang.bind(this, this._updateTransientEvent)); + }, + + requestRange: function(begin, end) { + }, + + getEvents: function(begin, end) { + let result = []; + //log('begin:' + begin); + //log('end: ' + end); + for(let n = 0; n < this._fakeEvents.length; n++) { + let event = this._fakeEvents[n]; + if (event.date >= begin && event.date <= end) { + result.push(event); + } + //log('when:' + event.date + ' summary:' + event.summary); + } + if (this._includeTransientEvent && this._transientEvent.date >= begin && this._transientEvent.date <= end) + result.push(this._transientEvent); + result.sort(function(event1, event2) { + return event1.date.getTime() - event2.date.getTime(); + }); + return result; + }, + + hasEvents: function(day) { + let dayBegin = _getBeginningOfDay(day); + let dayEnd = _getEndOfDay(day); + + let events = this.getEvents(dayBegin, dayEnd); + + if (events.length == 0) + return false; + + return true; + } + +}; +Signals.addSignalMethods(FakeEventSource.prototype); + +// Calendar: +// @eventSource: is an object implementing the EventSource API, e.g. the +// requestRange(), getEvents(), hasEvents() methods and the ::changed signal. +function Calendar(eventSource) { + this._init(eventSource); +} + +Calendar.prototype = { + _init: function(eventSource) { + this._eventSource = eventSource; + + this._eventSource.connect('changed', Lang.bind(this, this._update)); + // 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. @@ -71,6 +377,7 @@ Calendar.prototype = { } // Find the ordering for month/year in the calendar heading + this._headerFormatWithoutYear = '%B'; switch (Gettext_gtk30.gettext('calendar:MY')) { case 'calendar:MY': this._headerFormat = '%B %Y'; @@ -85,7 +392,7 @@ Calendar.prototype = { } // Start off with the current date - this.date = new Date(); + this._selectedDate = new Date(); this.actor = new St.Table({ homogeneous: false, style_class: 'calendar', @@ -100,9 +407,10 @@ Calendar.prototype = { // Sets the calendar to show a specific date setDate: function(date) { - if (!_sameDay(date, this.date)) { - this.date = date; + if (!_sameDay(date, this._selectedDate)) { + this._selectedDate = date; this._update(); + this.emit('selected-date-changed', new Date(this._selectedDate)); } }, @@ -116,45 +424,36 @@ Calendar.prototype = { { row: 0, col: 0, col_span: offsetCols + 7 }); this.actor.connect('style-changed', Lang.bind(this, this._onStyleChange)); - let [backlabel, forwardlabel] = ['<', '>']; - if (St.Widget.get_default_direction () == St.TextDirection.RTL) { - [backlabel, forwardlabel] = [forwardlabel, backlabel]; - } - let back = new St.Button({ label: backlabel, style_class: 'calendar-change-month' }); + let back = new St.Button({ style_class: 'calendar-change-month-back' }); this._topBox.add(back); - back.connect('clicked', Lang.bind(this, this._prevMonth)); + back.connect('clicked', Lang.bind(this, this._onPrevMonthButtonClicked)); - this._dateLabel = new St.Label(); - this._topBox.add(this._dateLabel, { expand: true, x_fill: false, x_align: St.Align.MIDDLE }); + this._monthLabel = new St.Label({style_class: 'calendar-month-label'}); + this._topBox.add(this._monthLabel, { expand: true, x_fill: false, x_align: St.Align.MIDDLE }); - let forward = new St.Button({ label: forwardlabel, style_class: 'calendar-change-month' }); + let forward = new St.Button({ style_class: 'calendar-change-month-forward' }); this._topBox.add(forward); - forward.connect('clicked', Lang.bind(this, this._nextMonth)); + forward.connect('clicked', Lang.bind(this, this._onNextMonthButtonClicked)); + // Add weekday labels... + // // 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); + let iter = new Date(this._selectedDate); iter.setSeconds(0); // Leap second protection. Hah! iter.setHours(12); - - if (this._useWeekdate) { - this._weekdateHeader = new St.Label(); - this.actor.add(this._weekdateHeader, - { row: 1, - col: 0, - x_fill: false, x_align: St.Align.MIDDLE }); - this._setWeekdateHeaderWidth(); - } else { - this._weekdateHeader = null; - } - for (let i = 0; i < 7; i++) { - this.actor.add(new St.Label({ text: iter.toLocaleFormat('%a') }), + // Could use iter.toLocaleFormat('%a') but that normally gives three characters + // and we want, ideally, a single character for e.g. S M T W T F S + let customDayAbbrev = _getCalendarDayAbbreviation(iter.getDay()); + let label = new St.Label({ style_class: 'calendar-day-base calendar-day-heading', + text: customDayAbbrev }); + this.actor.add(label, { row: 1, col: offsetCols + (7 + iter.getDay() - this._weekStart) % 7, - x_fill: false, x_align: St.Align.END }); + x_fill: false, x_align: St.Align.MIDDLE }); iter.setTime(iter.getTime() + MSECS_IN_DAY); } @@ -178,33 +477,35 @@ Calendar.prototype = { switch (event.get_scroll_direction()) { case Clutter.ScrollDirection.UP: case Clutter.ScrollDirection.LEFT: - this._prevMonth(); + this._onPrevMonthButtonClicked(); break; case Clutter.ScrollDirection.DOWN: case Clutter.ScrollDirection.RIGHT: - this._nextMonth(); + this._onNextMonthButtonClicked(); break; } }, - _prevMonth: function() { - if (this.date.getMonth() == 0) { - this.date.setMonth(11); - this.date.setFullYear(this.date.getFullYear() - 1); + _onPrevMonthButtonClicked: function() { + let newDate = new Date(this._selectedDate); + if (newDate.getMonth() == 0) { + newDate.setMonth(11); + newDate.setFullYear(newDate.getFullYear() - 1); } else { - this.date.setMonth(this.date.getMonth() - 1); + newDate.setMonth(newDate.getMonth() - 1); } - this._update(); + this.setDate(newDate); }, - _nextMonth: function() { - if (this.date.getMonth() == 11) { - this.date.setMonth(0); - this.date.setFullYear(this.date.getFullYear() + 1); + _onNextMonthButtonClicked: function() { + let newDate = new Date(this._selectedDate); + if (newDate.getMonth() == 11) { + newDate.setMonth(0); + newDate.setFullYear(newDate.getFullYear() + 1); } else { - this.date.setMonth(this.date.getMonth() + 1); + newDate.setMonth(newDate.getMonth() + 1); } - this._update(); + this.setDate(newDate); }, _onSettingsChange: function() { @@ -214,7 +515,12 @@ Calendar.prototype = { }, _update: function() { - this._dateLabel.text = this.date.toLocaleFormat(this._headerFormat); + let now = new Date(); + + if (_sameYear(this._selectedDate, now)) + this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormatWithoutYear); + else + this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormat); // Remove everything but the topBox and the weekday labels let children = this.actor.get_children(); @@ -222,45 +528,201 @@ Calendar.prototype = { 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.setHours(12); - let daysToWeekStart = (7 + iter.getDay() - this._weekStart) % 7; - iter.setTime(iter.getTime() - daysToWeekStart * MSECS_IN_DAY); - - let now = new Date(); + let beginDate = new Date(this._selectedDate); + beginDate.setDate(1); + beginDate.setSeconds(0); + beginDate.setHours(12); + let daysToWeekStart = (7 + beginDate.getDay() - this._weekStart) % 7; + beginDate.setTime(beginDate.getTime() - daysToWeekStart * MSECS_IN_DAY); + let iter = new Date(beginDate); 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'; + let button = new St.Button({ label: iter.getDate().toString() }); + + let iterStr = iter.toUTCString(); + button.connect('clicked', Lang.bind(this, function() { + let newlySelectedDate = new Date(iterStr); + this.setDate(newlySelectedDate); + })); + + let hasEvents = this._eventSource.hasEvents(iter); + let styleClass = 'calendar-day-base calendar-day'; + if (_isWorkDay(iter)) + styleClass += ' calendar-work-day' else - label.style_class = 'calendar-day'; + styleClass += ' calendar-nonwork-day' + + // Hack used in lieu of border-collapse - see gnome-shell.css + if (row == 2) + styleClass = 'calendar-day-top ' + styleClass; + if (iter.getDay() == 0) + styleClass = 'calendar-day-left ' + styleClass; + + if (_sameDay(now, iter)) + styleClass += ' calendar-today'; + else if (iter.getMonth() != this._selectedDate.getMonth()) + styleClass += ' calendar-other-month-day'; + + if (_sameDay(this._selectedDate, iter)) + button.add_style_pseudo_class('active'); + + if (hasEvents) + styleClass += ' calendar-day-with-events' + + button.style_class = styleClass; let offsetCols = this._useWeekdate ? 1 : 0; - this.actor.add(label, - { row: row, col: offsetCols + (7 + iter.getDay() - this._weekStart) % 7, - x_fill: false, x_align: St.Align.END }); + this.actor.add(button, + { row: row, col: offsetCols + (7 + iter.getDay() - this._weekStart) % 7 }); if (this._useWeekdate && iter.getDay() == 4) { let label = new St.Label({ text: _getCalendarWeekForDate(iter).toString(), - style_class: 'calendar-day calendar-calendarweek'}); + style_class: 'calendar-day-base calendar-week-number'}); this.actor.add(label, - { row: row, col: 0, - x_fill: false, x_align: St.Align.MIDDLE }); + { row: row, col: 0, y_align: St.Align.MIDDLE }); } 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()) + if (iter.getMonth() > this._selectedDate.getMonth() || iter.getYear() > this._selectedDate.getYear()) break; row++; } } + // Signal to the event source that we are interested in events + // only from this date range + this._eventSource.requestRange(beginDate, iter); + } +}; + +Signals.addSignalMethods(Calendar.prototype); + +function EventsList(eventSource) { + this._init(eventSource); +} + +EventsList.prototype = { + _init: function(eventSource) { + this.actor = new St.BoxLayout({ vertical: true, style_class: 'events-header-vbox'}); + this._date = new Date(); + this._eventSource = eventSource; + this._eventSource.connect('changed', Lang.bind(this, this._update)); + this._desktopSettings = new Gio.Settings({ schema: 'org.gnome.desktop.interface' }); + this._desktopSettings.connect('changed', Lang.bind(this, this._update)); + this._update(); + }, + + _addEvent: function(dayNameBox, timeBox, eventTitleBox, includeDayName, day, time, desc) { + if (includeDayName) { + dayNameBox.add(new St.Label( { style_class: 'events-day-dayname', + text: day } ), + { x_fill: true } ); + } + timeBox.add(new St.Label( { style_class: 'events-day-time', + text: time} ), + { x_fill: true } ); + eventTitleBox.add(new St.Label( { style_class: 'events-day-task', + text: desc} )); + }, + + _addPeriod: function(header, begin, end, includeDayName, showNothingScheduled) { + let events = this._eventSource.getEvents(begin, end); + + let clockFormat = this._desktopSettings.get_string(CLOCK_FORMAT_KEY);; + + if (events.length == 0 && !showNothingScheduled) + return; + + let vbox = new St.BoxLayout( {vertical: true} ); + this.actor.add(vbox); + + vbox.add(new St.Label({ style_class: 'events-day-header', text: header })); + let box = new St.BoxLayout({style_class: 'events-header-hbox'}); + 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, {x_fill: false}); + box.add(timeBox, {x_fill: false}); + box.add(eventTitleBox, {expand: true}); + vbox.add(box); + + for (let n = 0; n < events.length; n++) { + let event = events[n]; + let dayString = _getEventDayAbbreviation(event.date.getDay()); + let timeString = _formatEventTime(event, clockFormat); + let summaryString = event.summary; + this._addEvent(dayNameBox, timeBox, eventTitleBox, includeDayName, dayString, timeString, summaryString); + } + + if (events.length == 0 && showNothingScheduled) { + let now = new Date(); + /* Translators: Text to show if there are no events */ + let nothingEvent = new CalendarEvent(now, _("Nothing Scheduled"), true); + let timeString = _formatEventTime(nothingEvent, clockFormat); + this._addEvent(dayNameBox, timeBox, eventTitleBox, false, "", timeString, nothingEvent.summary); + } + }, + + _showOtherDay: function(day) { + this.actor.destroy_children(); + + let dayBegin = _getBeginningOfDay(day); + let dayEnd = _getEndOfDay(day); + + let dayString; + let now = new Date(); + if (_sameYear(day, now)) + dayString = day.toLocaleFormat('%A, %B %d'); + else + dayString = day.toLocaleFormat('%A, %B %d, %Y'); + this._addPeriod(dayString, dayBegin, dayEnd, false, true); + }, + + _showToday: function() { + this.actor.destroy_children(); + + let now = new Date(); + let dayBegin = _getBeginningOfDay(now); + let dayEnd = _getEndOfDay(now); + this._addPeriod(_("Today"), dayBegin, dayEnd, false, true); + + let tomorrowBegin = new Date(dayBegin.getTime() + 86400 * 1000); + let tomorrowEnd = new Date(dayEnd.getTime() + 86400 * 1000); + this._addPeriod(_("Tomorrow"), tomorrowBegin, tomorrowEnd, false, true); + + if (dayEnd.getDay() <= 4) { + /* if now is Sunday through Thursday show "This week" and include events up until + * and including Saturday + */ + let thisWeekBegin = new Date(dayBegin.getTime() + 2 * 86400 * 1000); + let thisWeekEnd = new Date(dayEnd.getTime() + (6 - dayEnd.getDay()) * 86400 * 1000); + this._addPeriod(_("This week"), thisWeekBegin, thisWeekEnd, true, false); + } else { + /* otherwise it's a Friday or Saturday... show "Next week" and include events up + * until and including *next* Saturday + */ + let nextWeekBegin = new Date(dayBegin.getTime() + 2 * 86400 * 1000); + let nextWeekEnd = new Date(dayEnd.getTime() + (13 - dayEnd.getDay()) * 86400 * 1000); + this._addPeriod(_("Next week"), nextWeekBegin, nextWeekEnd, true, false); + } + }, + + // Sets the event list to show events from a specific date + setDate: function(date) { + if (!_sameDay(date, this._date)) { + this._date = date; + this._update(); + } + }, + + _update: function() { + let today = new Date(); + if (_sameDay (this._date, today)) { + this._showToday(); + } else { + this._showOtherDay(this._date); + } } }; diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js new file mode 100644 index 000000000..09f8dfe17 --- /dev/null +++ b/js/ui/dateMenu.js @@ -0,0 +1,212 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Cairo = imports.cairo; +const Clutter = imports.gi.Clutter; +const Shell = imports.gi.Shell; +const St = imports.gi.St; +const Gettext = imports.gettext.domain('gnome-shell'); +const _ = Gettext.gettext; + +const Util = imports.misc.util; +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const Calendar = imports.ui.calendar; + +// in org.gnome.desktop.interface +const CLOCK_FORMAT_KEY = 'clock-format'; + +// in org.gnome.shell.clock +const CLOCK_SHOW_DATE_KEY = 'show-date'; +const CLOCK_SHOW_SECONDS_KEY = 'show-seconds'; + +function _onVertSepRepaint (area) +{ + let cr = area.get_context(); + let themeNode = area.get_theme_node(); + let [width, height] = area.get_surface_size(); + let stippleColor = new Clutter.Color(); + let stippleWidth = themeNode.get_length('-stipple-width'); + let x = Math.floor(width/2) + 0.5; + themeNode.lookup_color('-stipple-color', false, stippleColor); + cr.moveTo(x, 0); + cr.lineTo(x, height); + Clutter.cairo_set_source_color(cr, stippleColor); + cr.setDash([1, 3], 1); // Hard-code for now + cr.setLineWidth(stippleWidth); + cr.stroke(); +}; + +function DateMenuButton() { + this._init(); +} + +DateMenuButton.prototype = { + __proto__: PanelMenu.Button.prototype, + + _init: function() { + let item; + let hbox; + let vbox; + + //this._eventSource = new Calendar.EmptyEventSource(); + //this._eventSource = new Calendar.FakeEventSource(); + this._eventSource = new Calendar.EvolutionEventSource(); + + PanelMenu.Button.prototype._init.call(this, St.Align.START); + + this._clock = new St.Label(); + this.actor.set_child(this._clock); + + hbox = new St.BoxLayout({name: 'calendarArea'}); + this.menu.addActor(hbox); + + // Fill up the first column + + vbox = new St.BoxLayout({vertical: true}); + hbox.add(vbox); + + // Date + this._date = new St.Label(); + this._date.style_class = 'datemenu-date-label'; + vbox.add(this._date); + + this._eventList = new Calendar.EventsList(this._eventSource); + + // Calendar + this._calendar = new Calendar.Calendar(this._eventSource); + this._calendar.connect('selected-date-changed', + Lang.bind(this, function(calendar, date) { + this._eventList.setDate(date); + })); + vbox.add(this._calendar.actor); + + item = new PopupMenu.PopupSeparatorMenuItem(); + item.setColumnWidths(1); + vbox.add(item.actor, {y_align: St.Align.END, expand: true, y_fill: false}); + item = new PopupMenu.PopupMenuItem(_("Date and Time Settings")); + item.connect('activate', Lang.bind(this, this._onPreferencesActivate)); + vbox.add(item.actor); + + // Add vertical separator + + item = new St.DrawingArea({ style_class: 'calendar-vertical-separator', + pseudo_class: 'highlighted' }); + item.connect('repaint', Lang.bind(this, _onVertSepRepaint)); + hbox.add(item); + + // Fill up the second column + + vbox = new St.BoxLayout({vertical: true}); + hbox.add(vbox); + + // Event list + vbox.add(this._eventList.actor); + + item = new PopupMenu.PopupMenuItem(_("Open Calendar")); + item.connect('activate', Lang.bind(this, this._onOpenCalendarActivate)); + vbox.add(item.actor, {y_align: St.Align.END, expand: true, y_fill: false}); + + // Whenever the menu is opened, select today + this.menu.connect('open-state-changed', Lang.bind(this, function(menu, isOpen) { + if (isOpen) { + let now = new Date(); + this._calendar.setDate(now); + // No need to update this._eventList as ::selected-date-changed + // signal will fire + } + })); + + // Done with hbox for calendar and event list + + // Track changes to clock settings + this._desktopSettings = new Gio.Settings({ schema: 'org.gnome.desktop.interface' }); + this._clockSettings = new Gio.Settings({ schema: 'org.gnome.shell.clock' }); + this._desktopSettings.connect('changed', Lang.bind(this, this._updateClockAndDate)); + this._clockSettings.connect('changed', Lang.bind(this, this._updateClockAndDate)); + + // Start the clock + this._updateClockAndDate(); + }, + + _updateClockAndDate: function() { + let format = this._desktopSettings.get_string(CLOCK_FORMAT_KEY); + let showDate = this._clockSettings.get_boolean(CLOCK_SHOW_DATE_KEY); + let showSeconds = this._clockSettings.get_boolean(CLOCK_SHOW_SECONDS_KEY); + + let clockFormat; + let dateFormat; + + switch (format) { + case '24h': + if (showDate) + /* Translators: This is the time format with date used + in 24-hour mode. */ + clockFormat = showSeconds ? _("%a %b %e, %R:%S") + : _("%a %b %e, %R"); + else + /* Translators: This is the time format without date used + in 24-hour mode. */ + clockFormat = showSeconds ? _("%a %R:%S") + : _("%a %R"); + break; + case '12h': + default: + if (showDate) + /* Translators: This is a time format with date used + for AM/PM. */ + clockFormat = showSeconds ? _("%a %b %e, %l:%M:%S %p") + : _("%a %b %e, %l:%M %p"); + else + /* Translators: This is a time format without date used + for AM/PM. */ + clockFormat = showSeconds ? _("%a %l:%M:%S %p") + : _("%a %l:%M %p"); + break; + } + + let displayDate = new Date(); + let msecRemaining; + if (showSeconds) { + msecRemaining = 1000 - displayDate.getMilliseconds(); + if (msecRemaining < 50) { + displayDate.setSeconds(displayDate.getSeconds() + 1); + msecRemaining += 1000; + } + } else { + msecRemaining = 60000 - (1000 * displayDate.getSeconds() + + displayDate.getMilliseconds()); + if (msecRemaining < 500) { + displayDate.setMinutes(displayDate.getMinutes() + 1); + msecRemaining += 60000; + } + } + + this._clock.set_text(displayDate.toLocaleFormat(clockFormat)); + + /* Translators: This is the date format to use when the calendar popup is + * shown - it is shown just below the time in the shell (e.g. "Tue 9:29 AM"). + */ + dateFormat = _("%A %B %e, %Y"); + this._date.set_text(displayDate.toLocaleFormat(dateFormat)); + + Mainloop.timeout_add(msecRemaining, Lang.bind(this, this._updateClockAndDate)); + return false; + }, + + _onPreferencesActivate: function() { + this.menu.close(); + Util.spawnDesktop('gnome-datetime-panel'); + }, + + _onOpenCalendarActivate: function() { + this.menu.close(); + // TODO: pass '-c calendar' (to force the calendar at startup) + // TODO: pass the selected day + Util.spawnDesktop('evolution'); + }, +}; diff --git a/js/ui/main.js b/js/ui/main.js index 572709c0e..1f650fed9 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -284,10 +284,8 @@ function _relayout() { // To avoid updating the position and size of the workspaces // in the overview, we just hide the overview. The positions - // will be updated when it is next shown. We do the same for - // the calendar popdown. + // will be updated when it is next shown. overview.hide(); - panel.hideCalendar(); } // metacity-clutter currently uses the same prefs as plain metacity, diff --git a/js/ui/panel.js b/js/ui/panel.js index 1b8863fb8..9b7c3b36a 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -17,6 +17,7 @@ const Overview = imports.ui.overview; const PopupMenu = imports.ui.popupMenu; const PanelMenu = imports.ui.panelMenu; const StatusMenu = imports.ui.statusMenu; +const DateMenu = imports.ui.dateMenu; const Main = imports.ui.main; const Tweener = imports.ui.tweener; @@ -492,121 +493,6 @@ AppMenuButton.prototype = { Signals.addSignalMethods(AppMenuButton.prototype); -function ClockButton() { - this._init(); -} - -ClockButton.prototype = { - _init: function() { - this.actor = new St.Bin({ style_class: 'panel-button', - reactive: true, - can_focus: true, - x_fill: true, - y_fill: false, - track_hover: true }); - this.actor._delegate = this; - this.actor.connect('button-press-event', - Lang.bind(this, this._toggleCalendar)); - - this._clock = new St.Label(); - this.actor.set_child(this._clock); - - this._calendarPopup = null; - - this._desktopSettings = new Gio.Settings({ schema: 'org.gnome.desktop.interface' }); - this._clockSettings = new Gio.Settings({ schema: 'org.gnome.shell.clock' }); - - this._desktopSettings.connect('changed', Lang.bind(this, this._updateClock)); - this._clockSettings.connect('changed', Lang.bind(this, this._updateClock)); - - // Start the clock - this._updateClock(); - }, - - closeCalendar: function() { - if (!this._calendarPopup || !this._calendarPopup.isOpen) - return; - - this._calendarPopup.hide(); - - this.actor.remove_style_pseudo_class('pressed'); - }, - - openCalendar: function() { - this._calendarPopup.show(); - - this.actor.add_style_pseudo_class('pressed'); - }, - - _toggleCalendar: function() { - if (this._calendarPopup == null) { - this._calendarPopup = new CalendarPopup(); - this._calendarPopup.actor.hide(); - } - - if (!this._calendarPopup.isOpen) - this.openCalendar(); - else - this.closeCalendar(); - }, - - _updateClock: function() { - let format = this._desktopSettings.get_string(CLOCK_FORMAT_KEY); - let showDate = this._clockSettings.get_boolean(CLOCK_SHOW_DATE_KEY); - let showSeconds = this._clockSettings.get_boolean(CLOCK_SHOW_SECONDS_KEY); - - let clockFormat; - switch (format) { - case '24h': - if (showDate) - /* Translators: This is the time format with date used - in 24-hour mode. */ - clockFormat = showSeconds ? _("%a %b %e, %R:%S") - : _("%a %b %e, %R"); - else - /* Translators: This is the time format without date used - in 24-hour mode. */ - clockFormat = showSeconds ? _("%a %R:%S") - : _("%a %R"); - break; - case '12h': - default: - if (showDate) - /* Translators: This is a time format with date used - for AM/PM. */ - clockFormat = showSeconds ? _("%a %b %e, %l:%M:%S %p") - : _("%a %b %e, %l:%M %p"); - else - /* Translators: This is a time format without date used - for AM/PM. */ - clockFormat = showSeconds ? _("%a %l:%M:%S %p") - : _("%a %l:%M %p"); - break; - } - - let displayDate = new Date(); - let msecRemaining; - if (showSeconds) { - msecRemaining = 1000 - displayDate.getMilliseconds(); - if (msecRemaining < 50) { - displayDate.setSeconds(displayDate.getSeconds() + 1); - msecRemaining += 1000; - } - } else { - msecRemaining = 60000 - (1000 * displayDate.getSeconds() + - displayDate.getMilliseconds()); - if (msecRemaining < 500) { - displayDate.setMinutes(displayDate.getMinutes() + 1); - msecRemaining += 60000; - } - } - - this._clock.set_text(displayDate.toLocaleFormat(clockFormat)); - Mainloop.timeout_add(msecRemaining, Lang.bind(this, this._updateClock)); - return false; - } -}; - function Panel() { this._init(); } @@ -803,9 +689,9 @@ Panel.prototype = { this._menus.addMenu(appMenuButton.menu); /* center */ - - this._clockButton = new ClockButton(); - this._centerBox.add(this._clockButton.actor, { y_fill: true }); + this._dateMenu = new DateMenu.DateMenuButton(); + this._centerBox.add(this._dateMenu.actor, { y_fill: true }); + this._menus.addMenu(this._dateMenu.menu); /* right */ @@ -884,10 +770,6 @@ Panel.prototype = { this._rightBox.add(this._statusmenu.actor); }, - hideCalendar: function() { - this._clockButton.closeCalendar(); - }, - startupAnimation: function() { this.actor.y = -this.actor.height; Tweener.addTween(this.actor, diff --git a/src/Makefile-calendar-client.am b/src/Makefile-calendar-client.am new file mode 100644 index 000000000..cac046f57 --- /dev/null +++ b/src/Makefile-calendar-client.am @@ -0,0 +1,22 @@ + +noinst_LTLIBRARIES += libcalendar-client.la + +libcalendar_client_la_SOURCES = \ + calendar-client/calendar-client.h calendar-client/calendar-client.c \ + calendar-client/calendar-debug.h \ + calendar-client/calendar-sources.c calendar-client/calendar-sources.h \ + $(NULL) + +libcalendar_client_la_CFLAGS = \ + -I$(top_srcdir)/src \ + -DPREFIX=\""$(prefix)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + -DDATADIR=\""$(datadir)"\" \ + -DG_DISABLE_DEPRECATED \ + -DG_LOG_DOMAIN=\"CalendarClient\" \ + $(LIBECAL_CFLAGS) \ + $(NULL) + +libcalendar_client_la_LIBADD = $(LIBECAL_LIBS) + +EXTRA_DIST += calendar-client/README diff --git a/src/Makefile.am b/src/Makefile.am index 5bfa8c764..a7a38d8cc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -27,6 +27,7 @@ include Makefile-gdmuser.am include Makefile-st.am include Makefile-tray.am include Makefile-gvc.am +include Makefile-calendar-client.am gnome_shell_cflags = \ $(MUTTER_PLUGIN_CFLAGS) \ @@ -89,6 +90,8 @@ libgnome_shell_la_SOURCES = \ shell-doc-system.c \ shell-drawing.c \ shell-embedded-window.c \ + shell-evolution-event-source.h \ + shell-evolution-event-source.c \ shell-generic-container.c \ shell-gtk-embed.c \ shell-global.c \ @@ -212,7 +215,9 @@ libgnome_shell_la_LIBADD = \ libst-1.0.la \ libgdmuser-1.0.la \ libtray.la \ - libgvc.la + libgvc.la \ + libcalendar-client.la \ + $(NULL) libgnome_shell_la_CPPFLAGS = $(gnome_shell_cflags) diff --git a/src/calendar-client/README b/src/calendar-client/README new file mode 100644 index 000000000..ad9b5e3d0 --- /dev/null +++ b/src/calendar-client/README @@ -0,0 +1 @@ +Please keep in sync with gnome-panel. diff --git a/src/calendar-client/calendar-client.c b/src/calendar-client/calendar-client.c new file mode 100644 index 000000000..bbdb47327 --- /dev/null +++ b/src/calendar-client/calendar-client.c @@ -0,0 +1,2169 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#include + +#include "calendar-client.h" + +#include +#include +#define HANDLE_LIBICAL_MEMORY +#include +#include +#include + +#include "calendar-sources.h" + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +#define CALENDAR_CONFIG_PREFIX "/apps/evolution/calendar" +#define CALENDAR_CONFIG_TIMEZONE CALENDAR_CONFIG_PREFIX "/display/timezone" + +#ifndef _ +#define _(x) gettext(x) +#endif + +#ifndef N_ +#define N_(x) x +#endif + +#define CALENDAR_CLIENT_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CALENDAR_TYPE_CLIENT, CalendarClientPrivate)) + +typedef struct _CalendarClientQuery CalendarClientQuery; +typedef struct _CalendarClientSource CalendarClientSource; + +struct _CalendarClientQuery +{ + ECalView *view; + GHashTable *events; +}; + +struct _CalendarClientSource +{ + CalendarClient *client; + ECal *source; + + CalendarClientQuery completed_query; + CalendarClientQuery in_progress_query; + + guint changed_signal_id; + + guint query_completed : 1; + guint query_in_progress : 1; +}; + +struct _CalendarClientPrivate +{ + CalendarSources *calendar_sources; + + GSList *appointment_sources; + GSList *task_sources; + + icaltimezone *zone; + + guint zone_listener; + GConfClient *gconf_client; + + guint day; + guint month; + guint year; +}; + +static void calendar_client_class_init (CalendarClientClass *klass); +static void calendar_client_init (CalendarClient *client); +static void calendar_client_finalize (GObject *object); +static void calendar_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void calendar_client_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +static GSList *calendar_client_update_sources_list (CalendarClient *client, + GSList *sources, + GSList *esources, + guint changed_signal_id); +static void calendar_client_appointment_sources_changed (CalendarClient *client); +static void calendar_client_task_sources_changed (CalendarClient *client); + +static void calendar_client_stop_query (CalendarClient *client, + CalendarClientSource *source, + CalendarClientQuery *query); +static void calendar_client_start_query (CalendarClient *client, + CalendarClientSource *source, + const char *query); + +static void calendar_client_source_finalize (CalendarClientSource *source); +static void calendar_client_query_finalize (CalendarClientQuery *query); + +static void +calendar_client_update_appointments (CalendarClient *client); +static void +calendar_client_update_tasks (CalendarClient *client); + +enum +{ + PROP_O, + PROP_DAY, + PROP_MONTH, + PROP_YEAR +}; + +enum +{ + APPOINTMENTS_CHANGED, + TASKS_CHANGED, + LAST_SIGNAL +}; + +static GObjectClass *parent_class = NULL; +static guint signals [LAST_SIGNAL] = { 0, }; + +GType +calendar_client_get_type (void) +{ + static GType client_type = 0; + + if (!client_type) + { + static const GTypeInfo client_info = + { + sizeof (CalendarClientClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) calendar_client_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (CalendarClient), + 0, /* n_preallocs */ + (GInstanceInitFunc) calendar_client_init, + }; + + client_type = g_type_register_static (G_TYPE_OBJECT, + "CalendarClient", + &client_info, 0); + } + + return client_type; +} + +static void +calendar_client_class_init (CalendarClientClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + parent_class = g_type_class_peek_parent (klass); + + gobject_class->finalize = calendar_client_finalize; + gobject_class->set_property = calendar_client_set_property; + gobject_class->get_property = calendar_client_get_property; + + g_type_class_add_private (klass, sizeof (CalendarClientPrivate)); + + g_object_class_install_property (gobject_class, + PROP_DAY, + g_param_spec_uint ("day", + "Day", + "The currently monitored day between 1 and 31 (0 denotes unset)", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, + PROP_MONTH, + g_param_spec_uint ("month", + "Month", + "The currently monitored month between 0 and 11", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE)); + + g_object_class_install_property (gobject_class, + PROP_YEAR, + g_param_spec_uint ("year", + "Year", + "The currently monitored year", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE)); + + signals [APPOINTMENTS_CHANGED] = + g_signal_new ("appointments-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarClientClass, tasks_changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals [TASKS_CHANGED] = + g_signal_new ("tasks-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarClientClass, tasks_changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +/* Timezone code adapted from evolution/calendar/gui/calendar-config.c */ +/* The current timezone, e.g. "Europe/London". It may be NULL, in which case + you should assume UTC. */ +static gchar * +calendar_client_config_get_timezone (GConfClient *gconf_client) +{ + char *location; + + location = gconf_client_get_string (gconf_client, + CALENDAR_CONFIG_TIMEZONE, + NULL); + + return location; +} + +static icaltimezone * +calendar_client_config_get_icaltimezone (GConfClient *gconf_client) +{ + char *location; + icaltimezone *zone = NULL; + + location = calendar_client_config_get_timezone (gconf_client); + if (!location) + return icaltimezone_get_utc_timezone (); + + zone = icaltimezone_get_builtin_timezone (location); + g_free (location); + + return zone; +} + +static void +calendar_client_set_timezone (CalendarClient *client) +{ + GSList *l; + GSList *esources; + + client->priv->zone = calendar_client_config_get_icaltimezone (client->priv->gconf_client); + + esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources); + for (l = esources; l; l = l->next) { + ECal *source = l->data; + + e_cal_set_default_timezone (source, client->priv->zone, NULL); + } +} + +static void +calendar_client_timezone_changed_cb (GConfClient *gconf_client, + guint id, + GConfEntry *entry, + CalendarClient *client) +{ + calendar_client_set_timezone (client); +} + +static void +cal_opened_cb (ECal *ecal, + ECalendarStatus status, + CalendarClientSource *cl_source) +{ + ECalSourceType s_type; + CalendarClient *client = cl_source->client; + + s_type = e_cal_get_source_type (ecal); + + if (status == E_CALENDAR_STATUS_BUSY && + e_cal_get_load_state (ecal) == E_CAL_LOAD_NOT_LOADED) + { + e_cal_open_async (ecal, FALSE); + return; + } + + g_signal_handlers_disconnect_by_func (ecal, cal_opened_cb, cl_source); + + if (status != E_CALENDAR_STATUS_OK) + { + if (s_type == E_CAL_SOURCE_TYPE_EVENT) + client->priv->appointment_sources = g_slist_remove (client->priv->appointment_sources, + cl_source); + else + client->priv->task_sources = g_slist_remove (client->priv->task_sources, + cl_source); + + calendar_client_source_finalize (cl_source); + g_free (cl_source); + + return; + } + + if (s_type == E_CAL_SOURCE_TYPE_EVENT) + calendar_client_update_appointments (client); + else + calendar_client_update_tasks (client); +} + +static void +load_calendars (CalendarClient *client, + CalendarEventType type) +{ + GSList *l, *clients; + + switch (type) + { + case CALENDAR_EVENT_APPOINTMENT: + clients = client->priv->appointment_sources; + break; + case CALENDAR_EVENT_TASK: + clients = client->priv->task_sources; + break; + default: + g_assert_not_reached (); + } + + for (l = clients; l != NULL; l = l->next) + { + ECal *ecal; + CalendarClientSource *cl_source = l->data; + + ecal = cl_source->source; + + if (e_cal_get_load_state (ecal) == E_CAL_LOAD_LOADED) + continue; + + g_signal_connect (G_OBJECT (ecal), "cal_opened", + G_CALLBACK (cal_opened_cb), cl_source); + e_cal_open_async (ecal, TRUE); + } +} + +static void +calendar_client_init (CalendarClient *client) +{ + GSList *esources; + + client->priv = CALENDAR_CLIENT_GET_PRIVATE (client); + + client->priv->calendar_sources = calendar_sources_get (); + client->priv->gconf_client = gconf_client_get_default (); + + esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources); + client->priv->appointment_sources = + calendar_client_update_sources_list (client, NULL, esources, signals [APPOINTMENTS_CHANGED]); + + esources = calendar_sources_get_task_sources (client->priv->calendar_sources); + client->priv->task_sources = + calendar_client_update_sources_list (client, NULL, esources, signals [TASKS_CHANGED]); + + /* set the timezone before loading the clients */ + calendar_client_set_timezone (client); + load_calendars (client, CALENDAR_EVENT_APPOINTMENT); + load_calendars (client, CALENDAR_EVENT_TASK); + + g_signal_connect_swapped (client->priv->calendar_sources, + "appointment-sources-changed", + G_CALLBACK (calendar_client_appointment_sources_changed), + client); + g_signal_connect_swapped (client->priv->calendar_sources, + "task-sources-changed", + G_CALLBACK (calendar_client_task_sources_changed), + client); + + gconf_client_add_dir (client->priv->gconf_client, + CALENDAR_CONFIG_PREFIX, + GCONF_CLIENT_PRELOAD_NONE, + NULL); + + client->priv->zone_listener = gconf_client_notify_add (client->priv->gconf_client, + CALENDAR_CONFIG_TIMEZONE, + (GConfClientNotifyFunc) calendar_client_timezone_changed_cb, + client, NULL, NULL); + + client->priv->day = -1; + client->priv->month = -1; + client->priv->year = -1; +} + +static void +calendar_client_finalize (GObject *object) +{ + CalendarClient *client = CALENDAR_CLIENT (object); + GSList *l; + + if (client->priv->zone_listener) + { + gconf_client_notify_remove (client->priv->gconf_client, + client->priv->zone_listener); + client->priv->zone_listener = 0; + } + + gconf_client_remove_dir (client->priv->gconf_client, + CALENDAR_CONFIG_PREFIX, + NULL); + + if (client->priv->gconf_client) + g_object_unref (client->priv->gconf_client); + client->priv->gconf_client = NULL; + + for (l = client->priv->appointment_sources; l; l = l->next) + { + calendar_client_source_finalize (l->data); + g_free (l->data); + } + g_slist_free (client->priv->appointment_sources); + client->priv->appointment_sources = NULL; + + for (l = client->priv->task_sources; l; l = l->next) + { + calendar_client_source_finalize (l->data); + g_free (l->data); + } + g_slist_free (client->priv->task_sources); + client->priv->task_sources = NULL; + + if (client->priv->calendar_sources) + g_object_unref (client->priv->calendar_sources); + client->priv->calendar_sources = NULL; + + if (G_OBJECT_CLASS (parent_class)->finalize) + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +calendar_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CalendarClient *client = CALENDAR_CLIENT (object); + + switch (prop_id) + { + case PROP_DAY: + calendar_client_select_day (client, g_value_get_uint (value)); + break; + case PROP_MONTH: + calendar_client_select_month (client, + g_value_get_uint (value), + client->priv->year); + break; + case PROP_YEAR: + calendar_client_select_month (client, + client->priv->month, + g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +calendar_client_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CalendarClient *client = CALENDAR_CLIENT (object); + + switch (prop_id) + { + case PROP_DAY: + g_value_set_uint (value, client->priv->day); + break; + case PROP_MONTH: + g_value_set_uint (value, client->priv->month); + break; + case PROP_YEAR: + g_value_set_uint (value, client->priv->year); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +CalendarClient * +calendar_client_new (void) +{ + return g_object_new (CALENDAR_TYPE_CLIENT, NULL); +} + +/* @day and @month can happily be out of range as + * mktime() will normalize them correctly. From mktime(3): + * + * "If structure members are outside their legal interval, + * they will be normalized (so that, e.g., 40 October is + * changed into 9 November)." + * + * "What?", you say, "Something useful in libc?" + */ +static inline time_t +make_time_for_day_begin (int day, + int month, + int year) +{ + struct tm localtime_tm = { 0, }; + + localtime_tm.tm_mday = day; + localtime_tm.tm_mon = month; + localtime_tm.tm_year = year - 1900; + localtime_tm.tm_isdst = -1; + + return mktime (&localtime_tm); +} + +static inline char * +make_isodate_for_day_begin (int day, + int month, + int year) +{ + time_t utctime; + + utctime = make_time_for_day_begin (day, month, year); + + return utctime != -1 ? isodate_from_time_t (utctime) : NULL; +} + +static time_t +get_time_from_property (icalcomponent *ical, + icalproperty_kind prop_kind, + struct icaltimetype (* get_prop_func) (const icalproperty *prop), + icaltimezone *default_zone) +{ + icalproperty *prop; + struct icaltimetype ical_time; + icalparameter *param; + icaltimezone *timezone = NULL; + + prop = icalcomponent_get_first_property (ical, prop_kind); + if (!prop) + return 0; + + ical_time = get_prop_func (prop); + + param = icalproperty_get_first_parameter (prop, ICAL_TZID_PARAMETER); + if (param) + timezone = icaltimezone_get_builtin_timezone_from_tzid (icalparameter_get_tzid (param)); + else if (icaltime_is_utc (ical_time)) + timezone = icaltimezone_get_utc_timezone (); + else + timezone = default_zone; + + return icaltime_as_timet_with_zone (ical_time, timezone); +} + +static char * +get_ical_uid (icalcomponent *ical) +{ + return g_strdup (icalcomponent_get_uid (ical)); +} + +static char * +get_ical_rid (icalcomponent *ical) +{ + icalproperty *prop; + struct icaltimetype ical_time; + + prop = icalcomponent_get_first_property (ical, ICAL_RECURRENCEID_PROPERTY); + if (!prop) + return NULL; + + ical_time = icalproperty_get_recurrenceid (prop); + + return icaltime_is_valid_time (ical_time) && !icaltime_is_null_time (ical_time) ? + g_strdup (icaltime_as_ical_string (ical_time)) : NULL; +} + +static char * +get_ical_summary (icalcomponent *ical) +{ + icalproperty *prop; + + prop = icalcomponent_get_first_property (ical, ICAL_SUMMARY_PROPERTY); + if (!prop) + return NULL; + + return g_strdup (icalproperty_get_summary (prop)); +} + +static char * +get_ical_description (icalcomponent *ical) +{ + icalproperty *prop; + + prop = icalcomponent_get_first_property (ical, ICAL_DESCRIPTION_PROPERTY); + if (!prop) + return NULL; + + return g_strdup (icalproperty_get_description (prop)); +} + +static inline time_t +get_ical_start_time (icalcomponent *ical, + icaltimezone *default_zone) +{ + return get_time_from_property (ical, + ICAL_DTSTART_PROPERTY, + icalproperty_get_dtstart, + default_zone); +} + +static inline time_t +get_ical_end_time (icalcomponent *ical, + icaltimezone *default_zone) +{ + return get_time_from_property (ical, + ICAL_DTEND_PROPERTY, + icalproperty_get_dtend, + default_zone); +} + +static gboolean +get_ical_is_all_day (icalcomponent *ical, + time_t start_time, + icaltimezone *default_zone) +{ + icalproperty *prop; + struct tm *start_tm; + time_t end_time; + struct icaldurationtype duration; + struct icaltimetype start_icaltime; + + start_icaltime = icalcomponent_get_dtstart (ical); + if (start_icaltime.is_date) + return TRUE; + + start_tm = gmtime (&start_time); + if (start_tm->tm_sec != 0 || + start_tm->tm_min != 0 || + start_tm->tm_hour != 0) + return FALSE; + + if ((end_time = get_ical_end_time (ical, default_zone))) + return (end_time - start_time) % 86400 == 0; + + prop = icalcomponent_get_first_property (ical, ICAL_DURATION_PROPERTY); + if (!prop) + return FALSE; + + duration = icalproperty_get_duration (prop); + + return icaldurationtype_as_int (duration) % 86400 == 0; +} + +static inline time_t +get_ical_due_time (icalcomponent *ical, + icaltimezone *default_zone) +{ + return get_time_from_property (ical, + ICAL_DUE_PROPERTY, + icalproperty_get_due, + default_zone); +} + +static guint +get_ical_percent_complete (icalcomponent *ical) +{ + icalproperty *prop; + icalproperty_status status; + int percent_complete; + + status = icalcomponent_get_status (ical); + if (status == ICAL_STATUS_COMPLETED) + return 100; + + prop = icalcomponent_get_first_property (ical, ICAL_COMPLETED_PROPERTY); + if (prop) + return 100; + + prop = icalcomponent_get_first_property (ical, ICAL_PERCENTCOMPLETE_PROPERTY); + if (!prop) + return 0; + + percent_complete = icalproperty_get_percentcomplete (prop); + + return CLAMP (percent_complete, 0, 100); +} + +static inline time_t +get_ical_completed_time (icalcomponent *ical, + icaltimezone *default_zone) +{ + return get_time_from_property (ical, + ICAL_COMPLETED_PROPERTY, + icalproperty_get_completed, + default_zone); +} + +static int +get_ical_priority (icalcomponent *ical) +{ + icalproperty *prop; + + prop = icalcomponent_get_first_property (ical, ICAL_PRIORITY_PROPERTY); + if (!prop) + return -1; + + return icalproperty_get_priority (prop); +} + +static char * +get_source_color (ECal *esource) +{ + ESource *source; + + g_return_val_if_fail (E_IS_CAL (esource), NULL); + + source = e_cal_get_source (esource); + + return g_strdup (e_source_peek_color_spec (source)); +} + +static gchar * +get_source_uri (ECal *esource) +{ + ESource *source; + gchar *string; + gchar **list; + + g_return_val_if_fail (E_IS_CAL (esource), NULL); + + source = e_cal_get_source (esource); + string = g_strdup (e_source_get_uri (source)); + if (string) { + list = g_strsplit (string, ":", 2); + g_free (string); + + if (list[0]) { + string = g_strdup (list[0]); + g_strfreev (list); + return string; + } + g_strfreev (list); + } + return NULL; +} + +static inline int +null_safe_strcmp (const char *a, + const char *b) +{ + return (!a && !b) ? 0 : (a && !b) || (!a && b) ? 1 : strcmp (a, b); +} + +static inline gboolean +calendar_appointment_equal (CalendarAppointment *a, + CalendarAppointment *b) +{ + GSList *la, *lb; + + if (g_slist_length (a->occurrences) != g_slist_length (b->occurrences)) + return FALSE; + + for (la = a->occurrences, lb = b->occurrences; la && lb; la = la->next, lb = lb->next) + { + CalendarOccurrence *oa = la->data; + CalendarOccurrence *ob = lb->data; + + if (oa->start_time != ob->start_time || + oa->end_time != ob->end_time) + return FALSE; + } + + return + null_safe_strcmp (a->uid, b->uid) == 0 && + null_safe_strcmp (a->uri, b->uri) == 0 && + null_safe_strcmp (a->summary, b->summary) == 0 && + null_safe_strcmp (a->description, b->description) == 0 && + null_safe_strcmp (a->color_string, b->color_string) == 0 && + a->start_time == b->start_time && + a->end_time == b->end_time && + a->is_all_day == b->is_all_day; +} + +static void +calendar_appointment_copy (CalendarAppointment *appointment, + CalendarAppointment *appointment_copy) +{ + GSList *l; + + g_assert (appointment != NULL); + g_assert (appointment_copy != NULL); + + appointment_copy->occurrences = g_slist_copy (appointment->occurrences); + for (l = appointment_copy->occurrences; l; l = l->next) + { + CalendarOccurrence *occurrence = l->data; + CalendarOccurrence *occurrence_copy; + + occurrence_copy = g_new0 (CalendarOccurrence, 1); + occurrence_copy->start_time = occurrence->start_time; + occurrence_copy->end_time = occurrence->end_time; + + l->data = occurrence_copy; + } + + appointment_copy->uid = g_strdup (appointment->uid); + appointment_copy->uri = g_strdup (appointment->uri); + appointment_copy->summary = g_strdup (appointment->summary); + appointment_copy->description = g_strdup (appointment->description); + appointment_copy->color_string = g_strdup (appointment->color_string); + appointment_copy->start_time = appointment->start_time; + appointment_copy->end_time = appointment->end_time; + appointment_copy->is_all_day = appointment->is_all_day; +} + +static void +calendar_appointment_finalize (CalendarAppointment *appointment) +{ + GSList *l; + + for (l = appointment->occurrences; l; l = l->next) + g_free (l->data); + g_slist_free (appointment->occurrences); + appointment->occurrences = NULL; + + g_free (appointment->uid); + appointment->uid = NULL; + + g_free (appointment->rid); + appointment->rid = NULL; + + g_free (appointment->uri); + appointment->uri = NULL; + + g_free (appointment->summary); + appointment->summary = NULL; + + g_free (appointment->description); + appointment->description = NULL; + + g_free (appointment->color_string); + appointment->color_string = NULL; + + appointment->start_time = 0; + appointment->is_all_day = FALSE; +} + +static void +calendar_appointment_init (CalendarAppointment *appointment, + icalcomponent *ical, + CalendarClientSource *source, + icaltimezone *default_zone) +{ + appointment->uid = get_ical_uid (ical); + appointment->rid = get_ical_rid (ical); + appointment->uri = get_source_uri (source->source); + appointment->summary = get_ical_summary (ical); + appointment->description = get_ical_description (ical); + appointment->color_string = get_source_color (source->source); + appointment->start_time = get_ical_start_time (ical, default_zone); + appointment->end_time = get_ical_end_time (ical, default_zone); + appointment->is_all_day = get_ical_is_all_day (ical, + appointment->start_time, + default_zone); +} + +static icaltimezone * +resolve_timezone_id (const char *tzid, + ECal *source) +{ + icaltimezone *retval; + + retval = icaltimezone_get_builtin_timezone_from_tzid (tzid); + if (!retval) + { + e_cal_get_timezone (source, tzid, &retval, NULL); + } + + return retval; +} + +static gboolean +calendar_appointment_collect_occurrence (ECalComponent *component, + time_t occurrence_start, + time_t occurrence_end, + gpointer data) +{ + CalendarOccurrence *occurrence; + GSList **collect_loc = data; + + occurrence = g_new0 (CalendarOccurrence, 1); + occurrence->start_time = occurrence_start; + occurrence->end_time = occurrence_end; + + *collect_loc = g_slist_prepend (*collect_loc, occurrence); + + return TRUE; +} + +static void +calendar_appointment_generate_ocurrences (CalendarAppointment *appointment, + icalcomponent *ical, + ECal *source, + time_t start, + time_t end, + icaltimezone *default_zone) +{ + ECalComponent *ecal; + + g_assert (appointment->occurrences == NULL); + + ecal = e_cal_component_new (); + e_cal_component_set_icalcomponent (ecal, + icalcomponent_new_clone (ical)); + + e_cal_recur_generate_instances (ecal, + start, + end, + calendar_appointment_collect_occurrence, + &appointment->occurrences, + (ECalRecurResolveTimezoneFn) resolve_timezone_id, + source, + default_zone); + + g_object_unref (ecal); + + appointment->occurrences = g_slist_reverse (appointment->occurrences); +} + +static inline gboolean +calendar_task_equal (CalendarTask *a, + CalendarTask *b) +{ + return + null_safe_strcmp (a->uid, b->uid) == 0 && + null_safe_strcmp (a->summary, b->summary) == 0 && + null_safe_strcmp (a->description, b->description) == 0 && + null_safe_strcmp (a->color_string, b->color_string) == 0 && + a->start_time == b->start_time && + a->due_time == b->due_time && + a->percent_complete == b->percent_complete && + a->completed_time == b->completed_time && + a->priority == b->priority; +} + +static void +calendar_task_copy (CalendarTask *task, + CalendarTask *task_copy) +{ + g_assert (task != NULL); + g_assert (task_copy != NULL); + + task_copy->uid = g_strdup (task->uid); + task_copy->summary = g_strdup (task->summary); + task_copy->description = g_strdup (task->description); + task_copy->color_string = g_strdup (task->color_string); + task_copy->start_time = task->start_time; + task_copy->due_time = task->due_time; + task_copy->percent_complete = task->percent_complete; + task_copy->completed_time = task->completed_time; + task_copy->priority = task->priority; +} + +static void +calendar_task_finalize (CalendarTask *task) +{ + g_free (task->uid); + task->uid = NULL; + + g_free (task->summary); + task->summary = NULL; + + g_free (task->description); + task->description = NULL; + + g_free (task->color_string); + task->color_string = NULL; + + task->percent_complete = 0; +} + +static void +calendar_task_init (CalendarTask *task, + icalcomponent *ical, + CalendarClientSource *source, + icaltimezone *default_zone) +{ + task->uid = get_ical_uid (ical); + task->summary = get_ical_summary (ical); + task->description = get_ical_description (ical); + task->color_string = get_source_color (source->source); + task->start_time = get_ical_start_time (ical, default_zone); + task->due_time = get_ical_due_time (ical, default_zone); + task->percent_complete = get_ical_percent_complete (ical); + task->completed_time = get_ical_completed_time (ical, default_zone); + task->priority = get_ical_priority (ical); +} + +void +calendar_event_free (CalendarEvent *event) +{ + switch (event->type) + { + case CALENDAR_EVENT_APPOINTMENT: + calendar_appointment_finalize (CALENDAR_APPOINTMENT (event)); + break; + case CALENDAR_EVENT_TASK: + calendar_task_finalize (CALENDAR_TASK (event)); + break; + default: + g_assert_not_reached (); + break; + } + + g_free (event); +} + +static CalendarEvent * +calendar_event_new (icalcomponent *ical, + CalendarClientSource *source, + icaltimezone *default_zone) +{ + CalendarEvent *event; + + event = g_new0 (CalendarEvent, 1); + + switch (icalcomponent_isa (ical)) + { + case ICAL_VEVENT_COMPONENT: + event->type = CALENDAR_EVENT_APPOINTMENT; + calendar_appointment_init (CALENDAR_APPOINTMENT (event), + ical, + source, + default_zone); + break; + case ICAL_VTODO_COMPONENT: + event->type = CALENDAR_EVENT_TASK; + calendar_task_init (CALENDAR_TASK (event), + ical, + source, + default_zone); + break; + default: + g_warning ("Unknown calendar component type: %d\n", + icalcomponent_isa (ical)); + g_free (event); + return NULL; + } + + return event; +} + +static CalendarEvent * +calendar_event_copy (CalendarEvent *event) +{ + CalendarEvent *retval; + + if (!event) + return NULL; + + retval = g_new0 (CalendarEvent, 1); + + retval->type = event->type; + + switch (event->type) + { + case CALENDAR_EVENT_APPOINTMENT: + calendar_appointment_copy (CALENDAR_APPOINTMENT (event), + CALENDAR_APPOINTMENT (retval)); + break; + case CALENDAR_EVENT_TASK: + calendar_task_copy (CALENDAR_TASK (event), + CALENDAR_TASK (retval)); + break; + default: + g_assert_not_reached (); + break; + } + + return retval; +} + +static char * +calendar_event_get_uid (CalendarEvent *event) +{ + switch (event->type) + { + case CALENDAR_EVENT_APPOINTMENT: + return g_strdup_printf ("%s%s", CALENDAR_APPOINTMENT (event)->uid, CALENDAR_APPOINTMENT (event)->rid ? CALENDAR_APPOINTMENT (event)->rid : ""); + break; + case CALENDAR_EVENT_TASK: + return g_strdup (CALENDAR_TASK (event)->uid); + break; + default: + g_assert_not_reached (); + break; + } + + return NULL; +} + +static gboolean +calendar_event_equal (CalendarEvent *a, + CalendarEvent *b) +{ + if (!a && !b) + return TRUE; + + if ((a && !b) || (!a && b)) + return FALSE; + + if (a->type != b->type) + return FALSE; + + switch (a->type) + { + case CALENDAR_EVENT_APPOINTMENT: + return calendar_appointment_equal (CALENDAR_APPOINTMENT (a), + CALENDAR_APPOINTMENT (b)); + case CALENDAR_EVENT_TASK: + return calendar_task_equal (CALENDAR_TASK (a), + CALENDAR_TASK (b)); + default: + break; + } + + g_assert_not_reached (); + + return FALSE; +} + +static void +calendar_event_generate_ocurrences (CalendarEvent *event, + icalcomponent *ical, + ECal *source, + time_t start, + time_t end, + icaltimezone *default_zone) +{ + if (event->type != CALENDAR_EVENT_APPOINTMENT) + return; + + calendar_appointment_generate_ocurrences (CALENDAR_APPOINTMENT (event), + ical, + source, + start, + end, + default_zone); +} + +static inline void +calendar_event_debug_dump (CalendarEvent *event) +{ +#ifdef CALENDAR_ENABLE_DEBUG + switch (event->type) + { + case CALENDAR_EVENT_APPOINTMENT: + { + char *start_str; + char *end_str; + GSList *l; + + start_str = CALENDAR_APPOINTMENT (event)->start_time ? + isodate_from_time_t (CALENDAR_APPOINTMENT (event)->start_time) : + g_strdup ("(undefined)"); + end_str = CALENDAR_APPOINTMENT (event)->end_time ? + isodate_from_time_t (CALENDAR_APPOINTMENT (event)->end_time) : + g_strdup ("(undefined)"); + + dprintf ("Appointment: uid '%s', summary '%s', description '%s', " + "start_time '%s', end_time '%s', is_all_day %s\n", + CALENDAR_APPOINTMENT (event)->uid, + CALENDAR_APPOINTMENT (event)->summary, + CALENDAR_APPOINTMENT (event)->description, + start_str, + end_str, + CALENDAR_APPOINTMENT (event)->is_all_day ? "(true)" : "(false)"); + + g_free (start_str); + g_free (end_str); + + dprintf (" Occurrences:\n"); + for (l = CALENDAR_APPOINTMENT (event)->occurrences; l; l = l->next) + { + CalendarOccurrence *occurrence = l->data; + + start_str = occurrence->start_time ? + isodate_from_time_t (occurrence->start_time) : + g_strdup ("(undefined)"); + + end_str = occurrence->end_time ? + isodate_from_time_t (occurrence->end_time) : + g_strdup ("(undefined)"); + + dprintf (" start_time '%s', end_time '%s'\n", + start_str, end_str); + + g_free (start_str); + g_free (end_str); + } + } + break; + case CALENDAR_EVENT_TASK: + { + char *start_str; + char *due_str; + char *completed_str; + + start_str = CALENDAR_TASK (event)->start_time ? + isodate_from_time_t (CALENDAR_TASK (event)->start_time) : + g_strdup ("(undefined)"); + due_str = CALENDAR_TASK (event)->due_time ? + isodate_from_time_t (CALENDAR_TASK (event)->due_time) : + g_strdup ("(undefined)"); + completed_str = CALENDAR_TASK (event)->completed_time ? + isodate_from_time_t (CALENDAR_TASK (event)->completed_time) : + g_strdup ("(undefined)"); + + dprintf ("Task: uid '%s', summary '%s', description '%s', " + "start_time '%s', due_time '%s', percent_complete %d, completed_time '%s'\n", + CALENDAR_TASK (event)->uid, + CALENDAR_TASK (event)->summary, + CALENDAR_TASK (event)->description, + start_str, + due_str, + CALENDAR_TASK (event)->percent_complete, + completed_str); + + g_free (completed_str); + } + break; + default: + g_assert_not_reached (); + break; + } +#endif +} + +static inline CalendarClientQuery * +goddamn_this_is_crack (CalendarClientSource *source, + ECalView *view, + gboolean *emit_signal) +{ + g_assert (view != NULL); + + if (source->completed_query.view == view) + { + if (emit_signal) + *emit_signal = TRUE; + return &source->completed_query; + } + else if (source->in_progress_query.view == view) + { + if (emit_signal) + *emit_signal = FALSE; + return &source->in_progress_query; + } + + g_assert_not_reached (); + + return NULL; +} + +static void +calendar_client_handle_query_completed (CalendarClientSource *source, + ECalendarStatus status, + ECalView *view) +{ + CalendarClientQuery *query; + + query = goddamn_this_is_crack (source, view, NULL); + + dprintf ("Query %p completed: %s\n", query, e_cal_get_error_message (status)); + + if (status != E_CALENDAR_STATUS_OK) + { + g_warning ("Calendar query failed: %s\n", + e_cal_get_error_message (status)); + calendar_client_stop_query (source->client, source, query); + return; + } + + g_assert (source->query_in_progress != FALSE); + g_assert (query == &source->in_progress_query); + + calendar_client_query_finalize (&source->completed_query); + + source->completed_query = source->in_progress_query; + source->query_completed = TRUE; + + source->query_in_progress = FALSE; + source->in_progress_query.view = NULL; + source->in_progress_query.events = NULL; + + g_signal_emit (source->client, source->changed_signal_id, 0); +} + +static void +calendar_client_handle_query_result (CalendarClientSource *source, + GList *objects, + ECalView *view) +{ + CalendarClientQuery *query; + CalendarClient *client; + gboolean emit_signal; + gboolean events_changed; + GList *l; + time_t month_begin; + time_t month_end; + + client = source->client; + + query = goddamn_this_is_crack (source, view, &emit_signal); + + dprintf ("Query %p result: %d objects:\n", + query, g_list_length (objects)); + + month_begin = make_time_for_day_begin (1, + client->priv->month, + client->priv->year); + + month_end = make_time_for_day_begin (1, + client->priv->month + 1, + client->priv->year); + + events_changed = FALSE; + for (l = objects; l; l = l->next) + { + CalendarEvent *event; + CalendarEvent *old_event; + icalcomponent *ical = l->data; + char *uid; + + event = calendar_event_new (ical, source, client->priv->zone); + if (!event) + continue; + + calendar_event_generate_ocurrences (event, + ical, + source->source, + month_begin, + month_end, + client->priv->zone); + + uid = calendar_event_get_uid (event); + + old_event = g_hash_table_lookup (query->events, uid); + + if (!calendar_event_equal (event, old_event)) + { + dprintf ("Event %s: ", old_event ? "modified" : "added"); + + calendar_event_debug_dump (event); + + g_hash_table_replace (query->events, uid, event); + + events_changed = TRUE; + } + else + { + g_free (uid); + } + } + + if (emit_signal && events_changed) + { + g_signal_emit (source->client, source->changed_signal_id, 0); + } +} + +static gboolean +check_object_remove (gpointer key, + gpointer value, + gpointer data) +{ + char *uid = data; + ssize_t len; + + len = strlen (uid); + + if (len <= strlen (key) && strncmp (uid, key, len) == 0) + { + dprintf ("Event removed: "); + + calendar_event_debug_dump (value); + + return TRUE; + } + + return FALSE; +} + +static void +calendar_client_handle_objects_removed (CalendarClientSource *source, + GList *ids, + ECalView *view) +{ + CalendarClientQuery *query; + gboolean emit_signal; + gboolean events_changed; + GList *l; + + query = goddamn_this_is_crack (source, view, &emit_signal); + + events_changed = FALSE; + for (l = ids; l; l = l->next) + { + CalendarEvent *event; + ECalComponentId *id = l->data; + char *uid = g_strdup_printf ("%s%s", id->uid, id->rid ? id->rid : ""); + + if (!id->rid || !(*id->rid)) + { + int size = g_hash_table_size (query->events); + + g_hash_table_foreach_remove (query->events, check_object_remove, id->uid); + + if (size != g_hash_table_size (query->events)) + events_changed = TRUE; + } + else if ((event = g_hash_table_lookup (query->events, uid))) + { + dprintf ("Event removed: "); + + calendar_event_debug_dump (event); + + g_assert (g_hash_table_remove (query->events, uid)); + + events_changed = TRUE; + } + g_free (uid); + } + + if (emit_signal && events_changed) + { + g_signal_emit (source->client, source->changed_signal_id, 0); + } +} + +static void +calendar_client_query_finalize (CalendarClientQuery *query) +{ + if (query->view) + g_object_unref (query->view); + query->view = NULL; + + if (query->events) + g_hash_table_destroy (query->events); + query->events = NULL; +} + +static void +calendar_client_stop_query (CalendarClient *client, + CalendarClientSource *source, + CalendarClientQuery *query) +{ + if (query == &source->in_progress_query) + { + dprintf ("Stopping in progress query %p\n", query); + + g_assert (source->query_in_progress != FALSE); + + source->query_in_progress = FALSE; + } + else if (query == &source->completed_query) + { + dprintf ("Stopping completed query %p\n", query); + + g_assert (source->query_completed != FALSE); + + source->query_completed = FALSE; + } + else + g_assert_not_reached (); + + calendar_client_query_finalize (query); +} + +static void +calendar_client_start_query (CalendarClient *client, + CalendarClientSource *source, + const char *query) +{ + ECalView *view = NULL; + GError *error = NULL; + + if (!e_cal_get_query (source->source, query, &view, &error)) + { + g_warning ("Error preparing the query: '%s': %s\n", + query, error->message); + g_error_free (error); + return; + } + + g_assert (view != NULL); + + if (source->query_in_progress) + calendar_client_stop_query (client, source, &source->in_progress_query); + + dprintf ("Starting query %p: '%s'\n", &source->in_progress_query, query); + + source->query_in_progress = TRUE; + source->in_progress_query.view = view; + source->in_progress_query.events = + g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) calendar_event_free); + + g_signal_connect_swapped (view, "objects-added", + G_CALLBACK (calendar_client_handle_query_result), + source); + g_signal_connect_swapped (view, "objects-modified", + G_CALLBACK (calendar_client_handle_query_result), + source); + g_signal_connect_swapped (view, "objects-removed", + G_CALLBACK (calendar_client_handle_objects_removed), + source); + g_signal_connect_swapped (view, "view-done", + G_CALLBACK (calendar_client_handle_query_completed), + source); + + e_cal_view_start (view); +} + +static void +calendar_client_update_appointments (CalendarClient *client) +{ + GSList *l; + char *query; + char *month_begin; + char *month_end; + + if (client->priv->month == -1 || + client->priv->year == -1) + return; + + month_begin = make_isodate_for_day_begin (1, + client->priv->month, + client->priv->year); + + month_end = make_isodate_for_day_begin (1, + client->priv->month + 1, + client->priv->year); + + query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " + "(make-time \"%s\")", + month_begin, month_end); + + for (l = client->priv->appointment_sources; l; l = l->next) + { + CalendarClientSource *cs = l->data; + + if (e_cal_get_load_state (cs->source) != E_CAL_LOAD_LOADED) + continue; + + calendar_client_start_query (client, cs, query); + } + + g_free (month_begin); + g_free (month_end); + g_free (query); +} + +/* FIXME: + * perhaps we should use evo's "hide_completed_tasks" pref? + */ +static void +calendar_client_update_tasks (CalendarClient *client) +{ + GSList *l; + char *query; + +#ifdef FIX_BROKEN_TASKS_QUERY + /* FIXME: this doesn't work for tasks without a start or + * due date + * Look at filter_task() to see the behaviour we + * want. + */ + + char *day_begin; + char *day_end; + + if (client->priv->day == -1 || + client->priv->month == -1 || + client->priv->year == -1) + return; + + day_begin = make_isodate_for_day_begin (client->priv->day, + client->priv->month, + client->priv->year); + + day_end = make_isodate_for_day_begin (client->priv->day + 1, + client->priv->month, + client->priv->year); + if (!day_begin || !day_end) + { + g_warning ("Cannot run query with invalid date: %dd %dy %dm\n", + client->priv->day, + client->priv->month, + client->priv->year); + g_free (day_begin); + g_free (day_end); + return; + } + + query = g_strdup_printf ("(and (occur-in-time-range? (make-time \"%s\") " + "(make-time \"%s\")) " + "(or (not is-completed?) " + "(and (is-completed?) " + "(not (completed-before? (make-time \"%s\"))))))", + day_begin, day_end, day_begin); +#else + query = g_strdup ("#t"); +#endif /* FIX_BROKEN_TASKS_QUERY */ + + for (l = client->priv->task_sources; l; l = l->next) + { + CalendarClientSource *cs = l->data; + + if (e_cal_get_load_state (cs->source) != E_CAL_LOAD_LOADED) + continue; + + calendar_client_start_query (client, cs, query); + } + +#ifdef FIX_BROKEN_TASKS_QUERY + g_free (day_begin); + g_free (day_end); +#endif + g_free (query); +} + +static void +calendar_client_source_finalize (CalendarClientSource *source) +{ + source->client = NULL; + + if (source->source) { + g_signal_handlers_disconnect_by_func (source->source, + cal_opened_cb, source); + g_object_unref (source->source); + } + source->source = NULL; + + calendar_client_query_finalize (&source->completed_query); + calendar_client_query_finalize (&source->in_progress_query); + + source->query_completed = FALSE; + source->query_in_progress = FALSE; +} + +static int +compare_calendar_sources (CalendarClientSource *s1, + CalendarClientSource *s2) +{ + return (s1->source == s2->source) ? 0 : 1; +} + +static GSList * +calendar_client_update_sources_list (CalendarClient *client, + GSList *sources, + GSList *esources, + guint changed_signal_id) +{ + GSList *retval, *l; + + retval = NULL; + + for (l = esources; l; l = l->next) + { + CalendarClientSource dummy_source; + CalendarClientSource *new_source; + GSList *s; + ECal *esource = l->data; + + dummy_source.source = esource; + + dprintf ("update_sources_list: adding client %s: ", + e_source_peek_uid (e_cal_get_source (esource))); + + if ((s = g_slist_find_custom (sources, + &dummy_source, + (GCompareFunc) compare_calendar_sources))) + { + dprintf ("already on list\n"); + new_source = s->data; + sources = g_slist_delete_link (sources, s); + } + else + { + dprintf ("added\n"); + new_source = g_new0 (CalendarClientSource, 1); + new_source->client = client; + new_source->source = g_object_ref (esource); + new_source->changed_signal_id = changed_signal_id; + } + + retval = g_slist_prepend (retval, new_source); + } + + for (l = sources; l; l = l->next) + { + CalendarClientSource *source = l->data; + + dprintf ("Removing client %s from list\n", + e_source_peek_uid (e_cal_get_source (source->source))); + + calendar_client_source_finalize (source); + g_free (source); + } + g_slist_free (sources); + + return retval; +} + +static void +calendar_client_appointment_sources_changed (CalendarClient *client) +{ + GSList *esources; + + dprintf ("appointment_sources_changed: updating ...\n"); + + esources = calendar_sources_get_appointment_sources (client->priv->calendar_sources); + + client->priv->appointment_sources = + calendar_client_update_sources_list (client, + client->priv->appointment_sources, + esources, + signals [APPOINTMENTS_CHANGED]); + + load_calendars (client, CALENDAR_EVENT_APPOINTMENT); + calendar_client_update_appointments (client); +} + +static void +calendar_client_task_sources_changed (CalendarClient *client) +{ + GSList *esources; + + dprintf ("task_sources_changed: updating ...\n"); + + esources = calendar_sources_get_task_sources (client->priv->calendar_sources); + + client->priv->task_sources = + calendar_client_update_sources_list (client, + client->priv->task_sources, + esources, + signals [TASKS_CHANGED]); + + load_calendars (client, CALENDAR_EVENT_TASK); + calendar_client_update_tasks (client); +} + +void +calendar_client_get_date (CalendarClient *client, + guint *year, + guint *month, + guint *day) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + + if (year) + *year = client->priv->year; + + if (month) + *month = client->priv->month; + + if (day) + *day = client->priv->day; +} + +void +calendar_client_select_month (CalendarClient *client, + guint month, + guint year) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (month <= 11); + + if (client->priv->year != year || client->priv->month != month) + { + client->priv->month = month; + client->priv->year = year; + + calendar_client_update_appointments (client); + calendar_client_update_tasks (client); + + g_object_freeze_notify (G_OBJECT (client)); + g_object_notify (G_OBJECT (client), "month"); + g_object_notify (G_OBJECT (client), "year"); + g_object_thaw_notify (G_OBJECT (client)); + } +} + +void +calendar_client_select_day (CalendarClient *client, + guint day) +{ + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (day <= 31); + + if (client->priv->day != day) + { + client->priv->day = day; + + /* don't need to update appointments unless + * the selected month changes + */ +#ifdef FIX_BROKEN_TASKS_QUERY + calendar_client_update_tasks (client); +#endif + + g_object_notify (G_OBJECT (client), "day"); + } +} + +typedef struct +{ + CalendarClient *client; + GSList *events; + time_t start_time; + time_t end_time; +} FilterData; + +typedef void (* CalendarEventFilterFunc) (const char *uid, + CalendarEvent *event, + FilterData *filter_data); + +static void +filter_appointment (const char *uid, + CalendarEvent *event, + FilterData *filter_data) +{ + GSList *occurrences, *l; + + if (event->type != CALENDAR_EVENT_APPOINTMENT) + return; + + occurrences = CALENDAR_APPOINTMENT (event)->occurrences; + CALENDAR_APPOINTMENT (event)->occurrences = NULL; + + for (l = occurrences; l; l = l->next) + { + CalendarOccurrence *occurrence = l->data; + time_t start_time = occurrence->start_time; + time_t end_time = occurrence->end_time; + + if ((start_time >= filter_data->start_time && + start_time < filter_data->end_time) || + (start_time <= filter_data->start_time && + (end_time - 1) > filter_data->start_time)) + { + CalendarEvent *new_event; + + new_event = calendar_event_copy (event); + + CALENDAR_APPOINTMENT (new_event)->start_time = occurrence->start_time; + CALENDAR_APPOINTMENT (new_event)->end_time = occurrence->end_time; + + filter_data->events = g_slist_prepend (filter_data->events, new_event); + } + } + + CALENDAR_APPOINTMENT (event)->occurrences = occurrences; +} + +static void +filter_task (const char *uid, + CalendarEvent *event, + FilterData *filter_data) +{ +#ifdef FIX_BROKEN_TASKS_QUERY + CalendarTask *task; +#endif + + if (event->type != CALENDAR_EVENT_TASK) + return; + +#ifdef FIX_BROKEN_TASKS_QUERY + task = CALENDAR_TASK (event); + + if (task->start_time && task->start_time > filter_data->start_time) + return; + + if (task->completed_time && + (task->completed_time < filter_data->start_time || + task->completed_time > filter_data->end_time)) + return; +#endif /* FIX_BROKEN_TASKS_QUERY */ + + filter_data->events = g_slist_prepend (filter_data->events, + calendar_event_copy (event)); +} + +static GSList * +calendar_client_filter_events (CalendarClient *client, + GSList *sources, + CalendarEventFilterFunc filter_func, + time_t start_time, + time_t end_time) +{ + FilterData filter_data; + GSList *l; + GSList *retval; + + if (!sources) + return NULL; + + filter_data.client = client; + filter_data.events = NULL; + filter_data.start_time = start_time; + filter_data.end_time = end_time; + + retval = NULL; + for (l = sources; l; l = l->next) + { + CalendarClientSource *source = l->data; + + if (source->query_completed) + { + filter_data.events = NULL; + g_hash_table_foreach (source->completed_query.events, + (GHFunc) filter_func, + &filter_data); + + filter_data.events = g_slist_reverse (filter_data.events); + + retval = g_slist_concat (retval, filter_data.events); + } + } + + return retval; +} + +GSList * +calendar_client_get_events (CalendarClient *client, + CalendarEventType event_mask) +{ + GSList *appointments; + GSList *tasks; + time_t day_begin; + time_t day_end; + + g_return_val_if_fail (CALENDAR_IS_CLIENT (client), NULL); + g_return_val_if_fail (client->priv->day != -1 && + client->priv->month != -1 && + client->priv->year != -1, NULL); + + day_begin = make_time_for_day_begin (client->priv->day, + client->priv->month, + client->priv->year); + day_end = make_time_for_day_begin (client->priv->day + 1, + client->priv->month, + client->priv->year); + + appointments = NULL; + if (event_mask & CALENDAR_EVENT_APPOINTMENT) + { + appointments = calendar_client_filter_events (client, + client->priv->appointment_sources, + filter_appointment, + day_begin, + day_end); + } + + tasks = NULL; + if (event_mask & CALENDAR_EVENT_TASK) + { + tasks = calendar_client_filter_events (client, + client->priv->task_sources, + filter_task, + day_begin, + day_end); + } + + return g_slist_concat (appointments, tasks); +} + +static inline int +day_from_time_t (time_t t) +{ + struct tm *tm = localtime (&t); + + g_assert (tm == NULL || (tm->tm_mday >=1 && tm->tm_mday <= 31)); + + return tm ? tm->tm_mday : 0; +} + +void +calendar_client_foreach_appointment_day (CalendarClient *client, + CalendarDayIter iter_func, + gpointer user_data) +{ + GSList *appointments, *l; + gboolean marked_days [32] = { FALSE, }; + time_t month_begin; + time_t month_end; + int i; + + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (iter_func != NULL); + g_return_if_fail (client->priv->month != -1 && + client->priv->year != -1); + + month_begin = make_time_for_day_begin (1, + client->priv->month, + client->priv->year); + month_end = make_time_for_day_begin (1, + client->priv->month + 1, + client->priv->year); + + appointments = calendar_client_filter_events (client, + client->priv->appointment_sources, + filter_appointment, + month_begin, + month_end); + for (l = appointments; l; l = l->next) + { + CalendarAppointment *appointment = l->data; + + if (appointment->start_time) + { + time_t day_time = appointment->start_time; + + if (day_time >= month_begin) + marked_days [day_from_time_t (day_time)] = TRUE; + + if (appointment->end_time) + { + int day_offset; + int duration = appointment->end_time - appointment->start_time; + /* mark the days for the appointment, no need to add an extra one when duration is a multiple of 86400 */ + for (day_offset = 1; day_offset <= duration / 86400 && duration != day_offset * 86400; day_offset++) + { + time_t day_tm = appointment->start_time + day_offset * 86400; + + if (day_tm > month_end) + break; + if (day_tm >= month_begin) + marked_days [day_from_time_t (day_tm)] = TRUE; + } + } + } + calendar_event_free (CALENDAR_EVENT (appointment)); + } + + g_slist_free (appointments); + + for (i = 1; i < 32; i++) + { + if (marked_days [i]) + iter_func (client, i, user_data); + } +} + +void +calendar_client_set_task_completed (CalendarClient *client, + char *task_uid, + gboolean task_completed, + guint percent_complete) +{ + GSList *l; + ECal *esource; + icalcomponent *ical; + icalproperty *prop; + icalproperty_status status; + + g_return_if_fail (CALENDAR_IS_CLIENT (client)); + g_return_if_fail (task_uid != NULL); + g_return_if_fail (task_completed == FALSE || percent_complete == 100); + + ical = NULL; + esource = NULL; + for (l = client->priv->task_sources; l; l = l->next) + { + CalendarClientSource *source = l->data; + + esource = source->source; + e_cal_get_object (esource, task_uid, NULL, &ical, NULL); + if (ical) + break; + } + + if (!ical) + { + g_warning ("Cannot locate task with uid = '%s'\n", task_uid); + return; + } + + g_assert (esource != NULL); + + /* Completed time */ + prop = icalcomponent_get_first_property (ical, + ICAL_COMPLETED_PROPERTY); + if (task_completed) + { + struct icaltimetype completed_time; + + completed_time = icaltime_current_time_with_zone (client->priv->zone); + if (!prop) + { + icalcomponent_add_property (ical, + icalproperty_new_completed (completed_time)); + } + else + { + icalproperty_set_completed (prop, completed_time); + } + } + else if (prop) + { + icalcomponent_remove_property (ical, prop); + } + + /* Percent complete */ + prop = icalcomponent_get_first_property (ical, + ICAL_PERCENTCOMPLETE_PROPERTY); + if (!prop) + { + icalcomponent_add_property (ical, + icalproperty_new_percentcomplete (percent_complete)); + } + else + { + icalproperty_set_percentcomplete (prop, percent_complete); + } + + /* Status */ + status = task_completed ? ICAL_STATUS_COMPLETED : ICAL_STATUS_NEEDSACTION; + prop = icalcomponent_get_first_property (ical, ICAL_STATUS_PROPERTY); + if (prop) + { + icalproperty_set_status (prop, status); + } + else + { + icalcomponent_add_property (ical, + icalproperty_new_status (status)); + } + + e_cal_modify_object (esource, ical, CALOBJ_MOD_ALL, NULL); +} diff --git a/src/calendar-client/calendar-client.h b/src/calendar-client/calendar-client.h new file mode 100644 index 000000000..3ae3b2fc8 --- /dev/null +++ b/src/calendar-client/calendar-client.h @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#ifndef __CALENDAR_CLIENT_H__ +#define __CALENDAR_CLIENT_H__ + +#include + +G_BEGIN_DECLS + +typedef enum +{ + CALENDAR_EVENT_APPOINTMENT = 1 << 0, + CALENDAR_EVENT_TASK = 1 << 1, + CALENDAR_EVENT_ALL = (1 << 2) - 1 +} CalendarEventType; + +#define CALENDAR_TYPE_CLIENT (calendar_client_get_type ()) +#define CALENDAR_CLIENT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_CLIENT, CalendarClient)) +#define CALENDAR_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_CLIENT, CalendarClientClass)) +#define CALENDAR_IS_CLIENT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_CLIENT)) +#define CALENDAR_IS_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_CLIENT)) +#define CALENDAR_CLIENT_GET_CLASS(o)(G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_CLIENT, CalendarClientClass)) + +typedef struct _CalendarClient CalendarClient; +typedef struct _CalendarClientClass CalendarClientClass; +typedef struct _CalendarClientPrivate CalendarClientPrivate; + +struct _CalendarClient +{ + GObject parent; + CalendarClientPrivate *priv; +}; + +struct _CalendarClientClass +{ + GObjectClass parent_class; + + void (* appointments_changed) (CalendarClient *client); + void (* tasks_changed) (CalendarClient *client); +}; + + +typedef struct +{ + time_t start_time; + time_t end_time; +} CalendarOccurrence; + +typedef struct +{ + char *uid; + char *rid; + char *uri; + char *summary; + char *description; + char *color_string; + time_t start_time; + time_t end_time; + guint is_all_day : 1; + + /* Only used internally */ + GSList *occurrences; +} CalendarAppointment; + +typedef struct +{ + char *uid; + char *summary; + char *description; + char *color_string; + char *url; + time_t start_time; + time_t due_time; + guint percent_complete; + time_t completed_time; + int priority; +} CalendarTask; + +typedef struct +{ + union + { + CalendarAppointment appointment; + CalendarTask task; + } event; + CalendarEventType type; +} CalendarEvent; + +#define CALENDAR_EVENT(e) ((CalendarEvent *)(e)) +#define CALENDAR_APPOINTMENT(e) ((CalendarAppointment *)(e)) +#define CALENDAR_TASK(e) ((CalendarTask *)(e)) + +typedef void (* CalendarDayIter) (CalendarClient *client, + guint day, + gpointer user_data); + + +GType calendar_client_get_type (void) G_GNUC_CONST; +CalendarClient *calendar_client_new (void); + +void calendar_client_get_date (CalendarClient *client, + guint *year, + guint *month, + guint *day); +void calendar_client_select_month (CalendarClient *client, + guint month, + guint year); +void calendar_client_select_day (CalendarClient *client, + guint day); + +GSList *calendar_client_get_events (CalendarClient *client, + CalendarEventType event_mask); +void calendar_client_foreach_appointment_day (CalendarClient *client, + CalendarDayIter iter_func, + gpointer user_data); + +void calendar_client_set_task_completed (CalendarClient *client, + char *task_uid, + gboolean task_completed, + guint percent_complete); + +void calendar_event_free (CalendarEvent *event); + +G_END_DECLS + +#endif /* __CALENDAR_CLIENT_H__ */ diff --git a/src/calendar-client/calendar-debug.h b/src/calendar-client/calendar-debug.h new file mode 100644 index 000000000..61ad15652 --- /dev/null +++ b/src/calendar-client/calendar-debug.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: + * Mark McLoughlin + */ + +#ifndef __CALENDAR_DEBUG_H__ +#define __CALENDAR_DEBUG_H__ + +#include + +G_BEGIN_DECLS + +#ifdef CALENDAR_ENABLE_DEBUG + +#include + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) fprintf (stderr, __VA_ARGS__); +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) fprintf (stderr, args); +#endif + +#else /* if !defined (CALENDAR_DEBUG) */ + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) +#endif + +#endif /* CALENDAR_ENABLE_DEBUG */ + +G_END_DECLS + +#endif /* __CALENDAR_DEBUG_H__ */ diff --git a/src/calendar-client/calendar-sources.c b/src/calendar-client/calendar-sources.c new file mode 100644 index 000000000..fa1fcaccd --- /dev/null +++ b/src/calendar-client/calendar-sources.c @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#include + +#include "calendar-sources.h" + +#include +#include +#include +#define HANDLE_LIBICAL_MEMORY +#include +#include +#include + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +#ifndef _ +#define _(x) gettext(x) +#endif + +#ifndef N_ +#define N_(x) x +#endif + +#define CALENDAR_SOURCES_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), CALENDAR_TYPE_SOURCES, CalendarSourcesPrivate)) + +#define CALENDAR_SOURCES_EVO_DIR "/apps/evolution" +#define CALENDAR_SOURCES_APPOINTMENT_SOURCES_KEY CALENDAR_SOURCES_EVO_DIR "/calendar/sources" +#define CALENDAR_SOURCES_SELECTED_APPOINTMENT_SOURCES_DIR CALENDAR_SOURCES_EVO_DIR "/calendar/display" +#define CALENDAR_SOURCES_SELECTED_APPOINTMENT_SOURCES_KEY CALENDAR_SOURCES_SELECTED_APPOINTMENT_SOURCES_DIR "/selected_calendars" +#define CALENDAR_SOURCES_TASK_SOURCES_KEY CALENDAR_SOURCES_EVO_DIR "/tasks/sources" +#define CALENDAR_SOURCES_SELECTED_TASK_SOURCES_DIR CALENDAR_SOURCES_EVO_DIR "/calendar/tasks" +#define CALENDAR_SOURCES_SELECTED_TASK_SOURCES_KEY CALENDAR_SOURCES_SELECTED_TASK_SOURCES_DIR "/selected_tasks" + +typedef struct _CalendarSourceData CalendarSourceData; + +struct _CalendarSourceData +{ + ECalSourceType source_type; + CalendarSources *sources; + guint changed_signal; + + GSList *clients; + GSList *selected_sources; + ESourceList *esource_list; + + guint selected_sources_listener; + char *selected_sources_dir; + + guint timeout_id; + + guint loaded : 1; +}; + +struct _CalendarSourcesPrivate +{ + CalendarSourceData appointment_sources; + CalendarSourceData task_sources; + + GConfClient *gconf_client; +}; + +static void calendar_sources_class_init (CalendarSourcesClass *klass); +static void calendar_sources_init (CalendarSources *sources); +static void calendar_sources_finalize (GObject *object); + +static void backend_died_cb (ECal *client, CalendarSourceData *source_data); +static void calendar_sources_esource_list_changed (ESourceList *source_list, + CalendarSourceData *source_data); + +enum +{ + APPOINTMENT_SOURCES_CHANGED, + TASK_SOURCES_CHANGED, + LAST_SIGNAL +}; +static guint signals [LAST_SIGNAL] = { 0, }; + +static GObjectClass *parent_class = NULL; +static CalendarSources *calendar_sources_singleton = NULL; + +GType +calendar_sources_get_type (void) +{ + static GType sources_type = 0; + + if (!sources_type) + { + static const GTypeInfo sources_info = + { + sizeof (CalendarSourcesClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) calendar_sources_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (CalendarSources), + 0, /* n_preallocs */ + (GInstanceInitFunc) calendar_sources_init, + }; + + sources_type = g_type_register_static (G_TYPE_OBJECT, + "CalendarSources", + &sources_info, 0); + } + + return sources_type; +} + +static void +calendar_sources_class_init (CalendarSourcesClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + parent_class = g_type_class_peek_parent (klass); + + gobject_class->finalize = calendar_sources_finalize; + + g_type_class_add_private (klass, sizeof (CalendarSourcesPrivate)); + + signals [APPOINTMENT_SOURCES_CHANGED] = + g_signal_new ("appointment-sources-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarSourcesClass, + appointment_sources_changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + signals [TASK_SOURCES_CHANGED] = + g_signal_new ("task-sources-changed", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (CalendarSourcesClass, + task_sources_changed), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); +} + +static void +calendar_sources_init (CalendarSources *sources) +{ + sources->priv = CALENDAR_SOURCES_GET_PRIVATE (sources); + + sources->priv->appointment_sources.source_type = E_CAL_SOURCE_TYPE_EVENT; + sources->priv->appointment_sources.sources = sources; + sources->priv->appointment_sources.changed_signal = signals [APPOINTMENT_SOURCES_CHANGED]; + sources->priv->appointment_sources.timeout_id = 0; + + sources->priv->task_sources.source_type = E_CAL_SOURCE_TYPE_TODO; + sources->priv->task_sources.sources = sources; + sources->priv->task_sources.changed_signal = signals [TASK_SOURCES_CHANGED]; + sources->priv->task_sources.timeout_id = 0; + + sources->priv->gconf_client = gconf_client_get_default (); +} + +static void +calendar_sources_finalize_source_data (CalendarSources *sources, + CalendarSourceData *source_data) +{ + if (source_data->loaded) + { + GSList *l; + + if (source_data->selected_sources_dir) + { + gconf_client_remove_dir (sources->priv->gconf_client, + source_data->selected_sources_dir, + NULL); + + g_free (source_data->selected_sources_dir); + source_data->selected_sources_dir = NULL; + } + + if (source_data->selected_sources_listener) + { + gconf_client_notify_remove (sources->priv->gconf_client, + source_data->selected_sources_listener); + source_data->selected_sources_listener = 0; + } + + for (l = source_data->clients; l; l = l->next) + { + g_signal_handlers_disconnect_by_func (G_OBJECT (l->data), + G_CALLBACK (backend_died_cb), + source_data); + g_object_unref (l->data); + } + g_slist_free (source_data->clients); + source_data->clients = NULL; + + if (source_data->esource_list) + { + g_signal_handlers_disconnect_by_func (source_data->esource_list, + G_CALLBACK (calendar_sources_esource_list_changed), + source_data); + g_object_unref (source_data->esource_list); + } + source_data->esource_list = NULL; + + for (l = source_data->selected_sources; l; l = l->next) + g_free (l->data); + g_slist_free (source_data->selected_sources); + source_data->selected_sources = NULL; + + if (source_data->timeout_id != 0) + { + g_source_remove (source_data->timeout_id); + source_data->timeout_id = 0; + } + + source_data->loaded = FALSE; + } +} + +static void +calendar_sources_finalize (GObject *object) +{ + CalendarSources *sources = CALENDAR_SOURCES (object); + + calendar_sources_finalize_source_data (sources, &sources->priv->appointment_sources); + calendar_sources_finalize_source_data (sources, &sources->priv->task_sources); + + if (sources->priv->gconf_client) + g_object_unref (sources->priv->gconf_client); + sources->priv->gconf_client = NULL; + + if (G_OBJECT_CLASS (parent_class)->finalize) + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +CalendarSources * +calendar_sources_get (void) +{ + gpointer singleton_location = &calendar_sources_singleton; + + if (calendar_sources_singleton) + return g_object_ref (calendar_sources_singleton); + + calendar_sources_singleton = g_object_new (CALENDAR_TYPE_SOURCES, NULL); + g_object_add_weak_pointer (G_OBJECT (calendar_sources_singleton), + singleton_location); + + return calendar_sources_singleton; +} + +static gboolean +is_source_selected (ESource *esource, + GSList *selected_sources) +{ + const char *uid; + GSList *l; + + uid = e_source_peek_uid (esource); + + for (l = selected_sources; l; l = l->next) + { + const char *source = l->data; + + if (!strcmp (source, uid)) + return TRUE; + } + + return FALSE; +} + +static char * +auth_func_cb (ECal *ecal, + const char *prompt, + const char *key, + gpointer user_data) +{ + ESource *source; + const gchar *auth_domain; + const gchar *component_name; + + source = e_cal_get_source (ecal); + auth_domain = e_source_get_property (source, "auth-domain"); + component_name = auth_domain ? auth_domain : "Calendar"; + + return e_passwords_get_password (component_name, key); +} + +/* The clients are just created here but not loaded */ +static ECal * +get_ecal_from_source (ESource *esource, + ECalSourceType source_type, + GSList *existing_clients) +{ + ECal *retval; + + if (existing_clients) + { + GSList *l; + + for (l = existing_clients; l; l = l->next) + { + ECal *client = E_CAL (l->data); + + if (e_source_equal (esource, e_cal_get_source (client))) + { + dprintf (" load_esource: found existing source ... returning that\n"); + + return g_object_ref (client); + } + } + } + + retval = e_cal_new (esource, source_type); + if (!retval) + { + g_warning ("Could not load source '%s' from '%s'\n", + e_source_peek_name (esource), + e_source_peek_relative_uri (esource)); + return NULL; + } + + e_cal_set_auth_func (retval, auth_func_cb, NULL); + + return retval; +} + +/* - Order doesn't matter + * - Can just compare object pointers since we + * re-use client connections + */ +static gboolean +compare_ecal_lists (GSList *a, + GSList *b) +{ + GSList *l; + + if (g_slist_length (a) != g_slist_length (b)) + return FALSE; + + for (l = a; l; l = l->next) + { + if (!g_slist_find (b, l->data)) + return FALSE; + } + + return TRUE; +} + +static inline void +debug_dump_selected_sources (GSList *selected_sources) +{ +#ifdef CALENDAR_ENABLE_DEBUG + GSList *l; + + dprintf ("Selected sources:\n"); + for (l = selected_sources; l; l = l->next) + { + char *source = l->data; + + dprintf (" %s\n", source); + } + dprintf ("\n"); +#endif +} + +static inline void +debug_dump_ecal_list (GSList *ecal_list) +{ +#ifdef CALENDAR_ENABLE_DEBUG + GSList *l; + + dprintf ("Loaded clients:\n"); + for (l = ecal_list; l; l = l->next) + { + ECal *client = l->data; + ESource *source = e_cal_get_source (client); + + dprintf (" %s %s %s\n", + e_source_peek_uid (source), + e_source_peek_name (source), + e_cal_get_uri (client)); + } +#endif +} + +static void +calendar_sources_load_esource_list (CalendarSourceData *source_data); + +static gboolean +backend_restart (gpointer data) +{ + CalendarSourceData *source_data = data; + + calendar_sources_load_esource_list (source_data); + + source_data->timeout_id = 0; + + return FALSE; +} + +static void +backend_died_cb (ECal *client, CalendarSourceData *source_data) +{ + const char *uristr; + + source_data->clients = g_slist_remove (source_data->clients, client); + if (g_slist_length (source_data->clients) < 1) + { + g_slist_free (source_data->clients); + source_data->clients = NULL; + } + uristr = e_cal_get_uri (client); + g_warning ("The calendar backend for %s has crashed.", uristr); + + if (source_data->timeout_id != 0) + { + g_source_remove (source_data->timeout_id); + source_data->timeout_id = 0; + } + + source_data->timeout_id = g_timeout_add_seconds (2, backend_restart, + source_data); +} + +static void +calendar_sources_load_esource_list (CalendarSourceData *source_data) +{ + GSList *clients = NULL; + GSList *groups, *l; + gboolean emit_signal = FALSE; + + g_return_if_fail (source_data->esource_list != NULL); + + debug_dump_selected_sources (source_data->selected_sources); + + dprintf ("Source groups:\n"); + groups = e_source_list_peek_groups (source_data->esource_list); + for (l = groups; l; l = l->next) + { + GSList *esources, *s; + + dprintf (" %s\n", e_source_group_peek_uid (l->data)); + dprintf (" sources:\n"); + + esources = e_source_group_peek_sources (l->data); + for (s = esources; s; s = s->next) + { + ESource *esource = E_SOURCE (s->data); + ECal *client; + + dprintf (" type = '%s' uid = '%s', name = '%s', relative uri = '%s': \n", + source_data->source_type == E_CAL_SOURCE_TYPE_EVENT ? "appointment" : "task", + e_source_peek_uid (esource), + e_source_peek_name (esource), + e_source_peek_relative_uri (esource)); + + if (is_source_selected (esource, source_data->selected_sources) && + (client = get_ecal_from_source (esource, source_data->source_type, source_data->clients))) + { + clients = g_slist_prepend (clients, client); + } + } + } + dprintf ("\n"); + + if (source_data->loaded && + !compare_ecal_lists (source_data->clients, clients)) + emit_signal = TRUE; + + for (l = source_data->clients; l; l = l->next) + { + g_signal_handlers_disconnect_by_func (G_OBJECT (l->data), + G_CALLBACK (backend_died_cb), + source_data); + + g_object_unref (l->data); + } + g_slist_free (source_data->clients); + source_data->clients = g_slist_reverse (clients); + + /* connect to backend_died after we disconnected the previous signal + * handlers. If we do it before, we'll lose some handlers (for clients that + * were already there before) */ + for (l = source_data->clients; l; l = l->next) + { + g_signal_connect (G_OBJECT (l->data), "backend_died", + G_CALLBACK (backend_died_cb), source_data); + } + + if (emit_signal) + { + dprintf ("Emitting %s-sources-changed signal\n", + source_data->source_type == E_CAL_SOURCE_TYPE_EVENT ? "appointment" : "task"); + g_signal_emit (source_data->sources, source_data->changed_signal, 0); + } + + debug_dump_ecal_list (source_data->clients); +} + +static void +calendar_sources_esource_list_changed (ESourceList *source_list, + CalendarSourceData *source_data) + +{ + dprintf ("ESourceList changed, reloading\n"); + + calendar_sources_load_esource_list (source_data); +} + +static void +calendar_sources_selected_sources_notify (GConfClient *client, + guint cnx_id, + GConfEntry *entry, + CalendarSourceData *source_data) +{ + GSList *l; + + if (!entry->value || + entry->value->type != GCONF_VALUE_LIST || + gconf_value_get_list_type (entry->value) != GCONF_VALUE_STRING) + return; + + dprintf ("Selected sources key (%s) changed, reloading\n", entry->key); + + for (l = source_data->selected_sources; l; l = l->next) + g_free (l->data); + source_data->selected_sources = NULL; + + for (l = gconf_value_get_list (entry->value); l; l = l->next) + { + const char *source = gconf_value_get_string (l->data); + + source_data->selected_sources = + g_slist_prepend (source_data->selected_sources, + g_strdup (source)); + } + source_data->selected_sources = + g_slist_reverse (source_data->selected_sources); + + calendar_sources_load_esource_list (source_data); +} + +static void +calendar_sources_load_sources (CalendarSources *sources, + CalendarSourceData *source_data, + const char *sources_key, + const char *selected_sources_key, + const char *selected_sources_dir) +{ + GConfClient *gconf_client; + GError *error; + + dprintf ("---------------------------\n"); + dprintf ("Loading sources:\n"); + dprintf (" sources_key: %s\n", sources_key); + dprintf (" selected_sources_key: %s\n", selected_sources_key); + dprintf (" selected_sources_dir: %s\n", selected_sources_dir); + + gconf_client = sources->priv->gconf_client; + + error = NULL; + source_data->selected_sources = gconf_client_get_list (gconf_client, + selected_sources_key, + GCONF_VALUE_STRING, + &error); + if (error) + { + g_warning ("Failed to get selected sources from '%s': %s\n", + selected_sources_key, + error->message); + g_error_free (error); + return; + } + + gconf_client_add_dir (gconf_client, + selected_sources_dir, + GCONF_CLIENT_PRELOAD_NONE, + NULL); + source_data->selected_sources_dir = g_strdup (selected_sources_dir); + + source_data->selected_sources_listener = + gconf_client_notify_add (gconf_client, + selected_sources_dir, + (GConfClientNotifyFunc) calendar_sources_selected_sources_notify, + source_data, NULL, NULL); + + source_data->esource_list = e_source_list_new_for_gconf (gconf_client, sources_key); + g_signal_connect (source_data->esource_list, "changed", + G_CALLBACK (calendar_sources_esource_list_changed), + source_data); + + calendar_sources_load_esource_list (source_data); + + source_data->loaded = TRUE; + + dprintf ("---------------------------\n"); +} + +GSList * +calendar_sources_get_appointment_sources (CalendarSources *sources) +{ + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); + + if (!sources->priv->appointment_sources.loaded) + { + calendar_sources_load_sources (sources, + &sources->priv->appointment_sources, + CALENDAR_SOURCES_APPOINTMENT_SOURCES_KEY, + CALENDAR_SOURCES_SELECTED_APPOINTMENT_SOURCES_KEY, + CALENDAR_SOURCES_SELECTED_APPOINTMENT_SOURCES_DIR); + } + + return sources->priv->appointment_sources.clients; +} + +GSList * +calendar_sources_get_task_sources (CalendarSources *sources) +{ + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); + + if (!sources->priv->task_sources.loaded) + { + calendar_sources_load_sources (sources, + &sources->priv->task_sources, + CALENDAR_SOURCES_TASK_SOURCES_KEY, + CALENDAR_SOURCES_SELECTED_TASK_SOURCES_KEY, + CALENDAR_SOURCES_SELECTED_TASK_SOURCES_DIR); + } + + return sources->priv->task_sources.clients; +} diff --git a/src/calendar-client/calendar-sources.h b/src/calendar-client/calendar-sources.h new file mode 100644 index 000000000..52741abd7 --- /dev/null +++ b/src/calendar-client/calendar-sources.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#ifndef __CALENDAR_SOURCES_H__ +#define __CALENDAR_SOURCES_H__ + +#include + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ()) +#define CALENDAR_SOURCES(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), CALENDAR_TYPE_SOURCES, CalendarSources)) +#define CALENDAR_SOURCES_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), CALENDAR_TYPE_SOURCES, CalendarSourcesClass)) +#define CALENDAR_IS_SOURCES(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), CALENDAR_TYPE_SOURCES)) +#define CALENDAR_IS_SOURCES_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), CALENDAR_TYPE_SOURCES)) +#define CALENDAR_SOURCES_GET_CLASS(o)(G_TYPE_INSTANCE_GET_CLASS ((o), CALENDAR_TYPE_SOURCES, CalendarSourcesClass)) + +typedef struct _CalendarSources CalendarSources; +typedef struct _CalendarSourcesClass CalendarSourcesClass; +typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate; + +struct _CalendarSources +{ + GObject parent; + CalendarSourcesPrivate *priv; +}; + +struct _CalendarSourcesClass +{ + GObjectClass parent_class; + + void (* appointment_sources_changed) (CalendarSources *sources); + void (* task_sources_changed) (CalendarSources *sources); +}; + + +GType calendar_sources_get_type (void) G_GNUC_CONST; +CalendarSources *calendar_sources_get (void); +GSList *calendar_sources_get_appointment_sources (CalendarSources *sources); +GSList *calendar_sources_get_task_sources (CalendarSources *sources); + +G_END_DECLS + +#endif /* __CALENDAR_SOURCES_H__ */ diff --git a/src/shell-evolution-event-source.c b/src/shell-evolution-event-source.c new file mode 100644 index 000000000..dd2aea91f --- /dev/null +++ b/src/shell-evolution-event-source.c @@ -0,0 +1,255 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "calendar-client/calendar-client.h" +#include "shell-evolution-event-source.h" + + +struct _ShellEvolutionEventSourceClass +{ + GObjectClass parent_class; +}; + +struct _ShellEvolutionEventSource { + GObject parent; + CalendarClient *client; + /* The month that we are currently requesting events from */ + gint req_year; + gint req_mon; /* starts at 1, not zero */ +}; + +/* Signals */ +enum +{ + CHANGED_SIGNAL, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (ShellEvolutionEventSource, shell_evolution_event_source, G_TYPE_OBJECT); + +static void +on_tasks_changed (CalendarClient *client, + gpointer user_data) +{ + ShellEvolutionEventSource *source = SHELL_EVOLUTION_EVENT_SOURCE (user_data); + /* g_print ("on tasks changed\n"); */ + g_signal_emit (source, signals[CHANGED_SIGNAL], 0); +} + +static void +on_appointments_changed (CalendarClient *client, + gpointer user_data) +{ + ShellEvolutionEventSource *source = SHELL_EVOLUTION_EVENT_SOURCE (user_data); + /* g_print ("on appointments changed\n"); */ + g_signal_emit (source, signals[CHANGED_SIGNAL], 0); +} + +static void +shell_evolution_event_source_init (ShellEvolutionEventSource *source) +{ + source->client = calendar_client_new (); + g_signal_connect (source->client, + "tasks-changed", + G_CALLBACK (on_tasks_changed), + source); + g_signal_connect (source->client, + "appointments-changed", + G_CALLBACK (on_appointments_changed), + source); +} + +static void +shell_evolution_event_source_finalize (GObject *object) +{ + ShellEvolutionEventSource *source = SHELL_EVOLUTION_EVENT_SOURCE (object); + g_object_unref (source->client); + G_OBJECT_CLASS (shell_evolution_event_source_parent_class)->finalize (object); +} + +static void +shell_evolution_event_source_class_init (ShellEvolutionEventSourceClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_evolution_event_source_finalize; + + signals[CHANGED_SIGNAL] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + +} + +ShellEvolutionEventSource * +shell_evolution_event_source_new (void) +{ + return SHELL_EVOLUTION_EVENT_SOURCE (g_object_new (SHELL_TYPE_EVOLUTION_EVENT_SOURCE, NULL)); +} + +void +shell_evolution_event_source_request_range (ShellEvolutionEventSource *source, + gint64 msec_begin, + gint64 msec_end) +{ + GDateTime *middle; + + /* The CalendarClient type is a convenience wrapper on top of + * Evolution Data Server. It is based on the assumption that only + * a single month is shown at a time. + * + * To avoid reimplemting all the work already done in CalendarClient + * we make the same assumption. This means that we only show events + * in the month that is in the middle of @msec_begin and + * @msec_end. Since the Shell displays a month at a time (plus the + * days before and after) it works out just fine. + */ + + middle = g_date_time_new_from_unix_utc ((msec_begin + msec_end) / 2 / 1000); + g_date_time_get_ymd (middle, &source->req_year, &source->req_mon, NULL); + g_date_time_unref (middle); + calendar_client_select_month (source->client, source->req_mon - 1, source->req_year); +} + +static gint +event_cmp (gconstpointer a, + gconstpointer b) +{ + const ShellEvolutionEvent *ea; + const ShellEvolutionEvent *eb; + + ea = a; + eb = b; + if (ea->msec_begin < eb->msec_begin) + return -1; + else if (ea->msec_begin > eb->msec_begin) + return 1; + else + return 0; +} + +/** + * shell_evolution_event_source_get_events: + * @source: A #ShellEvolutionEventSource. + * @msec_begin: Start date (milli-seconds since Epoch). + * @msec_end: End date (milli-seconds since Epoch). + * + * Gets all events that occur between @msec_begin and @msec_end. + * + * Returns: (element-type ShellEvolutionEvent) (transfer full): List of events. + */ +GList * +shell_evolution_event_source_get_events (ShellEvolutionEventSource *source, + gint64 msec_begin, + gint64 msec_end) +{ + GList *result; + GDateTime *cur_date; + GDateTime *begin_date; + GDateTime *end_date; + + g_return_val_if_fail (msec_begin <= msec_end, NULL); + + result = NULL; + + begin_date = g_date_time_new_from_unix_utc (msec_begin / 1000); + end_date = g_date_time_new_from_unix_utc (msec_end / 1000); + cur_date = g_date_time_ref (begin_date); + do + { + gint year, mon, day; + GDateTime *next_date; + + g_date_time_get_ymd (cur_date, &year, &mon, &day); + /* g_print ("y=%04d m=%02d d=%02d\n", year, mon, day); */ + + /* Silently drop events not in range (see comment in + * shell_evolution_event_source_request_range() above) + */ + if (!(year == source->req_year && mon == source->req_mon)) + { + /* g_print ("skipping day\n"); */ + } + else + { + GSList *events; + GSList *l; + calendar_client_select_day (source->client, day); + events = calendar_client_get_events (source->client, CALENDAR_EVENT_APPOINTMENT); + /* g_print ("num_events: %d\n", g_slist_length (events)); */ + for (l = events; l; l = l->next) + { + CalendarAppointment *appointment = l->data; + ShellEvolutionEvent *event; + gint64 start_time; + + if (appointment->is_all_day) + { + start_time = g_date_time_to_unix (cur_date) * G_GINT64_CONSTANT (1000); + } + else + { + start_time = appointment->start_time * G_GINT64_CONSTANT (1000); + } + event = shell_evolution_event_new (appointment->summary, + appointment->is_all_day, + start_time); + result = g_list_prepend (result, event); + } + g_slist_foreach (events, (GFunc) calendar_event_free, NULL); + g_slist_free (events); + } + + next_date = g_date_time_add_days (cur_date, 1); + g_date_time_unref (cur_date); + cur_date = next_date; + } + while (g_date_time_difference (end_date, cur_date) > 0); + g_date_time_unref (begin_date); + g_date_time_unref (end_date); + + result = g_list_sort (result, event_cmp); + + return result; +} + +G_DEFINE_BOXED_TYPE (ShellEvolutionEvent, + shell_evolution_event, + shell_evolution_event_copy, + shell_evolution_event_free); + +void +shell_evolution_event_free (ShellEvolutionEvent *event) +{ + g_free (event->summary); + g_free (event); +} + +ShellEvolutionEvent * +shell_evolution_event_copy (ShellEvolutionEvent *event) +{ + ShellEvolutionEvent *copy; + copy = g_memdup (event, sizeof (ShellEvolutionEvent)); + copy->summary = g_strdup (event->summary); + return copy; +} + +ShellEvolutionEvent * +shell_evolution_event_new (const gchar *summary, + gboolean all_day, + gint64 msec_begin) +{ + ShellEvolutionEvent *event; + event = g_new0 (ShellEvolutionEvent, 1); + event->summary = g_strdup (summary); + event->all_day = all_day; + event->msec_begin = msec_begin; + return event; +} diff --git a/src/shell-evolution-event-source.h b/src/shell-evolution-event-source.h new file mode 100644 index 000000000..4866b4575 --- /dev/null +++ b/src/shell-evolution-event-source.h @@ -0,0 +1,45 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_EVOLUTION_EVENT_SOURCE_H__ +#define __SHELL_EVOLUTION_EVENT_SOURCE_H__ + +#include + +G_BEGIN_DECLS + +typedef struct _ShellEvolutionEvent ShellEvolutionEvent; + +struct _ShellEvolutionEvent +{ + gchar *summary; + gboolean all_day; + gint64 msec_begin; +}; + +GType shell_evolution_event_get_type (void) G_GNUC_CONST; +ShellEvolutionEvent *shell_evolution_event_new (const gchar *summary, + gboolean all_day, + gint64 msec_begin); +ShellEvolutionEvent *shell_evolution_event_copy (ShellEvolutionEvent *event); +void shell_evolution_event_free (ShellEvolutionEvent *event); + +typedef struct _ShellEvolutionEventSource ShellEvolutionEventSource; +typedef struct _ShellEvolutionEventSourceClass ShellEvolutionEventSourceClass; + +#define SHELL_TYPE_EVOLUTION_EVENT_SOURCE (shell_evolution_event_source_get_type ()) +#define SHELL_EVOLUTION_EVENT_SOURCE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_EVOLUTION_EVENT_SOURCE, ShellEvolutionEventSource)) +#define SHELL_EVOLUTION_EVENT_SOURCE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_EVOLUTION_EVENT_SOURCE, ShellEvolutionEventSourceClass)) +#define SHELL_IS_EVOLUTION_EVENT_SOURCE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_EVOLUTION_EVENT_SOURCE)) +#define SHELL_IS_EVOLUTION_EVENT_SOURCE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_EVOLUTION_EVENT_SOURCE)) +#define SHELL_EVOLUTION_EVENT_SOURCE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_EVOLUTION_EVENT_SOURCE, ShellEvolutionEventSourceClass)) + +GType shell_evolution_event_source_get_type (void) G_GNUC_CONST; +ShellEvolutionEventSource *shell_evolution_event_source_new (void); +void shell_evolution_event_source_request_range (ShellEvolutionEventSource *source, + gint64 msec_begin, + gint64 msec_end); +GList *shell_evolution_event_source_get_events (ShellEvolutionEventSource *source, + gint64 msec_begin, + gint64 msec_end); +G_END_DECLS + +#endif /* __SHELL_EVOLUTION_EVENT_SOURCE_H__ */ diff --git a/tools/build/gnome-shell-build-setup.sh b/tools/build/gnome-shell-build-setup.sh index 78e94d018..6f950529f 100755 --- a/tools/build/gnome-shell-build-setup.sh +++ b/tools/build/gnome-shell-build-setup.sh @@ -62,7 +62,8 @@ fi # libxklavier, libxml2, ORBit2, pam, python, readline, # spidermonkey ({mozilla,firefox,xulrunner}-js), startup-notification, # xdamage, icon-naming-utils, upower, libtool-ltdl, libvorbis, -# libgcrypt, libtasn1, libgnome-keyring, libgtop, cups +# libgcrypt, libtasn1, libgnome-keyring, libgtop, cups, +# evolution-data-server # # Non-devel packages needed by gnome-shell and its deps: # glxinfo, gstreamer-plugins-base, gstreamer-plugins-good, @@ -83,7 +84,7 @@ if test "x$system" = xUbuntu -o "x$system" = xDebian -o "x$system" = xLinuxMint xulrunner-dev xserver-xephyr gnome-terminal libcroco3-dev libgstreamer0.10-dev gstreamer0.10-plugins-base gstreamer0.10-plugins-good libltdl-dev libvorbis-dev libxklavier-dev libgnome-keyring-dev - libupower-glib-dev libcups2-dev + libupower-glib-dev libcups2-dev evolution-data-server-dev " if apt-cache show autopoint > /dev/null 2> /dev/null; then @@ -121,7 +122,7 @@ if test "x$system" = xFedora ; then startup-notification-devel xorg-x11-server-Xephyr gnome-terminal zenity icon-naming-utils upower-devel libtool-ltdl-devel libvorbis-devel libxklavier-devel libgcrypt-devel libtasn1-devel libtasn1-tools - libgnome-keyring-devel libgtop2-devel cups-devel + libgnome-keyring-devel libgtop2-devel cups-devel evolution-data-server-devel " if expr $version \>= 14 > /dev/null ; then @@ -147,7 +148,7 @@ if test "x$system" = xSUSE -o "x$system" = "xSUSE LINUX" ; then libgtop-devel libpulse-devel libtiff-devel cups-devel libffi-devel \ orbit2-devel libwnck-devel xorg-x11-proto-devel readline-devel \ mozilla-xulrunner191-devel libcroco-devel \ - xorg-x11-devel xorg-x11 xorg-x11-server-extra \ + xorg-x11-devel xorg-x11 xorg-x11-server-extra evolution-data-server-devel \ ; do if ! rpm -q $pkg > /dev/null 2>&1; then reqd="$pkg $reqd" @@ -168,7 +169,7 @@ if test "x$system" = xMandrivaLinux ; then intltool ffi5-devel libwnck-1-devel GL-devel ORBit2-devel \ readline-devel libxulrunner-devel \ libxdamage-devel mesa-demos x11-server-xephyr zenity \ - libcroco0.6-devel \ + libcroco0.6-devel libevolution-data-server3-devel \ ; do if ! rpm -q --whatprovides $pkg > /dev/null 2>&1; then reqd="$pkg $reqd"