diff --git a/src/Calendar.cpp b/src/Calendar.cpp --- a/src/Calendar.cpp +++ b/src/Calendar.cpp @@ -1,3399 +1,3399 @@ /*hdr ** FILE: Calendar.cpp ** AUTHOR: Matthew Allen ** DATE: 23/11/2001 ** DESCRIPTION: Scribe calender support ** ** Copyright (C) 2001 Matthew Allen ** fret@memecode.com */ #include "Scribe.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/Combo.h" #include "lgi/common/DateTimeCtrls.h" #include "lgi/common/TabView.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Edit.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/Button.h" #include "lgi/common/LgiRes.h" #include "lgi/common/Json.h" #include "CalendarView.h" #include "PrintContext.h" #include "resdefs.h" #include "resource.h" #include "AddressSelect.h" #include "ObjectInspector.h" #define MAX_RECUR 1024 #define DEBUG_REMINDER 0 #define DEBUG_DATES 0 #if DEBUG_DATES #define LOG_DEBUG(...) LgiTrace(__VA_ARGS__) #else #define LOG_DEBUG(...) #endif ////////////////////////////////////////////////////////////////////////////// ItemFieldDef CalendarFields[] = { {"Start", SdStart, GV_DATETIME, FIELD_CAL_START_UTC, IDC_START_DATE, 0, true}, {"End", SdEnd, GV_DATETIME, FIELD_CAL_END_UTC, IDC_END_DATE, 0, true}, {"Subject", SdSubject, GV_STRING, FIELD_CAL_SUBJECT, IDC_SUBJECT}, {"Location", SdLocation, GV_STRING, FIELD_CAL_LOCATION, IDC_LOCATION}, {"Show Time As", SdShowTimeAs, GV_INT32, FIELD_CAL_SHOW_TIME_AS, IDC_AVAILABLE_TYPE}, {"All Day", SdAllDay, GV_BOOL, FIELD_CAL_ALL_DAY, IDC_ALL_DAY}, // TRUE if the calendar event recurs {"Recur", SdRecur, GV_INT32, FIELD_CAL_RECUR, -1}, // Base time unit of recurring event. See enum CalRecurFreq: days, weeks, months, years. {"Recur Freq", SdRecurFreq, GV_INT32, FIELD_CAL_RECUR_FREQ, -1}, // Number of FIELD_CAL_RECUR_FREQ units of time between recurring events. (Minimum is '1') {"Recur Interval", SdRecurInterval, GV_INT32, FIELD_CAL_RECUR_INTERVAL, -1}, // Bitfield of days, Bit 0 = Sunday, Bit 1 = Monday, Bit 2 = Teusday etc. {"Filter Days", SdFilterDays, GV_INT32, FIELD_CAL_RECUR_FILTER_DAYS, -1}, // Bitfield of months, Bit 0 = Janurary, Bit 1 = feburary, Bit 2 = March etc. {"Filter Months", SdFilterMonths, GV_INT32, FIELD_CAL_RECUR_FILTER_MONTHS, -1}, // String of year numbers separated by commas: "2010,2014,2018" {"Filter Years", SdFilterYears, GV_STRING, FIELD_CAL_RECUR_FILTER_YEARS, -1}, // Position in month, "1" means the first matching day of the month. Multiple indexes can be joined with ',' // like: "1,3" {"Filter Pos", SdFilterPos, GV_STRING, FIELD_CAL_RECUR_FILTER_POS, -1}, // See the CalRecurEndType enumeration {"Recur End Type", SdRecurEndType, GV_INT32, FIELD_CAL_RECUR_END_TYPE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnDate then this specifies the date to end {"Recur End Date", SdRecurEndDate, GV_DATETIME, FIELD_CAL_RECUR_END_DATE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnCount then this specifies the count of events {"Recur End Count", SdRecurEndCount, GV_INT32, FIELD_CAL_RECUR_END_COUNT, -1}, // The timezone the start and end are referencing {"Timezone", SdTimeZone, GV_STRING, FIELD_CAL_TIMEZONE, IDC_TIMEZONE}, // User defined notes for the object {"Notes", SdNotes, GV_STRING, FIELD_CAL_NOTES, IDC_DESCRIPTION}, // See the CalendarType enumeration {"Type", SdType, GV_INT32, FIELD_CAL_TYPE, -1}, {"Reminders", SdReminders, GV_STRING, FIELD_CAL_REMINDERS, -1}, {"LastCheck", SdLastCheck, GV_DATETIME, FIELD_CAL_LAST_CHECK, -1}, {"DateModified", SdDateModified, GV_DATETIME, FIELD_DATE_MODIFIED, -1}, {0} }; ////////////////////////////////////////////////////////////////////////////// const char *sReminderType[] = { "Email", "Popup", "ScriptCallback" }; const char *sReminderUnits[] = { "Minutes", "Hours", "Days", "Weeks" }; class ReminderItem : public LListItem { CalendarReminderType Type; float Value; CalendarReminderUnits Units; LString Param; public: ReminderItem(CalendarReminderType type, float value, CalendarReminderUnits units, LString param) { Type = type; Value = value; Units = units; Param = param; Update(); SetText("x", 1); } ReminderItem(LString s) { if (!SetString(s)) { // Default Type = CalPopup; Value = 1.0; Units = CalMinutes; Param.Empty(); } Update(); SetText("x", 1); } CalendarReminderType GetType() { return Type; } float GetValue() { return Value; } CalendarReminderUnits GetUnits() { return Units; } LString GetParam() { return Param; } LString GetString() { // See also FIELD_CAL_REMINDERS LString s; s.Printf("%g,%i,%i,%s", Value, Units, Type, Param?LUrlEncode(Param, ",\r\n").Get():""); return s; } bool SetString(LString s) { // See also FIELD_CAL_REMINDERS LString::Array a = s.Split(","); if (a.Length() < 3) return false; Value = (float) a[0].Float(); Units = (CalendarReminderUnits) a[1].Int(); Type = (CalendarReminderType) a[2].Int(); if (a.Length() > 3) Param = LUrlDecode(a[3]); return true; } void Update() { char s[300]; if (Param) sprintf_s(s, sizeof(s), "%s '%s' @ %g %s", sReminderType[Type], Param.Get(), Value, sReminderUnits[Units]); else sprintf_s(s, sizeof(s), "%s @ %g %s", sReminderType[Type], Value, sReminderUnits[Units]); SetText(s); } void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LColour old = Ctx.Fore; if (i == 1) Ctx.Fore.Rgb(255, 0, 0); LListItem::OnPaintColumn(Ctx, i, c); Ctx.Fore = old; } void OnMouseClick(LMouse &m) { if (m.Down() && m.Left()) { if (GetList()->ColumnAtX(m.x) == 1) { delete this; return; } } LListItem::OnMouseClick(m); } }; //////////////////////////////////////////////////////////////////////////////////// static int DefaultCalenderFields[] = { FIELD_CAL_SUBJECT, FIELD_CAL_START_UTC, FIELD_CAL_END_UTC, 0, }; #define MINUTE 1 // we're in minutes... #define HOUR (60 * MINUTE) #define DAY (24 * HOUR) int ReminderOffsets[] = { 0, 15 * MINUTE, 30 * MINUTE, 1 * HOUR, 2 * HOUR, 3 * HOUR, 4 * HOUR, 5 * HOUR, 6 * HOUR, 7 * HOUR, 8 * HOUR, 9 * HOUR, 10 * HOUR, 11 * HOUR, 12 * HOUR, 1 * DAY, 2 * DAY }; ////////////////////////////////////////////////////////////////////////////// List Calendar::Reminders; int Calendar::DayStart = -1; int Calendar::DayEnd = -1; int Calendar::WorkDayStart = -1; int Calendar::WorkDayEnd = -1; int Calendar::WorkWeekStart = -1; int Calendar::WorkWeekEnd = -1; void InitCalendarView() { Calendar::DayStart = 6; Calendar::DayEnd = 23; Calendar::WorkDayStart = -1; Calendar::WorkDayEnd = -1; Calendar::WorkWeekStart = -1; Calendar::WorkWeekEnd = -1; auto s = LAppInst->GetConfig("Scribe.Calendar.WorkDayStart"); if (s) Calendar::WorkDayStart = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkDayEnd"); if (s) Calendar::WorkDayEnd = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekStart"); if (s) Calendar::WorkWeekStart = (int)s.Int() + 1; s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekEnd"); if (s) Calendar::WorkWeekEnd = (int)s.Int() + 1; if (Calendar::WorkDayStart < 0) Calendar::WorkDayStart = 9; if (Calendar::WorkDayEnd < 0) Calendar::WorkDayEnd = 18; if (Calendar::WorkWeekStart < 0) Calendar::WorkWeekStart = 1; if (Calendar::WorkWeekEnd < 0) Calendar::WorkWeekEnd = 5; } void Calendar::CheckReminders() { LDateTime Now; Now.SetNow(); #if DEBUG_REMINDER // Get all the times for today, including recent ones Now.SetTime("0:0:0"); #endif LDateTime Then = Now; Then.AddDays(1); #if DEBUG_REMINDER LgiTrace("%s:%i - Reminders.Len=%i, Now=%s, Then=%s\n", _FL, Reminders.Length(), Now.Get().Get(), Then.Get().Get()); #endif for (auto c: Reminders) { auto App = c->App; LHashTbl,bool> Added; Thing *ReminderThing = NULL; Mail *ReminderEmail = NULL; // Is a reminder on the entry? if (!c->GetObject()) continue; LString Rem = c->GetObject()->GetStr(FIELD_CAL_REMINDERS); if (!Rem) continue; const char *Subj = NULL, *Notes = NULL; c->GetField(FIELD_CAL_SUBJECT, Subj); c->GetField(FIELD_CAL_NOTES, Notes); LArray Times; if (!c->GetTimes(Now, Then, Times)) { #if DEBUG_REMINDER // LgiTrace(" No times for '%s', now=%s, then=%s\n", Subj, Now.Get().Get(), Then.Get().Get()); #endif continue; } auto Obj = c->GetObject(); if (!Obj) continue; LDateTime LastCheck = *Obj->GetDate(FIELD_CAL_LAST_CHECK); if (LastCheck.IsValid()) { LastCheck.ToLocal(); #if DEBUG_REMINDER // Helps with debugging... LastCheck.AddDays(-1); #endif } LString::Array r = Rem.SplitDelimit("\n"); for (unsigned i=0; i LastCheck; bool b = ts <= Now; LgiTrace(" last check = %s\n", LastCheck.Get().Get()); LgiTrace(" now = %s\n", Now.Get().Get()); LgiTrace(" %i %i\n", a, b); #endif if ( ( !LastCheck.IsValid() || ts > LastCheck ) && ts <= Now ) { // Save the last check field now auto NowUtc = Now.Utc(); Obj->SetDate(FIELD_CAL_LAST_CHECK, &NowUtc); c->SetDirty(); // Fire the event switch (ri.GetType()) { case CalEmail: { ScribeAccount *Acc = App->GetCurrentAccount(); if (!Acc || !Acc->Identity.IsValid()) { LView *Ui = c->GetUI(); LgiMsg(Ui ? Ui : c->App, "No from account to use for sending event notifications.", AppName, MB_OK); break; } auto Email = Acc->Identity.Email(); auto Name = Acc->Identity.Name(); LgiTrace("%s:%i - Using account ('%s' '%s') for the event reminder 'from' address.\n", _FL, Email.Str(), Name.Str()); if (!ReminderThing) ReminderThing = App->CreateThingOfType(MAGIC_MAIL); if (!ReminderEmail) { ReminderEmail = ReminderThing ? ReminderThing->IsMail() : NULL; if (ReminderEmail) { LString s; s.Printf("Calendar Notification: %s", Subj); ReminderEmail->SetSubject(s); s.Printf("The event '%s' is due at: %s\n" "\n" "%s\n" "\n" "(This email was generated by Scribe)", Subj, t.s.Get().Get(), Notes ? Notes : ""); ReminderEmail->SetBody(s); auto Frm = ReminderEmail->GetFrom(); Frm->SetStr(FIELD_EMAIL, Email.Str()); Frm->SetStr(FIELD_NAME, Name.Str()); } } if (ReminderEmail) { auto To = ReminderEmail->GetTo(); // Add any custom parameter email address: LString Param = ri.GetParam(); if (LIsValidEmail(Param)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { Added.Add(Param, true); Recip->SetStr(FIELD_EMAIL, Param); To->Insert(Recip); } } // Add all the guests LString sGuests = c->GetObject()->GetStr(FIELD_TO); LString::Array Guests = sGuests.SplitDelimit(","); for (auto Guest: Guests) { LAutoString e, n; DecodeAddrName(Guest, n, e, NULL); #if DEBUG_REMINDER LgiTrace("Attendee=%s,%s\n", n.Get(), e.Get()); #endif if (LIsValidEmail(e.Get()) && !Added.Find(e)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { if (n) Recip->SetStr(FIELD_NAME, n); Recip->SetStr(FIELD_EMAIL, e); To->Insert(Recip); Added.Add(e, true); } } else { #if DEBUG_REMINDER LgiTrace("Attendee not valid or added already: %s\n", e.Get()); #endif } } } break; } case CalPopup: { if (LgiMsg( 0, // this causes the dialog to be ontop of everything else LLoadString(IDS_EVENT_DUE), AppName, MB_YESNO | MB_SYSTEMMODAL, Subj) == IDYES) { // Open the calendar entry c->DoUI(); } break; } case CalScriptCallback: { break; } default: { LgiTrace("%s:%i - Unknown reminder type.\n", _FL); break; } } } } } } if (ReminderEmail) { ReminderEmail->CreateMailHeaders(); ReminderEmail->Update(); ReminderEmail->Send(true); } } } #define MIN_1 ((int64)LDateTime::Second64Bit * 60) #define HOUR_1 (MIN_1 * 60) #define DAY_1 (HOUR_1 * 24) #define YEAR_1 (DAY_1 * 365) const char *RelativeTime(LDateTime &Then) { static char s[256]; static const int Id[] = { IDS_CAL_LDAY_SUN, IDS_CAL_LDAY_MON, IDS_CAL_LDAY_TUE, IDS_CAL_LDAY_WED, IDS_CAL_LDAY_THU, IDS_CAL_LDAY_FRI, IDS_CAL_LDAY_SAT, }; LDateTime Now; Now.SetNow(); char Val[64]; LTimeStamp n, t; Now.Get(n); Then.Get(t); auto Diff = t - n; int Yrs = 0; int Months = 0; int Days = 0; int Hrs = 0; int Mins = 0; LDateTime i = Now; int Inc = Then > Now ? 1 : -1; char DirIndcator = Then > Now ? '+' : '-'; while (ABS(Diff) > YEAR_1) { Yrs++; i.Year(i.Year()+Inc); if (!i.IsValid()) break; i.Get(n); Diff = t - n; } int TotalDays = 0; if (ABS(Diff) > DAY_1) { TotalDays = Days = (int) (Diff / DAY_1); while (true) { LDateTime first = i; first.AddMonths(Inc); if ( (Inc < 0 && first > Then) // Tracking back in time.. || (Inc > 0 && Then > first) // Forward in time.. ) { Months += Inc; i = first; } else break; } if (Months) { LTimeStamp remaining; i.Get(remaining); Diff = t - remaining; Days = (int) (Diff / DAY_1); } Diff -= (int64) Days * DAY_1; } if (ABS(Diff) > HOUR_1) { Hrs = (int) (Diff / HOUR_1); Diff -= (int64) Hrs * HOUR_1; } if (ABS(Diff) > MIN_1) { Mins = (int) (Diff / MIN_1); Diff -= (int64) Mins * MIN_1; } if (Yrs) { // Years + months sprintf_s(Val, sizeof(Val), "%c%iy %im", DirIndcator, abs(Yrs), abs(Months)); } else if (Months) { // Months + days sprintf_s(Val, sizeof(Val), "%c%im %id", DirIndcator, abs(Months), abs(Days)); } else if (Days) { if (abs(Days) >= 7) { // Weeks + days... sprintf_s(Val, sizeof(Val), "%c%iw %id", DirIndcator, abs(Days)/7, abs(Days)%7); } else { // Days + hours... sprintf_s(Val, sizeof(Val), "%c%id %ih", DirIndcator, abs(Days), abs(Hrs)); } } else if (Hrs) { // Hours + min sprintf_s(Val, sizeof(Val), "%c%ih %im", DirIndcator, abs(Hrs), abs(Mins)); } else { // Mins sprintf_s(Val, sizeof(Val), "%c%im", DirIndcator, abs(Mins)); } if (Yrs != 0 || Months != 0) { sprintf_s(s, sizeof(s), "%s", Val); return s; } auto NowDay = n.Get() / DAY_1; auto ThenDay = t.Get() / DAY_1; auto DaysDiff = (int64_t)ThenDay - (int64_t)NowDay; int Ch = 0; if (NowDay == ThenDay) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TODAY)); else if (DaysDiff == -1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_YESTERDAY)); else if (DaysDiff == 1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TOMORROW)); else if (DaysDiff > 1 && DaysDiff < 7) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_THIS_WEEK), LLoadString(Id[Then.DayOfWeek()])); else if (DaysDiff >= 7 && DaysDiff < 14) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_NEXT_WEEK), LLoadString(Id[Then.DayOfWeek()])); else { sprintf_s(s, sizeof(s), "%s", Val); return s; } sprintf_s(s+Ch, sizeof(s)-Ch, ", %s", Val); return s; } int CalSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } CalendarSourceGetEvents *Calendar::GetEvents = NULL; void Calendar::SummaryOfToday(ScribeWnd *App, std::function Callback) { LDateTime Now; Now.SetNow(); LDateTime Next = Now; Next.AddMonths(1); LArray Sources; if (!App || !Callback || !App->GetCalendarSources(Sources)) return; if (GetEvents) return; new CalendarSourceGetEvents( App, &GetEvents, Now, Next, Sources, [Callback](auto e) { if (!e.Length()) { char s[256]; sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_NO_EVENTS)); Callback(s); } else { e.Sort(CalSorter); LStringPipe p; p.Print("\n"); for (unsigned n=0; nGetField(FIELD_CAL_SUBJECT, Subject); LColour Base32 = c->GetColour(); auto Edge = Base32.Mix(LColour(L_WORKSPACE), 0.85f); sprintf_s(Back, sizeof(Back), "#%2.2x%2.2x%2.2x", Edge.r(), Edge.g(), Edge.b()); sprintf_s(Fore, sizeof(Fore), "#%2.2x%2.2x%2.2x", Base32.r(), Base32.g(), Base32.b()); p.Print("\t
\n" "\t\t%s\n", Fore, Back, Fore, Fore, (Thing*)c, Subject); char Str[256]; const char *Rel = RelativeTime(tp.s); if (Rel) { p.Print("\t\t
%s\n", Rel); } tp.s.Get(Str, sizeof(Str)); p.Print("\t\t
%s\n", Str); tp.e.Get(Str, sizeof(Str)); p.Print("\t\t-
%s\n", Str); } p.Print("
\n"); Callback(p.NewLStr()); } }); } void Calendar::OnSerialize(bool Write) { // Update reminder list.. Reminders.Delete(this); const char *Rem = NULL; if (GetField(FIELD_CAL_REMINDERS, Rem) && ValidStr(Rem)) { Reminders.Insert(this); } SetImage(GetCalType() == CalTodo ? ICON_TODO : ICON_CALENDAR); if (Write && TodoView) { TodoView->Update(); TodoView->Resort(); } } ////////////////////////////////////////////////////////////////////////////// void TimePeriod::Set(CalendarSource *source, Calendar *cal, LDateTime start, LDateTime end) { src = source; c = cal; s = start; e = end; ToLocal(); } LString TimePeriod::ToString() { LString str; auto subj = c->GetObject()->GetStr(FIELD_CAL_SUBJECT); str.Printf("TimePeriod(%p, %s, %s, %s, %s)", c, subj, src ? src->ToString().Get() : NULL, s.Get().Get(), e.Get().Get()); return str; } ////////////////////////////////////////////////////////////////////////////// Calendar::Calendar(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); SetImage(ICON_CALENDAR); } Calendar::~Calendar() { CalendarView::OnDelete(this); DeleteObj(TodoView); Reminders.Delete(this); } bool Calendar::GetTimes(LDateTime StartLocal, LDateTime EndLocal, LArray &Times) { if (Calendar::DayStart < 0) InitCalendarView(); ssize_t StartLen = Times.Length(); LDateTime StartUtc = StartLocal; LDateTime EndUtc = EndLocal; StartUtc.ToUtc(); EndUtc.ToUtc(); TimePeriod w; w.s = StartUtc; w.e = EndUtc; const char *Subj = NULL; GetField(FIELD_CAL_SUBJECT, Subj); TimePeriod BaseUtc; LArray Periods; if (!GetField(FIELD_CAL_START_UTC, BaseUtc.s)) return false; if (!GetField(FIELD_CAL_END_UTC, BaseUtc.e)) { BaseUtc.e = BaseUtc.s; BaseUtc.e.AddHours(1); } if (BaseUtc.s > EndUtc) return true; LArray Dst; LDateTime::GetDaylightSavingsInfo(Dst, BaseUtc.s, &EndUtc); if (Dst.Length() < 2) { LgiTrace("%s:%i - GetDaylightSavingsInfo(%s, %s)\n", _FL, BaseUtc.s.Get().Get(), EndUtc.Get().Get()); LAssert(!"Need 2 dst points."); } Periods.Add(BaseUtc); LDateTime BaseS = BaseUtc.s; LDateTime::DstToLocal(Dst, BaseS); auto BaseTz = BaseS.GetTimeZone(); int AllDay = false; GetField(FIELD_CAL_ALL_DAY, AllDay); // Process recur rules int Recur = 0; if (GetField(FIELD_CAL_RECUR, Recur) && Recur) { LDateTime Diff = BaseUtc.e - BaseUtc.s; int FilterFreq = -1; int FilterInterval = 0; int FilterDay = 0; int FilterMonth = 0; int EndType = 0; int EndCount = 0; const char *FilterYear = NULL; const char *FilterPos = NULL; LDateTime EndDate; GetField(FIELD_CAL_RECUR_FREQ, FilterFreq); GetField(FIELD_CAL_RECUR_INTERVAL, FilterInterval); GetField(FIELD_CAL_RECUR_FILTER_DAYS, FilterDay); GetField(FIELD_CAL_RECUR_FILTER_MONTHS, FilterMonth); GetField(FIELD_CAL_RECUR_FILTER_YEARS, FilterYear); GetField(FIELD_CAL_RECUR_FILTER_POS, FilterPos); GetField(FIELD_CAL_RECUR_END_TYPE, EndType); GetField(FIELD_CAL_RECUR_END_DATE, EndDate); GetField(FIELD_CAL_RECUR_END_COUNT, EndCount); LDateTime CurUtc = BaseUtc.s; const char *Error = NULL; int Count = 0; while (!Error) { LAssert(CurUtc.GetTimeZone() == 0); // Advance the current date by interval * freq switch (FilterFreq) { case CalFreqDays: CurUtc.AddDays(FilterInterval); break; case CalFreqWeeks: CurUtc.AddDays(FilterInterval * 7); break; case CalFreqMonths: CurUtc.AddMonths(FilterInterval); break; case CalFreqYears: CurUtc.Year(CurUtc.Year() + FilterInterval); break; default: Error = "Invalid freq."; break; } LAssert(CurUtc.GetTimeZone() == 0); if (Error || CurUtc > EndUtc) break; // Check against end conditions bool IsEnded = CurUtc > EndUtc; switch (EndType) { case CalEndNever: break; case CalEndOnCount: // count IsEnded = Count >= EndCount - 1; break; case CalEndOnDate: // date IsEnded = CurUtc > EndDate; break; default: // error IsEnded = true; break; } if (IsEnded) break; // Check against filters LAssert(CurUtc.GetTimeZone() == 0); LDateTime CurLocal = CurUtc; LDateTime::DstToLocal(Dst, CurLocal); // This fixes the current time when it's in a different daylight saves zone. // Otherwise you get events one hour or whatever out of position after DST starts // or ends during the recurring set. int DiffMins = BaseTz - CurLocal.GetTimeZone(); CurLocal.AddMinutes(DiffMins); bool Show = true; if (FilterDay) { int Day = CurLocal.DayOfWeek(); for (int i=0; i<7; i++) { int Bit = 1 << i; if (Day == i && (FilterDay & Bit) == 0) { Show = false; break; } } } if (Show && FilterMonth) { for (int i=0; i<12; i++) { int Bit = 1 << i; if ((CurLocal.Month() == i + 1) && (FilterMonth & Bit) == 0) { Show = false; break; } } } if (Show && ValidStr(FilterYear)) { auto t = LString(FilterYear).SplitDelimit(" ,;:"); Show = false; for (unsigned i=0; i= MAX_RECUR) break; TimePeriod &p = Periods.New(); p.s = CurLocal; p.e = CurLocal; p.e.AddHours(Diff.Hours()); p.e.AddMinutes(Diff.Minutes()); } Count++; } } // Now process periods into 1 per day segments if needed for (unsigned k=0; k Calendar::WorkDayEnd) { t.e = t.e.EndOfDay(); } else { t.e.Hours(Calendar::WorkDayEnd); t.e.Minutes(0); t.e.Seconds(0); } if (t.Overlap(w)) { t.c = this; Times.Add(t); } } else if (i.IsSameDay(n.e)) { // End day TimePeriod t; t.s = n.e; t.e = n.e; int h = t.s.Hours(); if (h < Calendar::WorkDayStart) t.s.Hours(0); else t.s.Hours(Calendar::WorkDayStart); t.s.Minutes(0); t.s.Seconds(0); if (t.Overlap(w)) { t.c = this; Times.Add(t); } break; } else { // Middle day TimePeriod t; t.s = i.StartOfDay(); t.s.Hours(Calendar::WorkDayStart); t.e = i.StartOfDay(); t.e.Hours(Calendar::WorkDayEnd); if (t.Overlap(w)) { t.c = this; Times.Add(t); } } } } else if (n.Overlap(w)) { n.c = this; Times.Add(n); } } return (int)Times.Length() > StartLen; } CalendarType Calendar::GetCalType() { CalendarType Type = CalEvent; GetField(FIELD_CAL_TYPE, (int&)Type); return Type; } void Calendar::SetCalType(CalendarType Type) { SetField(FIELD_CAL_TYPE, (int)Type); SetImage(Type == CalTodo ? ICON_TODO : ICON_CALENDAR); } LString Calendar::ToString() { LString s; auto obj = GetObject(); s.Printf("Calendar(%s,%s,%s)", obj ? obj->GetStr(FIELD_CAL_SUBJECT) : "#NoObject", obj ? obj->GetDate(FIELD_CAL_START_UTC)->Get().Get() : NULL, obj ? obj->GetDate(FIELD_CAL_END_UTC)->Get().Get() : NULL); return s; } LColour Calendar::GetColour() { if (GetObject()) { int64 c = GetObject()->GetInt(FIELD_COLOUR); if (c >= 0) return LColour((uint32_t)c, 32); } if (Source) return Source->GetColour(); return LColour(L_LOW); } void Calendar::OnPaintView(LSurface *pDC, LFont *Font, LRect *Pos, TimePeriod *Period) { LRect p = *Pos; const char *Title = "..."; LDateTime Now; float Sx = 1.0; float Sy = 1.0; if (pDC->IsPrint()) { auto DcDpi = pDC->GetDpi(); auto SrcDpi = LScreenDpi(); Sx = (float)DcDpi.x / SrcDpi.x; Sy = (float)DcDpi.y / SrcDpi.y; } float Scale = Sx < Sy ? Sx : Sy; Now.SetNow(); GetField(FIELD_CAL_SUBJECT, Title); bool Delayed = GetObject() ? GetObject()->GetInt(FIELD_STATUS) == Store3Delayed : false; LColour Grey(192, 192, 192); auto View = GetView(); if (View && View->Selection.HasItem(this)) { // selected LColour f = L_FOCUS_SEL_FORE; LColour b = L_FOCUS_SEL_BACK; if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(f, b); } else { LColour Text(0x80, 0x80, 0x80); LColour Ws(L_WORKSPACE); auto Base = GetColour(); auto Qtr = Ws.Mix(Base, 0.1f); auto Half = Ws.Mix(Base, 0.4f); // not selected LColour f, b; if (Period && Now < Period->s) { // future entry (full colour) f = Base; b = Half; } else { // historical entry (half strength colour) f = Half; b = Qtr; } if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(Text, b); } pDC->Rectangle(&p); p.Inset((int)SX(1), (int)SY(1)); Font->Transparent(false); LDisplayString ds(Font, Title); float Ht = p.Y() > 0 ? (float)ds.Y() / p.Y() : 1.0f; if (Ht < 0.75f) p.Inset((int)SX(3), (int)SY(3)); else if (Ht < 0.95f) p.Inset((int)SX(1), (int)SY(1)); ds.Draw(pDC, p.x1, p.y1, &p); ViewPos.Union(Pos); } Thing &Calendar::operator =(Thing &Obj) { if (Obj.GetObject() && GetObject()) { GetObject()->CopyProps(*Obj.GetObject()); } return *this; } bool Calendar::operator ==(Thing &t) { Calendar *c = t.IsCalendar(); if (!c) return false; { LDateTime a, b; if (GetField(FIELD_CAL_START_UTC, a) && c->GetField(FIELD_CAL_START_UTC, b) && a != b) return false; if (GetField(FIELD_CAL_END_UTC, a) && c->GetField(FIELD_CAL_END_UTC, b) && a != b) return false; } { const char *a, *b; if (GetField(FIELD_CAL_SUBJECT, a) && c->GetField(FIELD_CAL_SUBJECT, b) && Stricmp(a, b)) return false; if (GetField(FIELD_CAL_LOCATION, a) && c->GetField(FIELD_CAL_LOCATION, b) && Stricmp(a, b)) return false; } return true; } int Calendar::Compare(LListItem *Arg, ssize_t FieldId) { Calendar *c1 = this; Calendar *c2 = dynamic_cast(Arg); if (c1 && c2) { switch (FieldId) { case FIELD_CAL_START_UTC: case FIELD_CAL_END_UTC: { LDateTime d1, d2; if (!c1->GetField((int)FieldId, d1)) d1.SetNow(); if (!c2->GetField((int)FieldId, d2)) d2.SetNow(); return d1.Compare(&d2); break; } case FIELD_CAL_SUBJECT: case FIELD_CAL_LOCATION: { const char *s1 = "", *s2 = ""; if (c1->GetField((int)FieldId, s1) && c2->GetField((int)FieldId, s2)) { return _stricmp(s1, s2); } break; } } } return 0; } uint32_t Calendar::GetFlags() { return 0; } ThingUi *Calendar::DoUI(MailContainer *c) { if (!Ui) { Ui = new CalendarUi(this); } return Ui; } ThingUi *Calendar::GetUI() { return Ui; } bool Calendar::SetUI(ThingUi *ui) { if (Ui) Ui->Item = NULL; Ui = dynamic_cast(ui); if (Ui) Ui->Item = this; return true; } void Calendar::OnMouseClick(LMouse &m) { if (m.Down()) { if (m.Left()) { if (m.Double()) { DoUI(); } } else if (m.Right()) { auto View = GetView(); DoContextMenu(m, View ? (LView*)View : (LView*)App); } } } void Calendar::OnCreate() { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } bool Calendar::OnDelete() { bool IsTodo = GetCalType() == CalTodo; bool Status = Thing::OnDelete(); if (IsTodo) DeleteObj(TodoView); for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); return Status; } bool Calendar::GetParentSelection(LList *Lst, List &s) { if (Lst) { return Lst->GetSelection(s); } if (auto View = GetView()) { LArray &a = View->GetSelection(); for (unsigned i=0; i(a[i])); } return true; } return false; } #define IDM_MOVE_TO 4000 void Calendar::DoContextMenu(LMouse &m, LView *Parent) { LSubMenu Sub; LScriptUi s(&Sub); if (s.Sub) { s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_INSPECT), IDM_INSPECT); CalendarView *Cv = dynamic_cast(Parent); if (Cv) { s.Sub->AppendSeparator(); for (unsigned n = 0; nGetName() && Cs->IsWritable()) { sprintf_s(m, sizeof(m), "Move to '%s'\n", Cs->GetName()); s.Sub->AppendItem(m, IDM_MOVE_TO + n, Source != Cs); } } } LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (unsigned i=0; iExecuteScriptCallback(*Callbacks[i], Args); Args.DeleteObjects(); } m.ToScreen(); int Result = s.Sub->Float(App, m.x, m.y); switch (Result) { default: { if (Cv) { int Idx = Result - IDM_MOVE_TO; if (Idx >= 0 && Idx < (int)CalendarSource::GetSources().Length()) { auto Dst = CalendarSource::GetSources().ItemAt(Idx); if (!Dst) { LAssert(!"No dst?"); return; } auto Fsrc = dynamic_cast(Dst); if (!Fsrc) { LAssert(!"No cal src?"); break; } auto Path = Fsrc->GetPath(); auto DstFolder = App->GetFolder(Path); if (!DstFolder) { LAssert(!"Path doesn't exist?"); return; } LArray Items{ this }; DstFolder->MoveTo( Items, false, [this, Cv, Dst](auto result, auto itemStatus) { if (result) { Source = Dst; Cv->Invalidate(); } } ); return; } } // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } break; } case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(Parent, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *PList = dynamic_cast(Parent); if (GetParentSelection(PList ? PList : GetList(), Del)) { for (auto i: Del) { Thing *t = dynamic_cast(i); if (t) { t->OnDelete(); } else { CalendarTodoItem *Todo = dynamic_cast(i); if (Todo) { Calendar *c = Todo->GetTodo(); c->OnDelete(); } } } } else { Thing *t = this; t->OnDelete(); } } break; } case IDM_EXPORT: { ExportAll(GetList(), sMimeVCalendar, NULL); break; } case IDM_INSPECT: { new ObjectInspector(App, this); break; } } } } const char *Calendar::GetText(int i) { static char s[64]; if (!GetObject()) { LAssert(!"No storage object"); return 0; } int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int) FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultCalenderFields)) { Field = DefaultCalenderFields[i]; } if (!Field) return 0; ItemFieldDef *Def = GetFieldDefById(Field); if (!Def) { LAssert(!"Where is the field def?"); return 0; } switch (Def->Type) { case GV_STRING: { return GetObject()->GetStr(Field); break; } case GV_INT32: { sprintf_s(s, sizeof(s), LPrintfInt64, GetObject()->GetInt(Field)); return s; break; } case GV_DATETIME: { // Get any effective timezone for this event. LString Tz = GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto dt = GetObject()->GetDate(Field); if (dt && dt->IsValid()) { LDateTime tmp = *dt; LOG_DEBUG("%s:%i - GetText.UTC %i = %s\n", _FL, i, tmp.Get().Get()); bool UseLocal = true; tmp.SetTimeZone(0, false); if (Tz.Get()) { bool HasPt = false, HasDigit = false; char *e = Tz.Get(); while (strchr(" \t\r\n-.+", *e) || IsDigit(*e)) { if (*e == '.') HasPt = true; if (IsDigit(*e)) HasDigit = true; e++; } if (HasDigit) { if (HasPt) { double TzHrs = Tz.Float(); double i, f = modf(TzHrs, &i); int Mins = (int) ((i * 60) + (f * 60)); tmp.AddMinutes(Mins); } else { int64 i = Tz.Int(); int a = (int)ABS(i); int Mins = (int) (((a / 100) * 60) + (a % 100)); tmp.AddMinutes(i < 0 ? -Mins : Mins); } UseLocal = false; } } if (UseLocal) tmp.ToLocal(); LOG_DEBUG("%s:%i - GetText.Local %i = %s\n", _FL, i, tmp.Get().Get()); tmp.Get(s, sizeof(s)); return s; } break; } default: { LAssert(0); break; } } return 0; } int *Calendar::GetDefaultFields() { return DefaultCalenderFields; } const char *Calendar::GetFieldText(int Field) { return 0; } bool Calendar::Overlap(Calendar *c) { if (c) { LDateTime Ts, Te, Cs, Ce; if (GetField(FIELD_CAL_START_UTC, Ts) && c->GetField(FIELD_CAL_START_UTC, Cs)) { if (!GetField(FIELD_CAL_END_UTC, Te)) { Te = Ts; Te.AddHours(1); } if (!c->GetField(FIELD_CAL_END_UTC, Ce)) { Ce = Cs; Ce.AddHours(1); } if ((Ce <= Ts) || (Cs >= Te)) { return false; } return true; } } return false; } bool Calendar::Save(ScribeFolder *Folder) { bool Status = false; // Check the dates are the right way around LDateTime Start, End; if (GetField(FIELD_CAL_START_UTC, Start) && GetField(FIELD_CAL_END_UTC, End)) { if (End < Start) { SetField(FIELD_CAL_START_UTC, End); SetField(FIELD_CAL_END_UTC, Start); } } auto ChangeEvent = [this](bool Status) { auto View = GetView(); if (View && Status) { View->OnContentsChanged(Source); OnSerialize(true); } }; if (!Folder) Folder = GetFolder(); if (!Folder && App) Folder = App->GetFolder(FOLDER_CALENDAR); if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Webdav) { auto ParentObj = Folder ? Folder->GetObject() : NULL; Store3Status s = GetObject()->Save(ParentObj); Status = s > Store3Error; if (Status) SetDirty(false); ChangeEvent(Status); } else { // FIXME: This can't wait for WriteThing to finish it's call back... Status = true; if (Folder) { LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow().ToUtc()); Folder->WriteThing(this, [this, ChangeEvent](auto Status) { if (Status > Store3Error) SetDirty(false); ChangeEvent(Status); }); } else ChangeEvent(Status); } return Status; } // Import/Export bool Calendar::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeVCalendar); return MimeTypes.Length() > 0; } Thing::IoProgress Calendar::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar) && Stricmp(mimeType, sMimeICalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Import(GetObject(), stream)) IoProgressError("vCal import failed."); IoProgressSuccess(); } Thing::IoProgress Calendar::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Export(GetObject(), stream)) IoProgressError("vCal export failed."); IoProgressSuccess(); } char *Calendar::GetDropFileName() { if (!DropFileName) { const char *Name = 0; GetField(FIELD_CAL_SUBJECT, Name); DropFileName.Reset(MakeFileName(Name ? Name : "Cal", "ics")); } return DropFileName; } bool Calendar::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); Export(AutoCast(F), sMimeVCalendar); } } if (LFileExists(DropFileName)) { Files.Add(DropFileName.Get()); Status = true; } } return Status; } void Calendar::OnPrintHeaders(ScribePrintContext &Context) { LDisplayString *ds = Context.Text(LLoadString(IDS_CAL_EVENT)); LRect &r = Context.MarginPx; int Line = ds->Y(); LDrawListSurface *Page = Context.Pages.Last(); Page->Rectangle(r.x1, Context.CurrentY + (Line * 5 / 10), r.x2, Context.CurrentY + (Line * 6 / 10)); Context.CurrentY += Line; } void Calendar::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Print document for (ItemFieldDef *Fld = CalendarFields; Fld->FieldId; Fld++) { LString Value; switch (Fld->Type) { case GV_STRING: { Value = GetObject()->GetStr(Fld->FieldId); break; } case GV_DATETIME: { auto Dt = GetObject()->GetDate(Fld->FieldId); if (Dt) { char s[64] = ""; Dt->Get(s, sizeof(s)); Value = s; } break; } default: break; } if (Value) { LString f; const char *Name = LLoadString(Fld->FieldId); f.Printf("%s: %s", Name ? Name : Fld->DisplayText, Value.Get()); // LDisplayString *ds = Context.Text(f); } } } bool Calendar::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: // Type: String Value = GetObject()->GetStr(FIELD_CAL_SUBJECT); break; case SdTo: // Type: String Value = GetObject()->GetStr(FIELD_TO); break; case SdLocation: // Type: String Value = GetObject()->GetStr(FIELD_CAL_LOCATION); break; case SdUid: // Type: String Value = GetObject()->GetStr(FIELD_UID); break; case SdReminders: // Type: String Value = GetObject()->GetStr(FIELD_CAL_REMINDERS); break; case SdNotes: // Type: String Value = GetObject()->GetStr(FIELD_CAL_NOTES); break; case SdStatus: // Type: String Value = GetObject()->GetStr(FIELD_CAL_STATUS); break; // Int variables case SdType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_TYPE); break; case SdCompleted: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_COMPLETED); break; case SdShowTimeAs: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_SHOW_TIME_AS); break; case SdRecur: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR); break; case SdRecurFreq: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FREQ); break; case SdRecurInterval: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_INTERVAL); break; case SdRecurEndCount: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_COUNT); break; case SdRecurEndType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_TYPE); break; case SdRecurFilterDays: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); break; case SdRecurFilterMonths: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_MONTHS); break; case SdPrivacy: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_PRIVACY); break; case SdAllDay: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_ALL_DAY); break; case SdColour: // Type: Int64 Value = GetObject()->GetInt(FIELD_COLOUR); break; // Date time fields case SdStart: // Type: DateTime return GetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: // Type: DateTime return GetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: // Type: DateTime return GetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: Value = GetObject()->SetStr(FIELD_CAL_SUBJECT, Value.Str()); break; case SdTo: Value = GetObject()->SetStr(FIELD_TO, Value.Str()); break; case SdLocation: Value = GetObject()->SetStr(FIELD_CAL_LOCATION, Value.Str()); break; case SdUid: Value = GetObject()->SetStr(FIELD_UID, Value.Str()); break; case SdReminders: Value = GetObject()->SetStr(FIELD_CAL_REMINDERS, Value.Str()); break; case SdNotes: Value = GetObject()->SetStr(FIELD_CAL_NOTES, Value.Str()); break; case SdStatus: Value = GetObject()->SetStr(FIELD_CAL_STATUS, Value.Str()); break; // Int variables case SdType: Value = GetObject()->SetInt(FIELD_CAL_TYPE, Value.CastInt32()); break; case SdCompleted: Value = GetObject()->SetInt(FIELD_CAL_COMPLETED, Value.CastInt32()); break; case SdShowTimeAs: Value = GetObject()->SetInt(FIELD_CAL_SHOW_TIME_AS, Value.CastInt32()); break; case SdRecur: Value = GetObject()->SetInt(FIELD_CAL_RECUR, Value.CastInt32()); break; case SdRecurFreq: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FREQ, Value.CastInt32()); break; case SdRecurInterval: Value = GetObject()->SetInt(FIELD_CAL_RECUR_INTERVAL, Value.CastInt32()); break; case SdRecurEndCount: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_COUNT, Value.CastInt32()); break; case SdRecurEndType: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_TYPE, Value.CastInt32()); break; case SdRecurFilterDays: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, Value.CastInt32()); break; case SdRecurFilterMonths: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, Value.CastInt32()); break; case SdPrivacy: Value = GetObject()->SetInt(FIELD_CAL_PRIVACY, Value.CastInt32()); break; case SdColour: Value = GetObject()->SetInt(FIELD_COLOUR, Value.CastInt32()); break; case SdAllDay: Value = GetObject()->SetInt(FIELD_CAL_ALL_DAY, Value.CastInt32()); break; // Date time fields case SdStart: return SetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: return SetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::CallMethod(const char *MethodName, LScriptArguments &Args) { return Thing::CallMethod(MethodName, Args); } ////////////////////////////////////////////////////////////////////////////// bool SerializeUi(ItemFieldDef *Defs, LDataI *Object, LViewI *View, bool ToUi) { if (!Object || !Defs || !View) return false; if (ToUi) { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { auto s = Object->GetStr(d->FieldId); View->SetCtrlName(d->CtrlId, s); break; } case GV_INT32: { int64 i = Object->GetInt(d->FieldId); View->SetCtrlValue(d->CtrlId, i); break; } case GV_DATETIME: { char s[64] = ""; auto dt = Object->GetDate(d->FieldId); if (dt && dt->Year()) dt->Get(s, sizeof(s)); View->SetCtrlName(d->CtrlId, s); break; } default: { LAssert(0); break; } } } } else { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { const char *s = View->GetCtrlName(d->CtrlId); Object->SetStr(d->FieldId, s); break; } case GV_INT32: { int i = (int)View->GetCtrlValue(d->CtrlId); Object->SetInt(d->FieldId, i); break; } case GV_DATETIME: { const char *s = View->GetCtrlName(d->CtrlId); LDateTime dt; dt.Set(s); Object->SetDate(d->FieldId, &dt); break; } default: { LAssert(0); break; } } } } return true; } ////////////////////////////////////////////////////////////////////////////// class LEditDropDown : public LEdit { public: enum DropType { DropNone, DropDate, DropTime, }; protected: DropType Type; LAutoPtr Popup; public: LEditDropDown(int id) : LEdit(id, 0, 0, 60, 20, NULL) { Type = DropNone; SetObjectName(Res_Custom); } void SetType(DropType type) { Type = type; } const char *GetClass() { return "LEditDropDown"; } void OnMouseClick(LMouse &m) { LEdit::OnMouseClick(m); if (Focus() && Popup && !Popup->Visible()) { Popup->Visible(true); } } void OnFocus(bool f) { if (f) { if (!Popup) { if (Type == DropDate) Popup.Reset(new LDatePopup(this)); else if (Type == DropTime) Popup.Reset(new LTimePopup(this)); if (Popup) { Popup->TakeFocus(false); LPoint p(0, Y()); PointToScreen(p); LRect r = Popup->GetPos(); r.Offset(p.x - r.x1, p.y - r.y1); Popup->SetPos(r); } } if (Popup) Popup->Visible(true); } } int OnNotify(LViewI *Wnd, LNotification n) { /* LgiTrace("OnNotify %s, %i\n", Wnd->GetClass(), Flags); if (Wnd == (LViewI*)Popup) { GDatePopup *Date; if ((Date = dynamic_cast(Popup.Get()))) { LDateTime Ts = Date->Get(); char s[256]; Ts.GetDate(s, sizeof(s)); Name(s); } else LgiTrace("%s:%i - Incorrect pop up type.\n", _FL); } */ return 0; } void OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)Popup.Get() && !Attaching) { // This gets called in the destructor of the Popup, so we should // lose the pointer to it. Popup.Release(); } } }; struct LEditDropDownFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "LEditDropDown")) { return new LEditDropDown(-1); } return NULL; } } EditDropDownFactory; ////////////////////////////////////////////////////////////////////////////// class LRecurDlg : public LDialog { CalendarUi *Ui = NULL; LEditDropDown *EndOnDate = NULL; LCombo *Repeats = NULL; bool AcceptNotify = true; public: LRecurDlg(CalendarUi *ui) { Ui = ui; SetParent(ui); if (LoadFromResource(IDD_CAL_RECUR)) { if (GetViewById(IDC_ON_DATE, EndOnDate)) { EndOnDate->SetType(LEditDropDown::DropDate); LDateTime dt; dt.SetNow(); char s[64]; dt.GetDate(s, sizeof(s)); EndOnDate->Name(s); } if (GetViewById(IDC_REPEATS, Repeats)) { Repeats->Insert(LLoadString(IDS_DAY)); Repeats->Insert(LLoadString(IDS_WEEK)); Repeats->Insert(LLoadString(IDS_MONTH)); Repeats->Insert(LLoadString(IDS_YEAR)); Repeats->Value(1); } Radio(IDC_NEVER); SetCtrlValue(IDC_EVERY, 1); Serialize(false); } } ~LRecurDlg() { } void Serialize(bool Write) { int DayCtrls[] = { IDC_SUNDAY, IDC_MONDAY, IDC_TUESDAY, IDC_WEDNESDAY, IDC_THURSDAY, IDC_FRIDAY, IDC_SATURDAY }; Calendar *c = Ui->GetCal(); LDataI *o = c->GetObject(); if (Write) { // Dlg -> Object int64 v = Repeats->Value(); o->SetInt(FIELD_CAL_RECUR_FREQ, v); v = GetCtrlValue(IDC_EVERY); o->SetInt(FIELD_CAL_RECUR_INTERVAL, v); int DayFilter = 0; for (int i=0; i<7; i++) { if (GetCtrlValue(DayCtrls[i])) DayFilter |= 1 << i; } o->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, DayFilter); if (GetCtrlValue(IDC_NEVER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndNever); } else if (GetCtrlValue(IDC_AFTER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnCount); o->SetInt(FIELD_CAL_RECUR_END_COUNT, GetCtrlValue(IDC_AFTER_COUNT)); } else if (GetCtrlValue(IDC_ON)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnDate); LDateTime e; e.SetDate(GetCtrlName(IDC_ON_DATE)); e.SetTime("11:59:59"); o->SetDate(FIELD_CAL_RECUR_END_DATE, &e); } else LAssert(0); } else { // Object -> Dlg int64 v = o->GetInt(FIELD_CAL_RECUR_FREQ); Repeats->Value(v); OnRepeat(v); v = o->GetInt(FIELD_CAL_RECUR_INTERVAL); SetCtrlValue(IDC_EVERY, MAX(v, 1)); int DayFilter = (int)o->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); for (int i=0; i<7; i++) { SetCtrlValue(DayCtrls[i], (DayFilter & (1 << i)) ? 1 : 0); } CalRecurEndType EndType = (CalRecurEndType) o->GetInt(FIELD_CAL_RECUR_END_TYPE); if (EndType == CalEndOnCount) { Radio(IDC_AFTER); SetCtrlValue(IDC_AFTER_COUNT, o->GetInt(FIELD_CAL_RECUR_END_COUNT)); } else if (EndType == CalEndOnDate) { Radio(IDC_ON); auto e = o->GetDate(FIELD_CAL_RECUR_END_DATE); if (e) SetCtrlName(IDC_ON_DATE, e->GetDate()); } else // Default to never... { Radio(IDC_NEVER); } } } void Radio(int Id) { AcceptNotify = false; SetCtrlValue(IDC_NEVER, Id == IDC_NEVER); SetCtrlValue(IDC_AFTER, Id == IDC_AFTER); SetCtrlEnabled(IDC_AFTER_COUNT, Id == IDC_AFTER); SetCtrlEnabled(IDC_OCCURRENCES, Id == IDC_AFTER); SetCtrlValue(IDC_ON, Id == IDC_ON); SetCtrlEnabled(IDC_ON_DATE, Id == IDC_ON); AcceptNotify = true; } void OnRepeat(int64 Val) { LViewI *v; if (!GetViewById(IDC_TIME_TYPE, v)) return; switch (Val) { case 0: v->Name(LLoadString(IDS_DAYS)); break; case 1: v->Name(LLoadString(IDS_WEEKS)); break; case 2: v->Name(LLoadString(IDS_MONTHS)); break; case 3: v->Name(LLoadString(IDS_YEARS)); break; } v->SendNotify(LNotifyTableLayoutRefresh); } int OnNotify(LViewI *Ctrl, LNotification n) { if (!AcceptNotify) return 0; switch (Ctrl->GetId()) { case IDC_NEVER: case IDC_AFTER: case IDC_ON: Radio(Ctrl->GetId()); break; case IDC_REPEATS: OnRepeat(Ctrl->Value()); break; case IDOK: Serialize(true); // Fall through case IDCANCEL: EndModal(Ctrl->GetId() == IDOK); break; } return 0; } }; /////////////////////////////////////////////////////////////////////////////// struct CalendarUiPriv { CalendarUi *Ui; LEditDropDown *StartDate, *StartTime; LEditDropDown *EndDate, *EndTime; LEdit *Entry; AddressBrowse *Browse; LList *Guests; LList *Reminders; LCombo *ReminderType; LCombo *ReminderUnit; LCombo *CalSelect; LColourSelect *Colour; CalendarUiPriv(CalendarUi *ui) { Ui = ui; StartDate = StartTime = NULL; EndDate = EndTime = NULL; CalSelect = NULL; Entry = NULL; Browse = NULL; Guests = NULL; Colour = NULL; Reminders = NULL; ReminderType = NULL; ReminderUnit = NULL; } void OnGuest() { const char *g = Ui->GetCtrlName(IDC_GUEST_ENTRY); if (!g) return; Mailto mt(Ui->App, g); for (auto a: mt.To) { ListAddr *la = new ListAddr(Ui->App, a); if (la) { Guests->Insert(la); } } Ui->SetCtrlName(IDC_GUEST_ENTRY, ""); } }; CalendarUi::CalendarUi(Calendar *item) : ThingUi(item, LLoadString(IDS_CAL_EVENT)) { NotifyOn = false; Item = item; d = new CalendarUiPriv(this); #if WINNATIVE CreateClassW32("Event", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_EVENT))); #endif if (!Attach(NULL)) LAssert(0); else { LoadFromResource(IDD_CAL, this); AttachChildren(); if (!SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, true)) { LRect p(100, 100, 900, 700); SetPos(p); MoveToCenter(); } if (GetViewById(IDC_START_DATE, d->StartDate)) { d->StartDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_START_TIME, d->StartTime)) { d->StartTime->SetType(LEditDropDown::DropTime); } if (GetViewById(IDC_END_DATE, d->EndDate)) { d->EndDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_END_TIME, d->EndTime)) { d->EndTime->SetType(LEditDropDown::DropTime); } GetViewById(IDC_GUEST_ENTRY, d->Entry); if (GetViewById(IDC_GUESTS, d->Guests)) { d->Guests->AddColumn(LLoadString(IDS_ADDRESS), 120); d->Guests->AddColumn(LLoadString(IDS_NAME), 120); d->Guests->ColumnHeaders(false); } if (GetViewById(IDC_REMINDERS, d->Reminders)) { d->Reminders->AddColumn(LLoadString(FIELD_CAL_REMINDER_TIME), 200); d->Reminders->AddColumn(LLoadString(IDC_DELETE), 20); d->Reminders->ColumnHeaders(false); } if (GetViewById(IDC_REMINDER_TYPE, d->ReminderType)) { d->ReminderType->Insert(LLoadString(IDS_EMAIL)); d->ReminderType->Insert(LLoadString(IDS_POPUP)); d->ReminderType->Insert("Script"); d->ReminderType->Value(1); } SetCtrlValue(IDC_REMINDER_VALUE, 10); if (GetViewById(IDC_REMINDER_UNIT, d->ReminderUnit)) { d->ReminderUnit->Insert(LLoadString(IDS_MINUTES)); d->ReminderUnit->Insert(LLoadString(IDS_HOURS)); d->ReminderUnit->Insert(LLoadString(IDS_DAYS)); d->ReminderUnit->Insert(LLoadString(IDS_WEEKS)); d->ReminderUnit->Value(0); } SetCtrlValue(IDC_AVAILABLE_TYPE, 1); if (GetViewById(IDC_COLOUR, d->Colour)) { LArray Colours; Colours.Add(LColour(84, 132, 237, 255)); Colours.Add(LColour(164, 189, 252, 255)); Colours.Add(LColour(70, 214, 219, 255)); Colours.Add(LColour(122, 231, 191, 255)); Colours.Add(LColour(81, 183, 73, 255)); Colours.Add(LColour(251, 215, 91, 255)); Colours.Add(LColour(255, 184, 120, 255)); Colours.Add(LColour(255, 136, 124, 255)); Colours.Add(LColour(220, 33, 39, 255)); Colours.Add(LColour(219, 173, 255, 255)); Colours.Add(LColour(225, 225, 225, 255)); d->Colour->SetColourList(&Colours); d->Colour->Value(0); } if (GetViewById(IDC_CALENDAR, d->CalSelect)) { LArray Sources; if (Item->App->GetCalendarSources(Sources)) { for (unsigned i=0; iCalSelect->Insert(cs->GetName()); } } } LView *s; if (GetViewById(IDC_SUBJECT, s)) s->Focus(true); LButton *btn; if (GetViewById(IDC_SAVE, btn)) btn->Default(true); OnLoad(); Visible(true); NotifyOn = true; } LViewI *v; if (GetViewById(IDC_REMINDER_TYPE, v)) { LNotification note; OnNotify(v, note); } RegisterHook(this, LKeyEvents); } CalendarUi::~CalendarUi() { if (Item) { if (Item->App) SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, false); Item->SetUI(NULL); } } bool CalendarUi::OnViewKey(LView *v, LKey &k) { THREAD_UNSAFE(false); if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } if (k.vkey == LK_RETURN) { if (!k.Down()) { OnSave(); Quit(); } return true; } } return false; } int CalendarUi::OnCommand(int Cmd, int Event, OsView Window) { THREAD_UNSAFE(0); return 0; } void CalendarUi::OnPosChange() { THREAD_UNSAFE(); LWindow::OnPosChange(); } void CalendarUi::CheckConsistancy() { THREAD_UNSAFE(); auto St = CurrentStart(); auto En = CurrentEnd(); if (En < St) { En = St; En.AddMinutes(30); SetCtrlName(IDC_END_DATE, En.GetDate().Get()); SetCtrlName(IDC_END_TIME, En.GetTime().Get()); } } LDateTime CalendarUi::CurrentStart() { LDateTime dt; THREAD_UNSAFE(dt); dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(GetCtrlName(IDC_START_TIME)); return dt; } LDateTime CalendarUi::CurrentEnd() { LDateTime dt; THREAD_UNSAFE(dt); dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(GetCtrlName(IDC_END_TIME)); return dt; } void CalendarUi::UpdateStartRelative() { THREAD_UNSAFE(); LDateTime dt = CurrentStart(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_START_REL, s); } } void CalendarUi::UpdateEndRelative() { THREAD_UNSAFE(); LDateTime dt = CurrentEnd(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_END_REL, s); } } void CalendarUi::UpdateRelative() { THREAD_UNSAFE(); CheckConsistancy(); UpdateStartRelative(); UpdateEndRelative(); } int CalendarUi::OnNotify(LViewI *Ctrl, LNotification n) { THREAD_UNSAFE(0); if (!NotifyOn) return false; switch (Ctrl->GetId()) { case IDC_START_DATE: case IDC_START_TIME: { CheckConsistancy(); UpdateStartRelative(); break; } case IDC_END_DATE: case IDC_END_TIME: { CheckConsistancy(); UpdateEndRelative(); break; } case IDC_REMINDER_TYPE: { LViewI *Label, *Param; if (GetViewById(IDC_PARAM_LABEL, Label) && GetViewById(IDC_REMINDER_PARAM, Param)) { Param->Name(NULL); switch (Ctrl->Value()) { case CalEmail: Label->Name("optional email:"); Param->Enabled(true); break; default: case CalPopup: Label->Name(NULL); Param->Enabled(false); break; case CalScriptCallback: Label->Name("callback fn:"); Param->Enabled(true); break; } } break; } case IDC_ALL_DAY: { int64 AllDay = Ctrl->Value(); LDataI *o = Item->GetObject(); char s[256] = ""; auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_START_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_END_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_END_TIME, !AllDay); } break; } case IDC_SUBJECT: { SetCtrlEnabled(IDC_SAVE, ValidStr(Ctrl->Name())); break; } case IDC_SHOW_LOCATION: { LString Loc = GetCtrlName(IDC_LOCATION); if (ValidStr(Loc)) { for (char *c = Loc; *c; c++) { if (Strchr(LWhiteSpace, *c)) *c = '+'; } LString Uri; Uri.Printf("http://maps.google.com/?q=%s", Loc.Get()); LExecute(Uri); } break; } case IDC_GUESTS: { if (n.Type == LNotifyItemInsert) { d->Guests->ResizeColumnsToContent(); } else if (n.Type == LNotifyDeleteKey) { List la; d->Guests->GetSelection(la); la.DeleteObjects(); } break; } case IDC_GUEST_ENTRY: { if (d->Entry) { if (ValidStr(d->Entry->Name())) { if (!d->Browse) { d->Browse = new AddressBrowse(Item->App, d->Entry, d->Guests, NULL); } } if (d->Browse) { d->Browse->OnNotify(d->Entry, n); } if (n.Type == LNotifyReturnKey) { d->OnGuest(); } } break; } case IDC_REPEAT: { if (!Ctrl->Value()) break; auto Dlg = new LRecurDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (!ctrlId) SetCtrlValue(IDC_REPEAT, false); delete dlg; }); break; } case IDC_TIMEZONE: { auto Tz = Item->GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto Dlg = new LInput(this, Tz, "Time zone:", "Calendar Event Timezone"); Dlg->DoModal([this, Dlg](auto dlg, auto Result) { if (Result) { Item->GetObject()->SetStr(FIELD_CAL_TIMEZONE, Dlg->GetStr()); Item->SetDirty(); } delete dlg; }); break; } case IDC_REMINDER_ADD: { const char *v = GetCtrlName(IDC_REMINDER_VALUE); const char *param = GetCtrlName(IDC_REMINDER_PARAM); d->Reminders->Insert ( new ReminderItem ( (CalendarReminderType) d->ReminderType->Value(), v ? (float)atof(v) : 1.0f, (CalendarReminderUnits) d->ReminderUnit->Value(), param ) ); d->Reminders->ResizeColumnsToContent(); break; } case IDC_ADD_GUEST: { d->OnGuest(); break; } case IDC_SAVE: { LViewI *v; if (GetViewById(IDC_GUEST_ENTRY, v) && v->Focus()) { d->OnGuest(); break; } else { OnSave(); // Fall through } } case IDCANCEL: { if (Item) { // Is the user canceling an object that hasn't been saved yet? // If so delete the object. auto obj = Item->GetObject(); if (obj && obj->IsOrphan()) { Item->DecRef(); Item = NULL; } } Quit(); break; } } return 0; } void CalendarUi::OnLoad() { THREAD_UNSAFE(); // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Copy object values into UI int64 AllDay = false; auto CalSubject = o->GetStr(FIELD_CAL_SUBJECT); SetCtrlName(IDC_SUBJECT, CalSubject); SetCtrlName(IDC_LOCATION, o->GetStr(FIELD_CAL_LOCATION)); SetCtrlName(IDC_DESCRIPTION, o->GetStr(FIELD_CAL_NOTES)); SetCtrlValue(IDC_ALL_DAY, AllDay = o->GetInt(FIELD_CAL_ALL_DAY)); if (d->Guests) { LJson j(o->GetStr(FIELD_ATTENDEE_JSON)); for (auto g: j.GetArray(LString())) { LAutoPtr la(new ListAddr(App)); if (la) { la->sAddr = g.Get("email"); la->sName = g.Get("name"); d->Guests->Insert(la.Release()); } } d->Guests->ResizeColumnsToContent(); } if (d->Reminders) { d->Reminders->Empty(); LString Rem = o->GetStr(FIELD_CAL_REMINDERS); LString::Array a = Rem.SplitDelimit("\n"); for (unsigned i=0; iSetString(a[i])) d->Reminders->Insert(ri); else delete ri; } } d->Reminders->ResizeColumnsToContent(); } CalendarShowTimeAs Show = (CalendarShowTimeAs)o->GetInt(FIELD_CAL_SHOW_TIME_AS); switch (Show) { case CalFree: SetCtrlValue(IDC_AVAILABLE_TYPE, 0); break; case CalTentative: SetCtrlValue(IDC_AVAILABLE_TYPE, 1); break; default: case CalBusy: SetCtrlValue(IDC_AVAILABLE_TYPE, 2); break; } CalendarPrivacyType Priv = (CalendarPrivacyType)o->GetInt(FIELD_CAL_PRIVACY); switch (Priv) { default: SetCtrlValue(IDC_PRIVACY_TYPE, 0); break; case CalPublic: SetCtrlValue(IDC_PRIVACY_TYPE, 1); break; case CalPrivate: SetCtrlValue(IDC_PRIVACY_TYPE, 2); break; } int64 Col = o->GetInt(FIELD_COLOUR); if (Col >= 0) d->Colour->Value(Col); for (unsigned i=0; iGetName(); LString i_path; if (Item->GetFolder()) i_path = Item->GetFolder()->GetPath(); if (s_path && i_path && _stricmp(s_path, i_path) == 0) { SetCtrlValue(IDC_CALENDAR, i); break; } } char s[256] = ""; auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt) { LOG_DEBUG("%s:%i - Load.Start.UTC=%s\n", _FL, dt->Get().Get()); LDateTime tmp = *dt; tmp.SetTimeZone(0, false); tmp.ToLocal(true); LOG_DEBUG("%s:%i - Load.Start.Local=%s\n", _FL, tmp.Get().Get()); tmp.GetDate(s, sizeof(s)); SetCtrlName(IDC_START_DATE, s); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_START_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt) { LOG_DEBUG("%s:%i - Load.End.UTC=%s\n", _FL, dt->Get().Get()); LDateTime tmp = *dt; tmp.SetTimeZone(0, false); tmp.ToLocal(true); LOG_DEBUG("%s:%i - Load.End.Local=%s\n", _FL, tmp.Get().Get()); tmp.GetDate(s, sizeof(s)); SetCtrlName(IDC_END_DATE, s); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_END_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_END_TIME, !AllDay); } SetCtrlValue(IDC_REPEAT, o->GetInt(FIELD_CAL_RECUR)); LViewI *ctrl; if (GetViewById(IDC_SUBJECT, ctrl)) { LNotification note(LNotifyValueChanged); OnNotify(ctrl, note); } if (ValidStr(CalSubject)) { LString s; s.Printf("%s - %s", LLoadString(IDS_CAL_EVENT), CalSubject); Name(s); } UpdateRelative(); } void CalendarUi::OnSave() { THREAD_UNSAFE(); // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Save UI values into object bool AllDay = false; o->SetStr(FIELD_CAL_SUBJECT, GetCtrlName(IDC_SUBJECT)); o->SetStr(FIELD_CAL_LOCATION, GetCtrlName(IDC_LOCATION)); o->SetStr(FIELD_CAL_NOTES, GetCtrlName(IDC_DESCRIPTION)); o->SetInt(FIELD_CAL_ALL_DAY, AllDay = (GetCtrlValue(IDC_ALL_DAY) != 0)); if (d->Guests) { List All; if (d->Guests->GetAll(All)) { LString::Array a; LString Sep(", "); for (auto la: All) { LString e; if (la->Serialize(e, true)) a.Add(e); } LString Guests = Sep.Join(a); o->SetStr(FIELD_TO, Guests); } } if (d->Reminders) { List All; if (d->Reminders->GetAll(All)) { LString::Array a; LString Sep("\n"); for (auto ri: All) { a.Add(ri->GetString()); } LString Reminders = Sep.Join(a); o->SetStr(FIELD_CAL_REMINDERS, Reminders); } else { o->SetStr(FIELD_CAL_REMINDERS, ""); } } int64 Show = GetCtrlValue(IDC_AVAILABLE_TYPE); switch (Show) { case 0: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalFree); break; case 1: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalTentative); break; default: case 2: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalBusy); break; } int64 Priv = GetCtrlValue(IDC_PRIVACY_TYPE); switch (Priv) { default: case 0: o->SetInt(FIELD_CAL_PRIVACY, CalDefaultPriv); break; case 1: o->SetInt(FIELD_CAL_PRIVACY, CalPublic); break; case 2: o->SetInt(FIELD_CAL_PRIVACY, CalPrivate); break; } auto Col = d->Colour->Value(); o->SetInt(FIELD_COLOUR, Col > 0 ? Col : -1); /* FIXME: change calendar item location if the user selects a different cal for (unsigned i=0; iSources.Length(); i++) { CalendarSource *src = d->Sources[i]; LAutoString s_path(src->GetPath()); LAutoString i_path; if (Item->GetFolder()) i_path = Item->GetFolder()->GetPath(); if (s_path && i_path && _stricmp(s_path, i_path) == 0) { SetCtrlValue(IDC_CALENDAR, i); break; } } */ LDateTime dt; dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(AllDay ? "0:0:0" : GetCtrlName(IDC_START_TIME)); LOG_DEBUG("%s:%i - Start.Local=%s\n", _FL, dt.Get().Get()); dt.ToUtc(true); LOG_DEBUG("%s:%i - Start.UTC=%s\n", _FL, dt.Get().Get()); o->SetDate(FIELD_CAL_START_UTC, &dt); dt.Empty(); dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(AllDay ? "11:59:59" : GetCtrlName(IDC_END_TIME)); LOG_DEBUG("%s:%i - End.Local=%s\n", _FL, dt.Get().Get()); dt.ToUtc(true); LOG_DEBUG("%s:%i - End.UTC=%s\n", _FL, dt.Get().Get()); o->SetDate(FIELD_CAL_END_UTC, &dt); o->SetInt(FIELD_CAL_RECUR, GetCtrlValue(IDC_REPEAT)); Item->Update(); Item->Save(); } ////////////////////////////////////////////////////////////// int LDateTimeViewBase = 1000; class LDateTimeView : public LLayout, public ResObject { LEdit *Edit; LDateDropDown *Date; LTimeDropDown *Time; public: LDateTimeView() : ResObject(Res_Custom) { AddView(Edit = new LEdit(10, 0, 0, 60, 20, NULL)); AddView(Date = new LDateDropDown()); AddView(Time = new LTimeDropDown()); Edit->SetId(LDateTimeViewBase++); Date->SetNotify(Edit); Date->SetId(LDateTimeViewBase++); Time->SetNotify(Edit); Time->SetId(LDateTimeViewBase++); } bool OnLayout(LViewLayoutInfo &Inf) { if (!Inf.Width.Max) { Inf.Width.Max = 220; Inf.Width.Min = 150; } else if (!Inf.Height.Max) { Inf.Height.Max = Inf.Height.Min = LSysFont->GetHeight() + 6; } else return false; return true; } void OnCreate() { AttachChildren(); } void OnPosChange() { LRect c = GetClient(); // int cy = c.Y(); // int py = Y(); LRect tc = c; tc.x1 = tc.x2 - 20; LRect dc = c; dc.x2 = tc.x1 - 1; dc.x1 = dc.x2 - 20; Date->SetPos(dc); Time->SetPos(tc); c.x2 = dc.x1 - 1; Edit->SetPos(c); } const char *Name() { return Edit->Name(); } bool Name(const char *s) { return Edit->Name(s); } }; class LDateTimeViewFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "LDateTimeView")) return new LDateTimeView; return NULL; } public: } DateTimeViewFactory; diff --git a/src/Resource.rc b/src/Resource.rc --- a/src/Resource.rc +++ b/src/Resource.rc @@ -1,139 +1,139 @@ // Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_APP ICON "..\\Resources\\Icons\\app.ico" IDI_MAIL ICON "..\\Resources\\Icons\\mail.ico" IDI_BLANK ICON "..\\Resources\\Icons\\blank.ico" IDI_SMALL ICON "..\\Resources\\Icons\\small.ico" IDI_CONTACT ICON "..\\Resources\\Icons\\contact.ico" IDI_FILTER ICON "..\\Resources\\Icons\\filter.ico" IDI_CALENDER ICON "..\\Resources\\Icons\\calender.ico" IDI_EVENT ICON "..\\Resources\\Icons\\event.ico" #endif // English (U.S.) resources ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// // English (Australia) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS #pragma code_page(1252) #endif //_WIN32 ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_ERR ICON "..\\Resources\\Icons\\error.ico" #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Version // VS_VERSION_INFO VERSIONINFO - FILEVERSION 3,9,328 - PRODUCTVERSION 3,9,328 + FILEVERSION 3,9,334 + PRODUCTVERSION 3,9,334 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L #else FILEFLAGS 0x0L #endif FILEOS 0x40004L FILETYPE 0x1L FILESUBTYPE 0x0L BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "0c0904b0" BEGIN VALUE "CompanyName", "Memecode" VALUE "FileDescription", "Scribe" - VALUE "FileVersion", "3,9,328\0" + VALUE "FileVersion", "3,9,334\0" VALUE "InternalName", "Scribe" VALUE "LegalCopyright", "Copyright © 2016" VALUE "OriginalFilename", "Scribe.exe" VALUE "ProductName", "i.Scribe\0" - VALUE "ProductVersion", "3,9,328\0" + VALUE "ProductVersion", "3,9,334\0" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0xc09, 1200 END END #endif // English (Australia) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED diff --git a/src/Store3Imap/ScribeImap_Mail.cpp b/src/Store3Imap/ScribeImap_Mail.cpp --- a/src/Store3Imap/ScribeImap_Mail.cpp +++ b/src/Store3Imap/ScribeImap_Mail.cpp @@ -1,1507 +1,1498 @@ #include "lgi/common/Lgi.h" #include "lgi/common/LgiRes.h" #include "lgi/common/TextConvert.h" #include "ScribeImap.h" #include "ScribeUtils.h" #include "resdefs.h" #if IMAP_PROTOBUF #include "include/Scribe.pb.cc" #endif const char *ToString(ImapMail::ImapMailState s) { switch (s) { case ImapMail::ImapMailIdle: return "ImapMailIdle"; case ImapMail::ImapMailGettingBody: return "ImapMailGettingBody"; case ImapMail::ImapMailMoving: return "ImapMailMoving"; case ImapMail::ImapMailDeleting: return "ImapMailDeleting"; } return "#errInvalidImapMailState"; } ImapMail::ImapMail(ImapStore *store, const char *file, int line, uint32_t uid) : AllocFile(file), AllocLine(line), From(store), Reply(store) { LocalFlags = 0; Seg = 0; State = ImapMailIdle; Loaded = Store3Unloaded; Parent = 0; SegDirty = false; RemoteFlags.ImapSeen = true; Priority = MAIL_PRIORITY_NORMAL; Store = store; Uid = uid; } ImapMail::~ImapMail() { if (Parent) { Parent->DelMail(this); } DeleteObj(Seg); To.DeleteObjects(); } void ImapMail::SetRemoteFlags(ImapMailFlags f) { RemoteFlags = f; // Replicate to local flags as well... if (f.ImapSeen) LocalFlags |= MAIL_READ; else LocalFlags &= ~MAIL_READ; if (f.ImapAnswered) LocalFlags |= MAIL_REPLIED; else LocalFlags &= ~MAIL_REPLIED; } ImapMail::IMeta ImapMail::GetMeta(bool AllowCreate) { if (Data) return Data->GetMeta(Uid, AllowCreate); if (Parent) return Parent->GetMeta(Uid, AllowCreate); return NULL; } void ImapMail::Serialize(IMeta m, bool Write) { LAssert(m != NULL); if (!m) return; #if IMAP_PROTOBUF if (Write) { } else { } #else if (Write) { // Mail -> Meta m->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); if (DateSent.IsValid()) { char c[32]; DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.ToUtc(); DateSent.Get(c, sizeof(c)); DateSent.SetFormat(DateSent.GetDefaultFormat()); ValidateImapDate(DateSent); m->SetAttr(ATTR_DATE, c); } m->SetAttr(ATTR_LABEL, Label); m->SetAttr(ATTR_COLOUR, (int64)Colour.c32()); char *l = Path ? strrchr(Path, DIR_CHAR) : 0; if (l) m->SetContent(l + 1); if (ValidStr(Subject)) m->SetAttr(ATTR_SUBJECT, Subject); else m->DelAttr(ATTR_SUBJECT); LVariant v; if (From.GetVariant("Text", v)) m->SetAttr(ATTR_FROM, v.Str()); if (Reply.GetVariant("Text", v)) m->SetAttr(ATTR_REPLYTO, v.Str()); Parent->SetDirty(); } else { // Meta -> Mail auto Flags = m->GetAttr(ATTR_FLAGS); if (Flags) RemoteFlags.Set(Flags); char *sLocal = m->GetAttr(ATTR_LOCAL); if (sLocal) LocalFlags = atoi(sLocal); else if (Flags) // Synthesis from remote flags LocalFlags = (RemoteFlags.ImapAnswered ? MAIL_REPLIED : 0) | (RemoteFlags.ImapSeen ? MAIL_READ : 0); char *s = m->GetAttr(ATTR_DATE); DateSent.SetFormat(GDTF_YEAR_MONTH_DAY); DateSent.SetTimeZone(0, false); DateSent.Set(s); DateSent.SetFormat(DateSent.GetDefaultFormat()); // Hmmmm: still don't know where these are coming from if (DateSent.Year() == 2000 && DateSent.Hours() == 0) { DateSent.Empty(); if (Parent && !Parent->IsLoading()) Parent->SetDirty(); } DateReceived = DateSent; Label = m->GetAttr(ATTR_LABEL); char *c = m->GetAttr(ATTR_COLOUR); if (c) Colour.Set((uint32_t)atoi64(c), 32); else Colour.Empty(); Subject = m->GetAttr(ATTR_SUBJECT); LVariant FromText = m->GetAttr(ATTR_FROM); #if 1 From.SetVariant("Text", FromText); #else char *sFrom = FromText.Str(); if (sFrom) { LRange Nm(-1), Em(-1); for (char *c = sFrom; *c; c++) { if (*c == '\"') if (Nm.Start < 0) Nm.Start = c - sFrom + 1; else Nm.Len = c - sFrom - Nm.Start; else if (*c == '<') Em.Start = c - sFrom + 1; else if (*c == '>') Em.Len = c - sFrom - Em.Start; } if (Nm.Len) { if (Nm.Start >= 0) From.Name.Set(sFrom + Nm.Start, Nm.Len); else LAssert(0); } if (Em.Len) { if (Em.Start >= 0) From.Addr.Set(sFrom + Em.Start, Em.Len); else LAssert(0); } } #endif if (Parent && m->GetContent()) { // Trip whitespace of the end of the filename... char *e = m->GetContent() + strlen(m->GetContent()); while (e > m->GetContent() && strchr(" \t\r\n", e[-1])) e--; *e = 0; char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Parent->Local, m->GetContent()); Path = p; } } #endif } void ImapMail::SetState(ImapMailState s, const char *file, int line) { // LgiTrace("%s:%i - %i::SetState(%s)\n", file, line, Uid, ToString(s)); State = s; } void ImapMail::Load() { if (Loaded == Store3Unloaded) { LFile f; if (LFileExists(Path) && f.Open(Path, O_READ)) { Loaded = Store3Headers; LArray Buf; int Chunk = 2 << 10; while (!HeaderCache) { Buf.Length(Buf.Length() + Chunk); f.Read(&Buf[Buf.Length()-Chunk], Chunk); auto e = Strnstr(&Buf[0], "\r\n\r\n", Buf.Length()); if (e) { ssize_t HeaderSize = e - &Buf[0]; HeaderCache.Reset(NewStr(&Buf[0], HeaderSize)); break; } if ((int64)Buf.Length() >= f.GetSize()) break; } LAutoString Type(InetGetHeaderField(HeaderCache, "Content-Type")); if (Type) { if (_strnicmp(Type, "multipart/", 10) == 0 && _strnicmp(Type, sMultipartAlternative, 21) != 0) { SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); } } } } } Store3Status ImapMail::SetRfc822(LStreamI *m) { DeleteObj(Seg); if (!Stream.Reset(new LTempStream(ScribeTempPath()))) { LgiTrace("%s:%i - Alloc failed.\n", _FL); return Store3Error; } LCopyStreamer Cp; if (!Cp.Copy(m, Stream)) { LgiTrace("%s:%i - LCopyStreamer copy failed.\n", _FL); return Store3Error; } Loaded = Store3Loaded; return Store3Success; } #define DEBUG_READ_MIME 0 void ImapMail::ReadMime(IMeta MetaTag) { #if DEBUG_READ_MIME LProfile Prof("ReadMime"); #endif if (Loaded == Store3Loaded || !Uid) return; auto t = MetaTag ? MetaTag : GetMeta(); if (!t) { LAssert(!"No Meta"); return; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif // bool exists = LFileExists(Path); // LgiTrace("ReadMime Path=%s exists=%i\n", Path.Get(), exists); if (!LFileExists(Path)) { if (State != ImapMailGettingBody) { // bool InParent = Parent && Parent->GetMail(Uid); // We don't have a local copy of the message, tell the thread to fetch it... auto Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { #if DEBUG_READ_MIME Prof.Add(_FL); #endif ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); #else Info.Size = t->GetAsInt(ATTR_SIZE); #endif Info.Uid = Uid; Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, true)) { SetState(ImapMailGettingBody, _FL); Loaded = Store3Loading; } else LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { // Load the rest of the mime parts Loaded = Store3Loaded; #if DEBUG_READ_MIME Prof.Add(_FL); #endif LStreamI *Load = NULL; LFile f; if (Stream) { Load = Stream; } else if (f.Open(Path, O_READ)) { Load = &f; } #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Load) { if (Load->GetSize() < 8) { // Hmmm, really? Try and re-download the mail auto Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { Loaded = Store3Headers; #if DEBUG_READ_MIME Prof.Add(_FL); #endif auto &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t->size(); Info.Uid = t->uid(); #else Info.Size = t->GetAsInt(ATTR_SIZE); Info.Uid = t->GetAsInt(ATTR_UID); #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); Store->PostThread(Msg, true); } } else { LAutoPtr Mime(new LMime); if (Mime) { uint64 Start = LCurrentTime(); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Mime->Text.Decode.Pull(Load); DeleteObj(Seg); #if DEBUG_READ_MIME Prof.Add(_FL); #endif Seg = new ImapAttachment(Store, this, Mime.Release()); #if DEBUG_READ_MIME Prof.Add(_FL); #endif if (Seg->IsMixed()) SetInt(FIELD_FLAGS, LocalFlags | MAIL_ATTACHMENTS); uint64 Dur = LCurrentTime() - Start; if (Dur > 100) LgiTrace("%s:%i - MimeDecode: " LPrintfInt64 "\n", _FL, Dur); } } } } } bool ImapMail::FindSegs(const char *Type, LArray &Segs) { if (!Seg) ReadMime(NULL); if (Seg) { Seg->FindSegs(Type, Segs); for (unsigned i=0; iGetSeg()->LGetFileName(); if (Fn) Segs.DeleteAt(i--, true); } } return Segs.Length() > 0; } static auto DefaultCharset = "windows-1252"; const char *ImapMail::GetStr(int id) { if (State != ImapMailIdle && id != FIELD_INTERNET_HEADER && id != FIELD_MESSAGE_ID && id != FIELD_SERVER_UID) { if (id != FIELD_SUBJECT) return NULL; // LgiTrace("%s:%i - State=%i, Uid=%i(0x%x)\n", _FL, State, Uid, Uid); if (State == ImapMailGettingBody) { static const char *Loading = NULL; if (!Loading) Loading = LLoadString(IDS_LOADING); return Loading; } else if (State == ImapMailDeleting) { static const char *Deleting = NULL; if (!Deleting) Deleting = LLoadString(IDS_DELETING); return Deleting; } else if (State == ImapMailMoving) { return (char*)"Moving..."; } return NULL; } switch (id) { case FIELD_DEBUG: { static char s[64]; sprintf_s(s, sizeof(s), "Imap.ServerUid=%i", Uid); return s; } case FIELD_INTERNET_HEADER: { if (Seg && Seg->GetSeg()) return Seg->GetSeg()->GetHeaders(); if (Path) { if (LFileExists(Path)) { Load(); } else if (State == ImapMailIdle) { // Request the headers from the worker thread... auto Msg = new ImapMsg(IMAP_DOWNLOAD, _FL); if (Msg) { auto t = GetMeta(); ImapMailInfo &Info = Msg->Mail.New(); #if IMAP_PROTOBUF Info.Size = t ? t->size() : -1; Info.Uid = t ? t->uid() : 0; #else Info.Size = t ? t->GetAsInt(ATTR_SIZE) : -1; Info.Uid = Uid; #endif Info.Local = Path.Get(); Msg->Parent = Parent->Remote.Get(); if (Store->PostThread(Msg, false)) { SetState(ImapMailGettingBody, _FL); Loaded = Store3Loading; } else { LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } } return HeaderCache; } case FIELD_MESSAGE_ID: { if (!MsgId) { auto m = GetMeta(); if (m) #if IMAP_PROTOBUF MsgId = m->messageid().c_str(); #else MsgId = m->GetAttr(ATTR_MSGID); #endif if (!MsgId) { auto Headers = GetStr(FIELD_INTERNET_HEADER); auto MsgIdHdr = LGetHeaderField(Headers, "Message-ID"); MsgId = LDecodeRfc2047(MsgIdHdr).Replace(" ", "").Replace("\n", ""); if (MsgId) { auto hasNewLine = strchr(MsgId, '\n'); LAssert(!hasNewLine); if (m) { #if IMAP_PROTOBUF m->set_messageid(MsgId); #else m->SetAttr(ATTR_MSGID, MsgId); #endif Parent->SetDirty(); } } } } return MsgId; } case FIELD_SUBJECT: { if (!Subject) { auto Headers = GetStr(FIELD_INTERNET_HEADER); if (!Headers) return LLoadString(IDS_LOADING); auto enc = LGetHeaderField(Headers, "Subject"); // LgiTrace("EncSubj: %s\n", enc.Get()); Subject = LDecodeRfc2047(enc); // LgiTrace("DecSubj: %s\n", Subject.Get()); auto Meta = GetMeta(); if (Meta) Serialize(Meta, true); } return Subject; } case FIELD_CHARSET: { // Conversion is done locally. return "utf-8"; } case FIELD_TEXT: { LArray Results; if (!TextCache && FindSegs("text/plain", Results)) { LStringPipe p; for (auto Segment: Results) { if (Segment->GetInt(FIELD_SIZE) > (512 << 10)) continue; LAutoStreamI Stream = Segment->GetStream(_FL); if (Stream) { LAutoString Buf; auto Size = Stream->GetSize(); if (Size > 0) { Buf.Reset(new char[Size+1]); auto rd = Stream->Read(Buf, Size); if (rd <= 0) continue; Buf.Get()[rd] = 0; LString Charset = Segment->GetStr(FIELD_CHARSET); if (!Charset) Charset = DetectCharset(Buf.Get()); if (!Charset) Charset = DefaultCharset; LAutoString Utf8; if (!Stricmp(Charset.Get(), "utf-8") && LIsUtf8(Buf)) Utf8 = Buf; else Utf8.Reset((char*)LNewConvertCp("utf-8", Buf, Charset, rd)); if (Results.Length() > 1) { p.Push(Utf8); } else { TextCache = Utf8; return TextCache; } } } } TextCache.Reset(p.NewStr()); } return TextCache; break; } case FIELD_HTML_CHARSET: { LArray Results; if (FindSegs("text/html", Results)) return Results[0]->GetStr(FIELD_CHARSET); break; } case FIELD_ALTERNATE_HTML: { LArray Results; if (!HtmlCache && FindSegs("text/html", Results)) { LAutoStreamI s = Results[0]->GetStream(_FL); if (s) { auto Size = s->GetSize(); if (Size > 0) { HtmlCache.Reset(new char[Size+1]); s->Read(HtmlCache, Size); HtmlCache[Size] = 0; } } } return HtmlCache; break; } case FIELD_CACHE_FLAGS: { static char s[96]; int ch = 0; if (RemoteFlags.ImapAnswered) ch += sprintf_s(s+ch, sizeof(s)-ch, "Answ "); if (RemoteFlags.ImapDeleted) ch += sprintf_s(s+ch, sizeof(s)-ch, "Del "); if (RemoteFlags.ImapDraft) ch += sprintf_s(s+ch, sizeof(s)-ch, "Drft "); if (RemoteFlags.ImapFlagged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Flg "); if (RemoteFlags.ImapRecent) ch += sprintf_s(s+ch, sizeof(s)-ch, "Rec "); if (RemoteFlags.ImapSeen) ch += sprintf_s(s+ch, sizeof(s)-ch, "Seen "); if (RemoteFlags.ImapExpunged) ch += sprintf_s(s+ch, sizeof(s)-ch, "Exp "); return s; break; } case FIELD_CACHE_FILENAME: { char *l = Path ? strrchr(Path, DIR_CHAR) : 0; return l ? l + 1 : Path.Get(); } case FIELD_SERVER_UID: { // We allow the app to get the integer UID here as a string // because POP3 UID's ARE strings, so we need to be compatible if (!UidCache) UidCache.Printf("%u", Uid); return UidCache; break; } case FIELD_LABEL: { auto m = GetMeta(); #if IMAP_PROTOBUF return m ? m->label().c_str() : NULL; #else return m ? m->GetAttr(ATTR_LABEL) : NULL; #endif break; } } return NULL; } void CopyImapSegs(ImapMail *Mail, LDataPropI *Dst, LDataPropI *Src) { if (!Dst || !Src) { LAssert(!"Invalid ptrs"); return; } LDataI *DDst = dynamic_cast(Dst); LDataI *DSrc = dynamic_cast(Src); if (!DDst || !DSrc) { LAssert(!"Cast failed."); return; } DDst->CopyProps(*DSrc); LDataIt DstLst = Dst->GetList(FIELD_MIME_SEG); LDataIt SrcLst = Src->GetList(FIELD_MIME_SEG); ImapAttachment *ParentAtt = dynamic_cast(Dst); LMime *ParentMime = ParentAtt->GetSeg(); for (LDataPropI *c = SrcLst->First(); c; c = SrcLst->Next()) { LDataPropI *n = DstLst->Create(Mail->GetStore()); if (n) { CopyImapSegs(Mail, n, c); ImapAttachment *ChildAtt = dynamic_cast(n); ChildAtt->AttachTo(ParentAtt); LMime *ChildMime = ChildAtt->GetSeg(); if (ParentMime && ChildMime) { if (!ParentMime->Insert(ChildMime)) { LAssert(!"Insert failed."); } } } else LAssert(!"Create object failed."); } } Store3CopyImpl(ImapMail) { Priority = (int)p.GetInt(FIELD_PRIORITY); SetInt(FIELD_FLAGS, p.GetInt(FIELD_FLAGS)); SetStr(FIELD_LABEL, p.GetStr(FIELD_LABEL)); int64 Col = p.GetInt(FIELD_COLOUR); SetInt(FIELD_COLOUR, Col); MsgId = p.GetStr(FIELD_MESSAGE_ID); Subject = p.GetStr(FIELD_SUBJECT); LDataPropI *i = p.GetObj(FIELD_FROM); if (i) From.CopyProps(*i); i = p.GetObj(FIELD_REPLY); if (i) Reply.CopyProps(*i); const LDateTime *d = p.GetDate(FIELD_DATE_RECEIVED); if (d) DateReceived = *d; else DateReceived.Empty(); d = p.GetDate(FIELD_DATE_SENT); if (d) { DateSent = *d; ValidateImapDate(DateSent); } else DateSent.Empty(); To.DeleteObjects(); LDataIt pTo = p.GetList(FIELD_TO); for (unsigned n=0; nLength(); n++) { Store3Addr *a = new Store3Addr(GetStore(), (*pTo)[n]); if (a) To.Insert(a, -1, true); } To.State = Store3Loaded; Seg = dynamic_cast(GetObj(FIELD_MIME_SEG)); if (!Seg) Seg = new ImapAttachment(Store, this); auto SrcSeg = p.GetObj(FIELD_MIME_SEG); if (SrcSeg) CopyImapSegs(this, Seg, SrcSeg); return true; } #define _Str(v) { char *s = NewStr(str); DeleteArray(v); v = s; return !((s != 0) ^ (str != 0)); } Store3Status ImapMail::SetStr(int id, const char *str) { Load(); switch (id) { case FIELD_SUBJECT: Subject = str; break; case FIELD_MESSAGE_ID: { // Set the local copy MsgId = str; // Set the folder meta data copy auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_messageid(str); #else m->SetAttr(ATTR_MSGID, str); #endif Parent->SetDirty(); } break; } case FIELD_LABEL: { auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_label(str); #else m->SetAttr(ATTR_LABEL, Label = str); #endif Parent->SetDirty(); } else { Label = str; } break; } case FIELD_SERVER_UID: // ?? break; case FIELD_INTERNET_HEADER: { if (HeaderCache.Get() != str) HeaderCache.Reset(NewStr(str)); // Reset all the cached values... MsgId.Empty(); Subject.Empty(); From.Empty(); Reply.Empty(); DateReceived.Empty(); DateSent.Empty(); To.DeleteObjects(); DeleteObj(Seg); Loaded = Store3Unloaded; break; } case FIELD_ALTERNATE_HTML: { const char *Mt; ImapAttachment *Alt = Seg ? Seg->FindChildByMimeType(sMultipartAlternative) : NULL; ImapAttachment *Html = Alt ? Alt->FindChildByMimeType(sTextHtml) : NULL; if (Html) { if (str) { // Set new stream LAutoStreamI a(new LMemStream(str, strlen(str))); if (Html->SetStream(a)) SegDirty = true; } else { // Delete content Html->Delete(); delete Html; break; } } else if (Seg && (Mt = Seg->GetStr(FIELD_MIME_TYPE)) && !_stricmp(Mt, sTextHtml)) // Is the root segment HTML? { // Set the data to an empty stream... LAutoStreamI a(str ? new LMemStream(str, strlen(str)) : NULL); if (Seg->SetStream(a)) SegDirty = true; } HtmlCache.Reset(); break; } } return Store3Success; } int64 ImapMail::GetInt(int id) { switch (id) { case FIELD_STORE_TYPE: return Store3Imap; case FIELD_LOADED: return Loaded; case FIELD_DONT_SHOW_PREVIEW: return true; case FIELD_SIZE: { auto Meta = GetMeta(); if (Meta) { #if IMAP_PROTOBUF return DataSize = Meta->size(); #else char *Sz = Meta->GetAttr(ATTR_SIZE); if (Sz) return atoi64(Sz); #endif } DataSize = LFileSize(Path); LAssert(DataSize <= 0x7fffffff); return DataSize; } case FIELD_PRIORITY: return Priority; case FIELD_FLAGS: // LgiTrace("%p.LocalFlags = %s\n", this, EmailFlagsToStr(LocalFlags).Get()); return LocalFlags; case FIELD_COLOUR: { uint32_t col = Colour.c32(); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF col = m->colour(); #else char *s = m->GetAttr(ATTR_COLOUR); if (s) col = (uint32_t)atoi64(s); #endif } return col; break; } case FIELD_ACCOUNT_ID: return Store->GetInt(id); case FIELD_SERVER_UID: return Uid; } return -1; } Store3Status ImapMail::SetInt(int id, int64 i) { Load(); switch (id) { case FIELD_LOADED: { if (Loaded >= i) return Store3Success; if (i == Store3Loaded) ReadMime(NULL); else LAssert(!"What other option is there?"); if (Loaded >= i) return Store3Success; if (Loaded == Store3Loading) return Store3Delayed; return Store3Error; // what happened here? break; } case FIELD_COLOUR: { Colour.Set((uint32_t)i, 32); auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF m->set_colour(i); #else m->SetAttr(ATTR_COLOUR, i); #endif Parent->SetDirty(); } break; } case FIELD_PRIORITY: Priority = (int)i; break; case FIELD_FLAGS: { bool Seen = RemoteFlags.ImapSeen; bool Answered = RemoteFlags.ImapAnswered; LocalFlags = (int)i; RemoteFlags.ImapSeen = TestFlag(i, MAIL_READ); RemoteFlags.ImapAnswered = TestFlag(i, MAIL_REPLIED); if (!Parent) return Store3Success; auto t = GetMeta(); if (t) { #if IMAP_PROTOBUF t->set_remoteflags(RemoteFlags.All); t->set_localflags(RemoteFlags.All); #else t->SetAttr(ATTR_FLAGS, RemoteFlags.Get()); t->SetAttr(ATTR_LOCAL, i); #endif Parent->SetDirty(); } else { LAssert(!"No tag."); return Store3Error; } if ( (RemoteFlags.ImapSeen ^ Seen) || (RemoteFlags.ImapAnswered ^ Answered) ) { // Update the IMAP server LAutoPtr Msg(new ImapMsg(IMAP_SET_FLAGS, _FL)); if (!Msg) return Store3Error; Msg->Parent = Parent->Remote.Get(); LAssert(Msg->Parent); ImapMailInfo &Info = Msg->Mail.New(); Info.Uid = Uid; Info.Flags = RemoteFlags; if (!Store->PostThread(Msg.Release(), false)) return Store3Error; return Store3Delayed; // caller doesn't have to set the object dirty } return Store3Success; } } return Store3Success; } const LDateTime *ImapMail::GetDate(int id) { if ((id == FIELD_DATE_RECEIVED && !DateReceived.Year()) || (id == FIELD_DATE_SENT && !DateSent.Year())) - { - /* - auto m = GetMeta(false); - if (m) - { - auto s = m->GetAttr(ATTR_DATE); - int asd=0; - } - */ - + { auto Headers = GetStr(FIELD_INTERNET_HEADER); - LAutoString h(InetGetHeaderField(Headers, "Date")); + auto h = LGetHeaderField(Headers, "Date"); if (h) { DateReceived.Decode(h); DateReceived.ToUtc(); DateSent = DateReceived; ValidateImapDate(DateSent); } } switch (id) { case FIELD_DATE_RECEIVED: return &DateReceived; case FIELD_DATE_SENT: return &DateSent; } return 0; } Store3Status ImapMail::SetDate(int id, const LDateTime *i) { Load(); switch (id) { case FIELD_DATE_RECEIVED: DateReceived = *i; break; case FIELD_DATE_SENT: DateSent = *i; ValidateImapDate(DateSent); break; default: return Store3Error; } return Store3Success; } Store3Addr *ImapMail::ProcessAddress(Store3Addr &Addr, const char *FieldId, const char *RfcField) { if (!Addr.Addr) { // Try and load from the meta... auto m = GetMeta(); if (m) { #if IMAP_PROTOBUF auto f = m->from(); if (!f.email().empty()) { Addr.SetStr(FIELD_NAME, f.name().c_str()); Addr.SetStr(FIELD_EMAIL, f.email().c_str()); return &Addr; } #else auto f = m->GetAttr(FieldId); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); return &Addr; } #endif } // Otherwise try and load from the headers... auto Headers = GetStr(FIELD_INTERNET_HEADER); auto f = LDecodeRfc2047(LGetHeaderField(Headers, RfcField)); if (f) { LAutoString Name, Email; DecodeAddrName(f, Name, Email, 0); Addr.SetStr(FIELD_NAME, Name); Addr.SetStr(FIELD_EMAIL, Email); if (Uid) Serialize(m, true); } } return &Addr; } LDataPropI *ImapMail::GetObj(int id) { switch (id) { case FIELD_MIME_SEG: { if (!Seg) ReadMime(NULL); return Seg; } case FIELD_FROM: { return ProcessAddress(From, ATTR_FROM, "From"); } case FIELD_REPLY: { return ProcessAddress(Reply, ATTR_REPLYTO, "Reply-To"); } } return 0; } Store3Status ImapMail::SetObj(int id, LDataPropI *i) { LAssert(0); return Store3Error; } LDataIt ImapMail::GetList(int id) { Load(); switch (id) { case FIELD_TO: { if (!To.Length()) { auto Headers = GetStr(FIELD_INTERNET_HEADER); auto h = LDecodeRfc2047(LGetHeaderField(Headers, "To")); if (h) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); To.Insert(la, -1, true); } } Addr.DeleteArrays(); } To.State = Store3Loaded; if ((h = LDecodeRfc2047(LGetHeaderField(Headers, "Cc")))) { List Addr; TokeniseStrList(h, Addr, ","); for (auto a: Addr) { Store3Addr *la = new Store3Addr(GetStore()); if (la) { DecodeAddrName(a, la->Name, la->Addr, 0); la->CC = true; To.Insert(la); } } Addr.DeleteArrays(); } } return &To; } } return 0; } Store3Status ImapMail::Save(LDataI *Folder) { Store3Status Status = Store3Error; ImapFolder *Fld = dynamic_cast(Folder); if (!Fld) { LAssert(0); LgiTrace("%s:%i - No folder.\n", _FL); } else if (Path) { LAssert(0); LgiTrace("%s:%i - Already has path?\n", _FL); } else { if (!Seg && !Stream) { // Need to create an empty text segment LMime *m = new LMime(ScribeTempPath()); if (!m) return Store3Error; m->SetMimeType(sTextPlain); if (Subject) m->Set("Subject", Subject); if (DateSent.IsValid()) m->Set("Date", DateSent.Get()); if (MsgId) m->Set("MessageID", MsgId); if (From.Addr) { LVariant v; if (From.GetValue("Text", v)) m->Set("From", v.Str()); } if (Reply.Addr) { LVariant v; if (Reply.GetValue("Text", v)) m->Set("Reply", v.Str()); } Seg = new ImapAttachment(Store, this, m); } char r[32], p[MAX_PATH_LEN]; do { sprintf_s(r, sizeof(r), "temp_%u.eml", LRand()); LMakePath(p, sizeof(p), Fld->Local, r); } while (LFileExists(p)); Path = p; LFile f; if (f.Open(Path, O_WRITE)) { bool SaveFailed = false; if (Stream) { // Copy the import stream directly to disk... LCopyStreamer Cp; Stream->SetPos(0); if (Cp.Copy(Stream, &f)) { // After writing to disk, this is not needed anymore. Stream.Release(); } else { LgiTrace("%s:%i - Copy failed.\n", _FL); SaveFailed = true; } } else if (Seg) { // Re-encode the MIME segment tree... auto s = Seg->GetSeg(); if (!s) { LgiTrace("%s:%i - No segment.\n", _FL); SaveFailed = true; } else if (!s->Text.Encode.Push(&f)) { LgiTrace("%s:%i - Mime encode failed.\n", _FL); SaveFailed = true; } } f.Close(); if (!SaveFailed) { LAutoPtr m(new ImapMsg(IMAP_APPEND, _FL)); m->Parent = Fld->Remote.Get(); m->Mail[0].Local = Path.Get(); m->Mail[0].Flags = RemoteFlags; if (Fld->Store->PostThread(m.Release(), false)) { Status = Store3Delayed; Fld->LoadMail(); Fld->AddMail(this, 0); } else { LgiTrace("%s:%i - PostThread failed.\n", _FL); } } } else { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, Path.Get()); } } return Status; } Store3Status ImapMail::Delete(bool ToTrash) { Store3Status Status = Store3Error; LAutoPtr m(new ImapMsg(IMAP_DELETE, _FL)); m->Mail[0].Uid = Uid; m->Parent = Parent->Remote.Get(); if (Store->PostThread(m.Release(), false)) { SetState(ImapMailDeleting, _FL); Status = Store3Delayed; LArray a; a.Add(this); Store->OnChange(_FL, a, 0); } return Status; } bool ImapMail::OnDelete() { bool Status = false; if (Parent) { Parent->DelMail(this); Parent = 0; } if (Path) { Status = FileDev->Delete(Path, NULL, false); } return Status; } LAutoStreamI ImapMail::GetStream(const char *file, int line) { LAutoStreamI Ret; if (Stream) { Ret.Reset(new LProxyStream(Stream)); } else { LFile *f; if (Ret.Reset(f = new LFile)) { if (!f->Open(Path, O_READWRITE)) Ret.Reset(); } } return Ret; } bool ImapMail::SetStream(LAutoStreamI stream) { if (!stream) return false; DeleteObj(Seg); // MIME parse the stream and store it to segments. LAutoPtr Mime(new LMime); if (Mime->Text.Decode.Pull(stream)) { Seg = new ImapAttachment(Store, this, Mime.Release()); if (Seg) { // This stops the objects being written to disk. // Which would mean we have multiple copies of the same // data on disk. This setting also propagates down the // tree automatically as GMimeToStore3 saves new child // notes to their parents. Seg->SetInMemoryOnly(true); Seg->AttachTo(this); } } return false; } void ImapMail::OnDownload(LAutoString &Headers) { LFile f; if (f.Open(Path, O_WRITE)) { f.Write(Headers, strlen(Headers)); f.Close(); } HeaderCache = Headers; Loaded = Store3Headers; SetState(ImapMailIdle, _FL); auto t = GetMeta(); if (t) { // t->SetAttr(ATTR_HEADER_ONLY, 1); // This pulls all the relevant fields out of the headers so they can be written // to the meta element. This means it gets loaded without having to touch the // email on disk. Which means faster filtering on the commonly displayed list // fields. GetStr(FIELD_SUBJECT); GetObj(FIELD_FROM); GetDate(FIELD_DATE_SENT); Serialize(t, true); } }