diff --git a/src/Calendar.cpp b/src/Calendar.cpp --- a/src/Calendar.cpp +++ b/src/Calendar.cpp @@ -1,3382 +1,3388 @@ /*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.IsContextMenu()) { auto View = GetView(); DoContextMenu(m, View ? (LView*)View : (LView*)App); } else if (m.Down() && m.Left() && m.Double()) { DoUI(); } } 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 (auto cal: a) { auto str = cal->ToString(); s.Insert(cal); } return true; } return false; } #define IDM_MOVE_TO 4000 void Calendar::DoContextMenu(LMouse &m, LView *Parent) { LSubMenu Sub; LScriptUi 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); auto Cv = dynamic_cast(Parent); if (Cv) { s.Sub->AppendSeparator(); for (unsigned n = 0; nGetName() && Cs->IsWritable()) { auto msg = LString::Fmt("Move to '%s'\n", Cs->GetName()); s.Sub->AppendItem(msg, 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 (auto cb: Callbacks) App->ExecuteScriptCallback(*cb, Args); Args.DeleteObjects(); } int Result = s.Sub->Float(Parent, m); switch (Result) { default: { auto Idx = Result - IDM_MOVE_TO; if (Cv && 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 (auto &cb: s.Callbacks) { if (cb.Param == Result) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(cb.Param); App->ExecuteScriptCallback(cb, Args); Args.DeleteObjects(); } } 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; auto PList = dynamic_cast(Parent); if (GetParentSelection(PList ? PList : GetList(), Del)) { for (auto i: Del) { if (auto t = dynamic_cast(i)) { t->OnDelete(); } else if (auto Todo = dynamic_cast(i)) { if (auto c = Todo->GetTodo()) c->OnDelete(); } } } else { 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)) + { + ErrMsg.Printf("Unknown mimetype '%s'", mimeType); IoProgressNotImpl(); + } VCal vCal; if (!vCal.Import(GetObject(), stream)) - IoProgressError("vCal import failed."); + { + ErrMsg.Printf("vCal import failed"); + IoProgressError(ErrMsg); + } 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/CalendarView.cpp b/src/CalendarView.cpp --- a/src/CalendarView.cpp +++ b/src/CalendarView.cpp @@ -1,3639 +1,3648 @@ #include "Scribe.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ListItemRadioBtn.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/DropFiles.h" #include "lgi/common/MonthView.h" #include "lgi/common/YearView.h" #include "lgi/common/Array.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/MonthView.h" #include "lgi/common/SkinEngine.h" #include "lgi/common/Combo.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Printer.h" #include "lgi/common/Notifications.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/PopupNotification.h" #include "CalendarView.h" #include "ScribePageSetup.h" #include "Store3Webdav/WebdavStore.h" #include "resdefs.h" #include "resource.h" -char ScribeCalendarObject[] = "com.memecode.Calendar"; +auto ScribeCalendarObject = "com.memecode.Calendar"; #define BORDER_YEAR 28 #define THRESHOLD_EDGE 3 // Px #define WEEKEND_TINT_LEVEL 0.97f #define WEEKEND_TINT_COLOUR LColour(0, 0, 0xd0) #define TODAY_TINT_LEVEL 0.95f #define TODAY_TINT_COLOUR LColour(255, 0, 0) ///////////////////////////////////////////////////////////////////////////////////// #define IDM_CAL_DAY 100 #define IDM_CAL_WEEK 101 #define IDM_CAL_MONTH 102 #define IDM_CAL_YEAR 103 #define IDM_PREV 200 #define IDM_BACK 201 #define IDM_TODAY 202 #undef IDM_FORWARD #define IDM_FORWARD 203 #define IDM_NEXT 204 #define IDM_CONFIG 205 #define IDM_TODO 206 #define IDM_15MIN 207 #define IDM_30MIN 208 #define IDM_1HR 209 #define IDM_NEW_EVENT 210 #define IDC_TODO 300 ///////////////////////////////////////////////////////////////////////////////////// void LoadCalendarStringTable() { ShortDayNames[0] = LLoadString(IDS_CAL_SDAY_SUN); ShortDayNames[1] = LLoadString(IDS_CAL_SDAY_MON); ShortDayNames[2] = LLoadString(IDS_CAL_SDAY_TUE); ShortDayNames[3] = LLoadString(IDS_CAL_SDAY_WED); ShortDayNames[4] = LLoadString(IDS_CAL_SDAY_THU); ShortDayNames[5] = LLoadString(IDS_CAL_SDAY_FRI); ShortDayNames[6] = LLoadString(IDS_CAL_SDAY_SAT); FullDayNames[0] = LLoadString(IDS_CAL_LDAY_SUN); FullDayNames[1] = LLoadString(IDS_CAL_LDAY_MON); FullDayNames[2] = LLoadString(IDS_CAL_LDAY_TUE); FullDayNames[3] = LLoadString(IDS_CAL_LDAY_WED); FullDayNames[4] = LLoadString(IDS_CAL_LDAY_THU); FullDayNames[5] = LLoadString(IDS_CAL_LDAY_FRI); FullDayNames[6] = LLoadString(IDS_CAL_LDAY_SAT); ShortMonthNames[0] = LLoadString(IDS_CAL_SMONTH_JAN); ShortMonthNames[1] = LLoadString(IDS_CAL_SMONTH_FEB); ShortMonthNames[2] = LLoadString(IDS_CAL_SMONTH_MAR); ShortMonthNames[3] = LLoadString(IDS_CAL_SMONTH_APR); ShortMonthNames[4] = LLoadString(IDS_CAL_SMONTH_MAY); ShortMonthNames[5] = LLoadString(IDS_CAL_SMONTH_JUN); ShortMonthNames[6] = LLoadString(IDS_CAL_SMONTH_JUL); ShortMonthNames[7] = LLoadString(IDS_CAL_SMONTH_AUG); ShortMonthNames[8] = LLoadString(IDS_CAL_SMONTH_SEP); ShortMonthNames[9] = LLoadString(IDS_CAL_SMONTH_OCT); ShortMonthNames[10] = LLoadString(IDS_CAL_SMONTH_NOV); ShortMonthNames[11] = LLoadString(IDS_CAL_SMONTH_DEC); FullMonthNames[0] = LLoadString(IDS_CAL_LMONTH_JAN); FullMonthNames[1] = LLoadString(IDS_CAL_LMONTH_FEB); FullMonthNames[2] = LLoadString(IDS_CAL_LMONTH_MAR); FullMonthNames[3] = LLoadString(IDS_CAL_LMONTH_APR); FullMonthNames[4] = LLoadString(IDS_CAL_LMONTH_MAY); FullMonthNames[5] = LLoadString(IDS_CAL_LMONTH_JUN); FullMonthNames[6] = LLoadString(IDS_CAL_LMONTH_JUL); FullMonthNames[7] = LLoadString(IDS_CAL_LMONTH_AUG); FullMonthNames[8] = LLoadString(IDS_CAL_LMONTH_SEP); FullMonthNames[9] = LLoadString(IDS_CAL_LMONTH_OCT); FullMonthNames[10] = LLoadString(IDS_CAL_LMONTH_NOV); FullMonthNames[11] = LLoadString(IDS_CAL_LMONTH_DEC); } ///////////////////////////////////////////////////////////////////////////////////// class LColourItem : public LListItemColumn { LListItem *Item; LView *Colour; public: LColourItem(LListItem *i, int c, COLOUR col = -1) : LListItemColumn(i, c) { Item = i; Colour = LViewFactory::Create("LColourSelect"); LAssert(Colour != NULL); LArray colours; for (int n=0; nSetColourList(&colours); Colour->Value(col); } } ~LColourItem() { DeleteObj(Colour); } void OnPaintColumn(ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (!Colour->IsAttached()) { Colour->Attach(Item->GetList()); } Colour->SetPos(Ctx); Colour->Visible(true); } void OnMouseClick(LMouse &m) { Colour->OnMouseClick(m); } int64 Value() { return Colour->Value(); } void Value(int64 i) { Colour->Value(i); } void Disconnect() { Colour->Detach(); } }; ///////////////////////////////////////////////////////////////////////////////////// CalendarTodoItem::CalendarTodoItem(ScribeWnd *app, Calendar *todo) { App = app; Todo = 0; Done = 0; SetTodo(todo); EditingLabel = false; } CalendarTodoItem::~CalendarTodoItem() { if (Todo) { LAssert(Todo->TodoView == this); Todo->TodoView = 0; } } int TodoCompare(LListItem *la, LListItem *lb, NativeInt d) { int Status = 0; CalendarTodoItem *a = dynamic_cast(la); CalendarTodoItem *b = dynamic_cast(lb); if (a && b) { int Col = abs((int)d) - 1; int Mul = d >= 0 ? 1 : -1; bool Atodo = a->Todo != 0; bool Btodo = b->Todo != 0; if (Atodo ^ Btodo) { Status = Btodo - Atodo; } else if (a->Todo && b->Todo) { switch (Col) { case 0: { // Completed int Acomplete = 0; int Bcomplete = 0; a->Todo->GetField(FIELD_CAL_COMPLETED, Acomplete); b->Todo->GetField(FIELD_CAL_COMPLETED, Bcomplete); if (Acomplete != Bcomplete) { Status = Mul * (Acomplete - Bcomplete); } else { goto ByDueDate; } break; } case 1: { // Subject BySubject: const char *Asub = 0; const char *Bsub = 0; a->Todo->GetField(FIELD_CAL_SUBJECT, Asub); b->Todo->GetField(FIELD_CAL_SUBJECT, Bsub); if (Asub && Bsub) { Status = Mul * _stricmp(Asub, Bsub); } break; } case 2: { // Due date ByDueDate: LDateTime Adate; LDateTime Bdate; a->Todo->GetField(FIELD_CAL_START_UTC, Adate); b->Todo->GetField(FIELD_CAL_START_UTC, Bdate); bool Ad = Adate.Year() != 0; bool Bd = Bdate.Year() != 0; if (Ad ^ Bd) { Status = Mul * (Bd - Ad); } else if (Ad && Bd) { Status = Mul * Adate.Compare(&Bdate); } else { goto BySubject; } break; } } } } return Status; } void CalendarTodoItem::Resort() { if (GetList()) { int Sort = 1; for (int i=0; iGetColumns(); i++) { auto c = GetList()->ColumnAt(i); if (c) { if (c->UpArrow()) { Sort = -(i + 1); break; } else if (c->DownArrow()) { Sort = i + 1; break; } } } GetList()->Sort(TodoCompare, Sort); } } void CalendarTodoItem::SetTodo(Calendar *todo) { if (Todo) { Todo->TodoView = 0; } Todo = todo; if (Todo) { Todo->TodoView = this; int Completed = false; Todo->GetField(FIELD_CAL_COMPLETED, Completed); Done = new LListItemCheckBox(this, 0, Completed != 0); } else { DeleteObj(Done); } SetImage(Todo ? ICON_TODO : -1); } const char *CalendarTodoItem::GetText(int Col) { if (Todo) { switch (Col) { case 1: { // Name const char *s; if (Todo->GetField(FIELD_CAL_SUBJECT, s)) { return s; } break; } case 2: { // Due LDateTime d; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); d.Get(DateCache, sizeof(DateCache)); return DateCache; } break; } } } else if (Col == 1) { if (EditingLabel) { EditingLabel = false; } else { return (char*)LLoadString(IDS_CLICK_TO_CREATE); } } return 0; } bool CalendarTodoItem::SetText(const char *s, int Col) { if (Col == 1) { if (Todo) { // Set the name... Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); Resort(); } else { if (ValidStr(s)) { // Create new todo ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (Cal) { Thing *t = App->CreateItem(MAGIC_CALENDAR, Cal, false); if (t && t->IsCalendar()) { SetTodo(t->IsCalendar()); Todo->SetCalType(CalTodo); Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); GetList()->Insert(new CalendarTodoItem(App)); Resort(); } } else { LgiMsg(GetList(), "No calendar folder.", AppName); } } else { return false; } } } return LListItem::SetText(s, Col); } void CalendarTodoItem::OnPaint(ItemPaintCtx &Ctx) { if (!Todo) { Ctx.Fore = LColour(L_LOW); Ctx.Back = LColour(L_WORKSPACE); } else if (Done && Done->Value()) { Ctx.Fore = LColour(L_LOW); } else { LDateTime d, Now; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); Now.SetNow(); if (Now.Compare(&d) > 0) { Ctx.Fore.Set(255, 0, 0); } } } LListItem::OnPaint(Ctx); } void CalendarTodoItem::OnColumnNotify(int Col, int64 Data) { if (Col == 0) { Todo->SetField(FIELD_CAL_COMPLETED, Data ? 100 : 0); Resort(); } } void CalendarTodoItem::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Down()) { if (m.Left()) { int Col = Todo ? GetList()->ColumnAtX(m.x) : 1; switch (Col) { case 1: { if (!Todo) { EditingLabel = true; } EditLabel(1); break; } default: { if (Todo && m.Double()) { Todo->DoUI(); } break; } } } else if (m.Right()) { if (Todo) { Todo->DoContextMenu(m, GetList()); } } } } ///////////////////////////////////////////////////////////////////////////////////// class LMonthView : public LView, public MonthView { LRect rTitle; LRect rCells; LRect rLeft, rRight; int Cell; CalendarView *CalView; public: LMonthView(int Id, LDateTime *n, CalendarView *calView); void OnCellClick(int Cx, int Cy); void SeekMonth(int Dir); void OnMouseClick(LMouse &m); void OnPaint(LSurface *pDC); }; // This structure encodes the direction to move the cursor // on different key presses. The 'years' isn't implemented in // the OnKey handler. struct CalViewArrow { public: CalendarViewMode Mode; int Key; int Flags; int Hours; int Days; int Months; int Years; }; CalViewArrow Arrows[] = { // Flags, H D M Y // Week view movement {CAL_VIEW_WEEK, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_WEEK, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_WEEK, LK_UP, 0, -1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_DOWN, 0, 1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, 0, 0, -7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,0, 0, 7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, LGI_EF_CTRL, 0, 0, -1, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 1, 0}, // Month view movement {CAL_VIEW_MONTH, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_MONTH, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_MONTH, LK_UP, 0, 0, -7, 0, 0}, {CAL_VIEW_MONTH, LK_DOWN, 0, 0, 7, 0, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, 0, 0, 0, -1, 0}, {CAL_VIEW_MONTH, LK_PAGEDOWN,0, 0, 0, 1, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, LGI_EF_CTRL, 0, 0, 0, -1}, {CAL_VIEW_MONTH, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 0, 1}, // Year view movement {CAL_VIEW_YEAR, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_YEAR, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_YEAR, LK_UP, 0, 0, 0, -1, 0}, {CAL_VIEW_YEAR, LK_DOWN, 0, 0, 0, 1, 0}, {CAL_VIEW_YEAR, LK_PAGEUP, 0, 0, 0, 0, -1}, {CAL_VIEW_YEAR, LK_PAGEDOWN,0, 0, 0, 0, 1} }; CalViewArrow *GetModeArrow(CalendarViewMode m, LKey &k) { for (int i=0; i CalendarView::CalendarViews; CalendarView::CalendarView(ScribeFolder *folder, int id, LRect *pos, const char *name) { CalendarViews.Add(this); if (Calendar::DayStart < 0) InitCalendarView(); DragMode = DragNone; DragEvent = NULL; LastTsOffset = 0; DayStart = Calendar::DayStart; DayEnd = Calendar::DayEnd; if (!App) App = folder->App; Mode = CAL_VIEW_MONTH; LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarViewMode, v)) Mode = (CalendarViewMode) v.CastInt32(); if (id > 0) SetId(id); if (pos) SetPos(*pos); if (name) Name(name); SetPourLargest(true); Sunken(true); MonthX = 7; MonthY = 5; LFontType Type; Type.GetSystemFont("Small"); Font.Reset(Type.Create()); Cursor.SetNow(); LoadUsers(); OnOptionsChange(); } CalendarView::~CalendarView() { LVariant v; App->GetOptions()->SetValue(OPT_CalendarViewMode, v = (int)Mode); CalendarViews.Delete(this); } void CalendarView::OnOptionsChange() { LVariant v; if (App && App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); for (auto c: CalendarViews) c->OnContentsChanged(); } CalendarView *Calendar::GetView() { if (CalendarView::CalendarViews.Length() == 0) return NULL; return CalendarView::CalendarViews[0]; } void CalendarView::LoadUsers() { LArray Sources; App->GetCalendarSources(Sources); LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); LDateTime dt = Cursor; Cursor.Set("1/1/1900"); SetCursor(dt); // Do we really need this? // OnContentsChanged(); } void CalendarView::DeleteSource(CalendarSource *cs) { // Delete events out of 'Current' for (unsigned i=0; iGetSource() == cs) { Current.DeleteAt(i--); } } // Delete the reference in the options file... cs->Delete(); // Delete the C++ object and list ref... DeleteObj(cs); // Refresh the screen. Invalidate(); } bool CalendarView::GetEventsBetween(LArray &Events, LDateTime Start, LDateTime End) { bool Status = false; LDateTime EndMinute = End; EndMinute.AddMinutes(-1); for (auto &t: Current) { if (t.Overlap(Start, End)) { Events.Add(t); Status = true; } } return Status; } void CalendarView::OnCreate() { SetWindow(this); SetupScroll(); SetPulse(3000); } void CalendarView::OnPulse() { CalendarViewWnd *w = dynamic_cast(GetWindow()); LArray all; if (w->CalLst->GetAll(all)) { for (auto a: all) a->OnPulse(); } } LMessage::Result CalendarView::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_GET_EVENTS_DONE: { if (SourceEventsDirty) { SourceEventsDirty = false; Invalidate(); } break; } } return LLayout::OnEvent(Msg); } bool CalendarView::OnLayout(LViewLayoutInfo &Inf) { if (Inf.Width.Max == 0) { Inf.Width.Min = -1; Inf.Width.Max = -1; } else { Inf.Height.Min = -1; Inf.Height.Max = -1; } return true; } int CalendarView::OnNotify(LViewI *v, LNotification n) { switch (v->GetId()) { case IDC_VSCROLL: { if (n.Type == LNotifyScrollBarCreate) { SetupScroll(); } Invalidate(); break; } } return 0; } void CalendarView::SelectDropTarget(LDateTime *start, LDateTime *end) { /* if (start && DropStart) { if (*start == *DropStart) { // the same return; } } DeleteObj(DropStart); DeleteObj(DropEnd); if (start) { DropStart = new LDateTime; if (DropStart) { *DropStart = *start; } if (end) { DropEnd = new LDateTime; if (DropEnd) { *DropEnd = *end; } } } Invalidate(); */ } void CalendarView::OnSelect(Calendar *c, bool Ctrl, bool Shift) { if (!Ctrl) { Selection.Length(0); } else { Selection.Delete(c); } if (c) { Selection.Add(c); Invalidate(&c->ViewPos); } } CalendarViewMode CalendarView::GetViewMode() { return Mode; } void CalendarView::SetupScroll() { SetScrollBars(false, Mode == CAL_VIEW_WEEK); if (VScroll) { int Page = (int)(Calendar::DayEnd - Calendar::DayStart); VScroll->SetRange(24); VScroll->SetPage(Page); VScroll->Value(Calendar::DayStart); } } void CalendarView::CalDelete(Calendar *c) { Selection.Delete(c); for (size_t i=0; i 0) Diff -= 7; Start.AddDays(Diff); First = Start; break; } case CAL_VIEW_MONTH: { First = Cursor; First.Day(1); Start = First; int DayOfWeek = Start.DayOfWeek(); int Diff = FirstDayOfWeek - DayOfWeek; if (Diff > 0) Diff -= 7; Start.AddDays(Diff); break; } case CAL_VIEW_YEAR: { YearView v(&Cursor); v.SetCursor(0, 0); First = v.Get(); Start = Cursor; Start.Day(1); Start.Month(1); break; } } bool YearCh = Old.Year() != Cursor.Year(); bool MthCh = Old.Month() != Cursor.Month() || YearCh; bool DayCh = Old.Day() != Cursor.Day() || MthCh; OnCursorChange(DayCh, MthCh, YearCh); } void CalendarView::OnSourceDelete(CalendarSource *s) { for (size_t i=0; i, bool> InCur; for (auto &c: Current) InCur.Add(c.c, true); for (unsigned i=0; iSource = NULL; } */ if (!GetEvents) { new CalendarSourceGetEvents( App, &GetEvents, s, e, CalendarSource::GetSources(), [this](auto Events) { Current = Events; Invalidate(); PostEvent(M_GET_EVENTS_DONE); }); } else { SourceEventsDirty = true; } LDateTime::GetDaylightSavingsInfo(Dst, s, &e); LWindow *Wnd = GetWindow(); if (Wnd) { char s[256]; sprintf_s(s, sizeof(s), "%s [%s %i]", LLoadString(IDS_CAL_VIEW), FullMonthNames[Cursor.Month()-1], Cursor.Year()); Wnd->Name(s); } } Invalidate(); } bool CalendarView::OnPrintPage(LPrintDC *pDC, int PageIndex) { LVariant Bx1, By1, Bx2, By2; LOptionsFile *Options = App->GetOptions(); LFontType FontType("Courier New", 8); FontType.GetSystemFont("small"); Bx1 = By1 = Bx2 = By2 = 1.0; // cm if (Options) { // read any options out.. #define GetMargin(opt, var) \ { LVariant v; if (Options->GetValue(opt, v)) var = v.CastDouble(); } GetMargin(OPT_MarginX1, Bx1); GetMargin(OPT_MarginY1, By1); GetMargin(OPT_MarginX2, Bx2); GetMargin(OPT_MarginY2, By2); } LAutoPtr ScreenFont = Font; if (pDC) { // setup device context double CmToInch = 0.393700787; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); double ScaleX = (double)ScreenDpi.x / DcDpi.x; double ScaleY = (double)ScreenDpi.y / DcDpi.y; // LRect c = GetClient(); PrintMargin.x1 = (int) (( (Bx1.CastDouble() * CmToInch) * DcDpi.x ) * ScaleX); PrintMargin.y1 = (int) (( (By1.CastDouble() * CmToInch) * DcDpi.y ) * ScaleY); PrintMargin.x2 = (int) (( pDC->X() - ((Bx2.CastDouble() * CmToInch) * DcDpi.x) ) * ScaleX); PrintMargin.y2 = (int) (( pDC->Y() - ((By2.CastDouble() * CmToInch) * DcDpi.y) ) * ScaleY); // setup font Font.Reset(FontType.Create(pDC)); if (Font) { Font->Colour(L_BLACK, L_WHITE); Font->Create(0, 0, pDC); LDisplayString ds(Font, " "); Font->TabSize(ds.X() * 8); OnPaint(pDC); } } Font = ScreenFont; return false; } int EventSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } bool CalendarView::Overlap(LDateTime &Start, LDateTime &End, Calendar *a, Calendar *b) { if (a && b) { if (a->Overlap(b)) { LArray Ap, Bp; a->GetTimes(Start, End, Ap); b->GetTimes(Start, End, Bp); for (unsigned i=0; iFont->Colour(L_TEXT, L_MED); i->Font->Transparent(true); LDisplayString ds(i->Font, (char*)i->Txt); ds.Draw(pDC, i->x, i->y, &r); } void CalendarView::DrawSelectionBox(LSurface *pDC, LRect &r) { int Edge = 4; // int Width = 2; #ifdef WINDOWS int Op = pDC->Op(GDC_XOR); pDC->Colour(Rgba32(0xff, 0xff, 0xff, 0), 32); #else // Other platforms don't have a working XOR operator... so black is // a good default against their default White background. pDC->Colour(Rgb24(0, 0, 0), 24); #endif LRect p; // Top p.Set(r.x1, r.y1, r.x2, r.y1 + Edge - 1); PatternBox(pDC, p); // Bottom p.Set(r.x1, r.y2 - Edge + 1, r.x2, r.y2); PatternBox(pDC, p); // Left p.Set(r.x1, r.y1 + Edge, r.x1 + Edge - 1, r.y2 - Edge); PatternBox(pDC, p); // Right p.Set(r.x2 - Edge + 1, r.y1 + Edge, r.x2, r.y2 - Edge); PatternBox(pDC, p); // WriteDC("c:\\temp\\cal.bmp", pDC); #ifdef WINDOWS pDC->Op(Op); #endif } void CalendarView::OnPaint(LSurface *pDC) { #ifndef MAC // Mac is double buffered anyway LDoubleBuffer Buf(pDC); #endif LColour InMonth(L_WORKSPACE); LColour OutMonth = GdcMixColour(LColour(L_HIGH), LColour(L_WORKSPACE), 0.5); LColour CellEdge(0xc0, 0xc0, 0xc0); LSkinEngine *SkinEngine = LAppInst->SkinEngine; pDC->Colour(Rgb32(255, 255, 255), 32); pDC->Rectangle(); LRect c = GetClient(); c.Offset(-c.x1, -c.y1); float _Sx = 1.0; float _Sy = 1.0; if (pDC->IsPrint()) { c = PrintMargin; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); _Sx = (float)DcDpi.x / ScreenDpi.x; _Sy = (float)DcDpi.y / ScreenDpi.y; } float Scale = _Sx < _Sy ? _Sx : _Sy; SRect(c); Layout = c; if (Mode != CAL_VIEW_YEAR) { Title = c; Title.y2 = Title.y1 + Font->GetHeight() + (int)SY(6); Layout.y1 = Title.y2 + 1; } else { Title.ZOff(-1, -1); } for (auto &c: Current) { c.c->ViewPos.Empty(); } switch (Mode) { /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { // Recalc day start/end int DayVisible = DayEnd - DayStart; DayStart = VScroll ? (int)VScroll->Value() : 6; DayEnd = DayStart + DayVisible; // Setup... LDateTime Dt = Start, Now; Dt.SetTime("0:0:0.0"); Now.SetNow(); LDisplayString ds(Font, "22:00p"); int TimeX = (int)((float)ds.X() + SX(10)); Layout.Set(TimeX, Title.y2 + 1, c.x2, c.y2); // d=0 is the hours column, d=1 is the first day (either sun or mon), etc... d=7 is last day for (int d=0; d<8; d++) { LDateTime Tomorrow = Dt; Tomorrow.AddDays(1); LTimeStamp TodayTs, TomorrowTs; Dt.Get(TodayTs); Tomorrow.Get(TomorrowTs); // Heading int x1 = d ? TimeX + ((d-1) * (c.X()-TimeX) / 7) : 0; int x2 = d ? TimeX + ((d * (c.X()-TimeX) / 7) - 1) : TimeX - 1; LRect p( x1, Title.y1, x2, Title.y2); if (d) { int NameIdx = (FirstDayOfWeek+d-1) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } else { pDC->Colour(L_LOW); pDC->Rectangle(&p); } // Content area p.Set( x1, Title.y2 + 1, x2, c.y2); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); int Divisions = DayEnd - DayStart; int DayOfWeek = Dt.DayOfWeek(); #define HourToY(hour) (p.y1 + (((hour)-(double)DayStart) * p.Y() / Divisions)) for (int h=DayStart; h<=DayEnd; h++) { LRect Hour( x1, (int)HourToY(h), x2, (int)HourToY(h+1)-1); LColour Back = DayOfWeek == 0 || DayOfWeek == 6 ? GdcMixColour(LColour(L_WORKSPACE), WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL) : OutMonth; if (DayOfWeek >= Calendar::WorkWeekStart && DayOfWeek <= Calendar::WorkWeekEnd && h >= Calendar::WorkDayStart && h < Calendar::WorkDayEnd) Back = InMonth; /* bool Select = Focus() && Cur->Day() == Dt.Day() && Cur->Hours() == h; if (Select) { Back = LC_FOCUS_SEL_BACK; } */ bool Today = Now.Day() == Dt.Day() && Now.Month() == Dt.Month() && Now.Year() == Dt.Year(); if (Today) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } if (d) { // Draw hourly blocks // Background pDC->Colour(Back); pDC->Rectangle(Hour.x1, Hour.y1, Hour.x2-1, Hour.y2-1); pDC->Colour(CellEdge); pDC->Line(Hour.x1, Hour.y2, Hour.x2, Hour.y2); // Date at the top.. if (h == DayStart) { char s[32]; Dt.GetDate(s, sizeof(s)); Font->Colour(/*Select ? LC_FOCUS_SEL_FORE :*/ L_LOW, L_MED); Font->Transparent(true); LDisplayString ds(Font, s); ds.Draw(pDC, Hour.x1 + 2, Hour.y1 + 2); } } else { // Draw times in the first column char s[32]; if (Now.GetFormat() & GDTF_24HOUR) sprintf_s(s, sizeof(s), "%i:00", h); else sprintf_s(s, sizeof(s), "%i:00%c", h == 0 ? 12 : h > 12 ? h - 12 : h, h >= 12 ? 'p' : 'a'); LRect Temp = Hour; LWideBorder(pDC, Temp, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, s); ds.Draw(pDC, Temp.x1 + (int)SX(2), Temp.y1, &Temp); } } if (d) { // Draw events LArray All; for (auto &t: Current) { if (t.Overlap(Dt, Tomorrow)) All.Add(t); } All.Sort(EventSorter); // Sort the entries into overlapping groups typedef LArray EventArray; LArray Groups; for (uint32_t e=0; e 0) { // Check if it overlaps anything in the previous group EventArray *a = Groups[Groups.Length()-1]; for (uint32_t i=0; iLength(); i++) { TimePeriod *t = (*a)[i]; if (t->Overlap(*Ev)) { // It does... so add it. a->Add(Ev); Ev = 0; break; } } } if (Ev) { // Create new group EventArray *g = new EventArray; if (g) { g->Add(Ev); Groups.Add(g); } } } // Lay the groups of entries out and then paint them for (uint32_t g=0; gGetObject(); auto allDay = obj ? obj->GetInt(FIELD_CAL_ALL_DAY) : false; double StartH = (double) t.s.Hours() + ((double)t.s.Minutes() / 60), EndH; if (t.e.IsSameDay(t.s) && t.e.Hours()) EndH = ((double)t.e.Hours()) + ((double)t.e.Minutes() / 60); else EndH = 24; int x1 = p.x1 + (int)SX(4); int x2 = p.x2 - (int)SX(5); double dx = x2 - x1 + SX(3); int StartX = x1 + (int)((double)i * dx / (double)Group.Length()); int EndX = StartX + (int)((dx / (double)Group.Length()) - SX(5)); LRect Vp( StartX, (int)HourToY(StartH) - 1, EndX, (int)HourToY(EndH) - 3); // LgiTrace("paint: %s %s\n", Vp.GetStr(), t.c->ToString().Get()); t.c->OnPaintView(pDC, Font, &Vp, &t); } } // Clean up the memory Groups.DeleteObjects(); for (unsigned i=0; i TomorrowTs) EndSec = (int)((TomorrowTs - TodayTs) / LDateTime::Second64Bit); else EndSec = (int)((rng.EndTs - TodayTs) / LDateTime::Second64Bit); int StartY = (int) HourToY((double)StartSec / LDateTime::HourLength); int EndY = (int) HourToY((double)EndSec / LDateTime::HourLength); /* printf("%f,%f - %i,%i - %i,%i\n", (double)StartSec / LDateTime::HourLength, (double)EndSec / LDateTime::HourLength, StartY, EndY, DayStart, Divisions); */ LRect r(p.x1 + 4, StartY, p.x2 - 4, EndY); DrawSelectionBox(pDC, r); } } // Increment date of day we're painting Dt = Tomorrow; } } break; } case CAL_VIEW_MONTH: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; int ObjY = Font->GetHeight() + (int)SY(4); LDateTime i = Start, Now, Tomorrow; Now.SetNow(); i.SetTime("0:0:0.0"); Tomorrow = i; Tomorrow.AddDays(1); char Str[256]; for (int h=0; h<7; h++) { LRect p( Title.x1 + (h * Title.X() / MonthX), Title.y1, Title.x1 + (((h+1) * Title.X() / MonthX) - 1), Title.y2); int NameIdx = (h + FirstDayOfWeek) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } for (int y=0; yMonth(); bool Today = Now.Day() == i.Day() && Now.Month() == i.Month() && Now.Year() == i.Year(); if (i.Day() == Cur->Day() && i.Month() == Cur->Month() && i.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Font->Fore(L_FOCUS_SEL_FORE); } else { // normal day Back = (IsInMonth) ? InMonth : OutMonth; Font->Fore(L_TEXT); } if (DayOfWeek == 0 || DayOfWeek == 6) Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); if (Today) Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); int Edge = (int)SX(1); if (!pDC->IsPrint() || Back != LColour(L_WORKSPACE)) { pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-Edge, p.y2-Edge); } pDC->Colour(CellEdge); pDC->Rectangle(p.x2-Edge+1, p.y1, p.x2, p.y2); pDC->Rectangle(p.x1, p.y2-Edge+1, p.x2-Edge, p.y2); if (pDC->IsPrint() && x == 0) { pDC->Rectangle(p.x1, p.y1, p.x1+Edge, p.y2-Edge); } Font->Transparent(true); Font->Back(Back); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SX(2)); LRect Clip = p; Clip.Inset(Edge, Edge); pDC->ClipRgn(&Clip); int CalY = ObjY + (int)SY(2); LArray All; uint32_t n; for (n=0; nGetTimes(i, Tomorrow, All); } All.Sort(EventSorter); for (n=0; nOnPaintView(pDC, Font, &Vp, &t); CalY += ObjY + (int)SY(2); } for (auto rng : Ranges) { if (rng.Overlap(i.Ts(), Tomorrow.Ts())) { LRect Vp(p.x1 + (int)SX(3), p.y1 + CalY, p.x2 - (int)SX(4), p.y1 + CalY + ObjY); DrawSelectionBox(pDC, Vp); CalY += ObjY + (int)SY(2); break; } } pDC->ClipRgn(0); i = Tomorrow; Tomorrow.AddDays(1); } } break; } case CAL_VIEW_YEAR: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; // int ObjY = Font->GetHeight() + (int)SY(2); LDateTime i = Start, Now, Tomorrow; YearView v(Cur); Now.SetNow(); i.Hours(0); i.Minutes(0); i.Seconds(0); i.Thousands(0); Tomorrow = i; Tomorrow.AddDays(1); Layout.x1 += (int)SX(BORDER_YEAR); int Fy = Font->GetHeight(); char Str[256]; for (int y=0; yTransparent(false); Font->Colour(L_BLACK, L_MED); LDisplayString ds(Font, (char*)ShortMonthNames[y]); ds.Draw(pDC, T.x1 + (int)SX(2), T.y1, &T); for (int x=0; xDay() && t.Month() == Cur->Month() && t.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Fore = LColour(L_FOCUS_SEL_FORE); } else { // Other day.. Fore = LColour(L_LOW); Back = v.IsMonth() ? InMonth : OutMonth; } // Weekend tint int Day = t.DayOfWeek(); if (Day == 0 || Day == 6) { Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); } // Today tint.. if (v.IsMonth() && Now.Day() == t.Day() && Now.Month() == t.Month() && Now.Year() == t.Year()) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } // Fill cell background pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-1, p.y2-1); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); pDC->Line(p.x1, p.y2, p.x2, p.y2); // Draw day number if (v.IsMonth()) { Font->Transparent(true); Font->Colour(Fore, Back); sprintf_s(Str, sizeof(Str), "%i", t.Day()); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1+1, p.y1-1); } // Paint events LArray e; LDateTime Tomorrow(t); Tomorrow.AddDays(1); int Cy = p.y1 + Fy; if (v.IsMonth() && GetEventsBetween(e, t, Tomorrow)) { LRect Safe = p; Safe.y2--; for (uint32_t i=0; i Safe.y2) { c->ViewPos.ZOff(-1, -1); } else { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); Vp.Bound(&Safe); c->OnPaintView(pDC, Font, &Vp, &e[i]); Cy += Fy + 1; } } } for (auto rng : Ranges) { if (rng.Overlap(t.Ts(), Tomorrow.Ts())) { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); DrawSelectionBox(pDC, Vp); Cy += Fy + 1; break; } } } } break; } default: { pDC->Colour(L_WHITE); pDC->Rectangle(); break; } } } bool CalendarView::OnKey(LKey &k) { switch (k.vkey) { case LK_ESCAPE: { if (IsCapturing() && k.Down()) { DragStart.Empty(); DragEnd.Empty(); Ranges.Length(0); DragMode = DragNone; Invalidate(); } return true; } default: { switch (k.c16) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down() && GetWindow()) { GetWindow()->Quit(); return true; } break; } } } } /* if (k.c16 != 17 && k.Down()) { // Arrow behaviour CalViewArrow *Arrow = GetModeArrow(Mode, k); if (Arrow) { LDateTime c = Cursor; c.AddHours(Arrow->Hours); c.AddDays(Arrow->Days); c.AddMonths(Arrow->Months); c.AddMonths(Arrow->Years * 12); SetCursor(c); return true; } // Other commands switch (k.c16) { case LK_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { OnDelete(); } return true; break; } } } */ return false; } void CalendarView::OnDelete() { Calendar *c; while ((c=Selection.First())) { c->OnDelete(); Selection.Delete(c); } } Calendar *CalendarView::CalendarAt(int x, int y) { switch (Mode) { default: LAssert(0); break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: case CAL_VIEW_MONTH: case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { for (uint32_t i=0; iViewPos.Overlap(x, y)) { return Current[i].c; } } } break; } } return 0; } LDateTime *CalendarView::TimeAt(int x, int y, int SnapMinutes, LPoint *Cell) { LPoint cell; if (!Cell) Cell = &cell; Cell->x = -1; Cell->y = -1; switch (Mode) { default: break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { int64 Page = (int)(Calendar::DayEnd - Calendar::DayStart); int FirstHr = 0; if (VScroll) { FirstHr = (int) VScroll->Value(); Page = VScroll->Page(); } Cell->x = ((x - Layout.x1) * 7) / Layout.X(); // day in week int MinuteOffset = ((y - Layout.y1) * (int)Page * 60) / Layout.Y(); Cell->y = ((y - Layout.y1) * (int)Page) / Layout.Y(); // hour of day LAssert(MinuteOffset / 60 == Cell->y); int Minutes = (FirstHr * 60) + MinuteOffset; if (SnapMinutes) { int Snap = Minutes % SnapMinutes; if (Snap) { Minutes -= Snap; } } *Day = Start; Day->AddDays(Cell->x); Day->Hours(Minutes / 60); Day->Minutes(Minutes % 60); Day->Seconds(0); Day->Thousands(0); return Day; } } break; } case CAL_VIEW_MONTH: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { Cell->x = ((x - Layout.x1) * MonthX) / Layout.X(); Cell->y = ((y - Layout.y1) * MonthY) / Layout.Y(); *Day = Start; Day->AddDays( (Cell->y * MonthX) + Cell->x ); Day->SetTime("0:0:0"); return Day; } } break; } case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { YearView v(&Cursor); Cell->x = (x - Layout.x1) * v.X() / Layout.X(); Cell->y = (y - Layout.y1) * v.Y() / Layout.Y(); v.SetCursor(Cell->x, Cell->y); *Day = v.Get(); Day->SetTime("0:0:0"); return Day; } } break; } } return 0; } LDateTime::LDstInfo *CalendarView::GetDstForDate(LDateTime t) { auto ts = t.Ts(); for (uint32_t i=0; i= Prev.Ts() && ts < Next.Ts()) { return &Dst[i]; } } else if (ts > Dst[i].Utc) { return &Dst[i]; } } return 0; } bool CalendarView::HitTest(int x, int y, EventDragMode &mode, Calendar *&event) { if (!Layout.Overlap(x, y)) return false; for (uint32_t i=0; iViewPos.First(); r; r = c->ViewPos.Next()) { if (x >= r->x1 && x <= r->x2) { if (abs(y-r->y1) < THRESHOLD_EDGE) { event = c; mode = DragMoveStart; return true; } if (abs(y-r->y2) < THRESHOLD_EDGE) { event = c; mode = DragMoveEnd; return true; } } } } if (c->ViewPos.Overlap(x, y)) { event = c; mode = DragMoveSelection; return true; } } return false; } Calendar *CalendarView::NewEvent(LDateTime &dtStart, LDateTime &dtEnd) { auto Src = FolderCalendarSource::GetCreateIn(); if (!Src) return NULL; LError createErr; auto c = Src->NewEvent(&createErr); if (!c) { LPopupNotification::Message ( GetWindow(), LString::Fmt("Can't create event in %s: %s", Src->GetClass(), createErr.ToString().Get()) ); return NULL; } LDateTime Start, End; if (dtStart < dtEnd) { Start = dtStart; End = dtEnd; } else { Start = dtEnd; End = dtStart; } LDateTime n = Start; auto CurDst = GetDstForDate(Start); if (CurDst) n.SetTimeZone(CurDst->Offset, false); char s[64]; sprintf_s(s, sizeof(s), "%+.1f", (double)n.GetTimeZone() / 60.0); c->SetField(FIELD_CAL_TIMEZONE, s); Start.ToUtc(true); c->SetField(FIELD_CAL_START_UTC, Start); End.ToUtc(true); c->SetField(FIELD_CAL_END_UTC, End); c->OnCreate(); return c; } void CalendarView::OnMouseClick(LMouse &m) { EventDragMode HitMode = DragNone; Calendar *c = NULL; HitTest(m.x, m.y, HitMode, c); bool AlreadySelected = (c) ? Selection.HasItem(c) : false; LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); /* if (Hit) printf("Hit=%s\n", Hit->Get().Get()); */ if (m.IsContextMenu()) { Focus(true); // Select the item if not already selected when asking for a context menu. if (c && !Selection.HasItem(c)) { Selection.Add(c); Invalidate(&c->ViewPos); } if (!c) { char sHit[64] = ""; if (Hit) Hit->GetTime(sHit, sizeof(sHit)); LString NewMsg; NewMsg.Printf("New event at %s", sHit); LSubMenu s; s.AppendItem(NewMsg, IDM_NEW_EVENT); LSubMenu *snap = s.AppendSub("Snap"); if (snap) { LMenuItem *it = snap->AppendItem("15 minutes", IDM_15MIN); if (it && SnapMinutes == 15) it->Checked(true); it = snap->AppendItem("30 minutes", IDM_30MIN); if (it && SnapMinutes == 30) it->Checked(true); it = snap->AppendItem("1 hour", IDM_1HR); if (it && SnapMinutes == 60) it->Checked(true); } m.ToScreen(); int Cmd = s.Float(this, m); switch (Cmd) { case IDM_NEW_EVENT: { LDateTime End = *Hit; End.AddHours(1); Calendar *ev = NewEvent(*Hit, End); if (ev) ev->DoUI(); break; } case IDM_15MIN: { SnapMinutes = 15; break; } case IDM_30MIN: { SnapMinutes = 30; break; } case IDM_1HR: { SnapMinutes = 60; break; } } } } else { Capture(m.Down()); if (m.Down()) { Focus(true); DragEvent = NULL; if (m.Left()) { Ranges.Length(0); if (!AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } ClickPt.x = m.x; ClickPt.y = m.y; if (Hit) { DragStart = *Hit; if (HitMode == DragMoveStart) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); r.StartTs = DragStart; r.EndTs = DragEnd; } else if (HitMode == DragMoveEnd) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); r.StartTs = DragStart; r.EndTs = DragEnd; } else if (Selection.Length()) { DragMode = DragMoveSelection; DragEnd = *Hit; for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); r.StartTs = dt.Ts().Get(); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else { r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); } // Does this new range overlap any existing range? for (unsigned n=0; n %s\n", DragStart.Get().Get(), DragEnd.Get().Get()); TsRange &r = Ranges.New(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } } else { DragStart.Empty(); DragEnd.Empty(); } Invalidate(); } } else // up { if (DragStart.IsValid() && DragEnd.IsValid()) { switch (DragMode) { case DragNewEvent: { // New event... // printf("DragStart=%s, DragEnd=%s\n", DragStart.Get().Get(), DragEnd.Get().Get()); c = NewEvent(DragStart, DragEnd); if (c) c->DoUI(); OnContentsChanged(NULL); break; } case DragMoveSelection: { // Adjust the times of the selection by the offset. int64 TsOffset = DragEnd.Ts() - DragStart.Ts(); if (TsOffset != 0) { // TsOffset = 0; for (unsigned i=0; iGetObject(); LDateTime start_dt = *o->GetDate(FIELD_CAL_START_UTC); start_dt.Set(start_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_START_UTC, &start_dt); LDateTime end_dt = *o->GetDate(FIELD_CAL_END_UTC); end_dt.Set(end_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_END_UTC, &end_dt); o->SetInt(FIELD_STATUS, Store3Delayed); s->SetDirty(); } OnContentsChanged(NULL); } break; } case DragMoveStart: { if (DragEvent) { if (DragEnd > DragStart) { LDateTime dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } case DragMoveEnd: { if (DragEvent) { if (DragEnd < DragStart) { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } default: break; } } if (AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } if (DragStart.IsValid() || Ranges.Length()) { Ranges.Length(0); DragStart.Empty(); DragEnd.Empty(); Invalidate(); } DragEvent = NULL; } } if (c) { c->OnMouseClick(m); } } void CalendarView::OnMouseMove(LMouse &m) { if (IsCapturing()) { LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); if ( Hit && ( abs(m.x - ClickPt.x) > 4 || abs(m.y - ClickPt.y) > 4 ) ) { #if 1 switch (DragMode) { default: break; case DragMoveStart: case DragMoveEnd: case DragNewEvent: { LDateTime End = *Hit; if (End >= DragStart) End.AddMinutes(SnapMinutes); if (End != DragEnd) { DragEnd = End; if (DragStart < DragEnd) { DragStart.Get(Ranges[0].StartTs); DragEnd.Get(Ranges[0].EndTs); } else { DragStart.Get(Ranges[0].EndTs); DragEnd.Get(Ranges[0].StartTs); } Invalidate(); } break; } case DragMoveSelection: { DragEnd = *Hit; auto TsOffset = DragEnd.Ts() - DragStart.Ts(); if (LastTsOffset != TsOffset) { LastTsOffset = TsOffset; Ranges.Length(0); for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); dt.Get(r.StartTs); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); r.StartTs += TsOffset; r.EndTs += TsOffset; // Does this new range overlap any existing range? for (unsigned n=0; nValue(); v += (int) ceil(Lines / 3.0); VScroll->Value(v); return true; } LCursor CalendarView::GetCursor(int x, int y) { EventDragMode mode = DragNone; Calendar *event = NULL; if (HitTest(x, y, mode, event)) { if (mode == DragMoveStart || mode == DragMoveEnd) return LCUR_SizeVer; } return LView::GetCursor(x, y); } void CalendarView::OnFocus(bool f) { Invalidate(); } ////////////////////////////////////////////////////////////////////////////// int CalendarView::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; LDateTime *Hit = TimeAt(Pt.x, Pt.y, SnapMinutes); SelectDropTarget(Hit); if (Hit) { DeleteObj(Hit); if (Formats.HasFormat(ScribeCalendarObject)) Formats.Supports(ScribeCalendarObject); else Formats.SupportsFileDrops(); if (Formats.GetSupported().Length()) Status = DROPEFFECT_MOVE; } return Status; } void CalendarView::OnDragExit() { SelectDropTarget(); } int CalendarView::OnDrop(LArray &Data, LPoint Pt, int KeyState) { - for (unsigned di=0; di in(new LFile); auto type = LGetFileMimeType(f); - if (in->Open(f, O_READ)) + LString errMsg; + LError createErr; + + if (!in->Open(f, O_READ)) { - LError createErr; - auto c = First->NewEvent(&createErr); - if (c) + errMsg.Printf("Failed to open '%s' for reading.", f); + } + else if (auto c = First->NewEvent(&createErr)) + { + if (c->Import(c->AutoCast(in), type)) { - if (c->Import(c->AutoCast(in), type)) - { - c->Save(); - Invalidate(); - } - else - { - delete c; - } + c->Save(); + Invalidate(); } else - LgiTrace("%s:%i - Failed to create calendar event in %s: %s\n", - _FL, + { + errMsg.Printf("Calendar import failed: %s", c->ErrMsg.Get()); + c->DecRef(); + } + } + else + { + errMsg.Printf("Failed to create calendar event in %s: %s", First->GetClass(), createErr.ToString().Get()); } + + if (errMsg) + { + LPopupNotification::Message(GetWindow(), errMsg); + LgiTrace("%s:%i - %s\n", _FL, errMsg.Get()); + } } } else LgiTrace("%s:%i - Do data sources?\n", _FL); } - else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) + else if (dd.IsFormat(ScribeCalendarObject)) { if (dd.Data.Length() == 0) continue; + LVariant *v = &dd.Data[0]; if (v->Type == GV_BINARY && v->Value.Binary.Length >= sizeof(NativeInt)*2 && DragStart.IsValid()) { NativeInt *d = (NativeInt*) v->Value.Binary.Data; if (d[0] == MAGIC_CALENDAR && d[1] > 0) { char Str[32]; if (Mode == CAL_VIEW_WEEK || Mode == CAL_VIEW_DAY) { DragStart.Get(Str, sizeof(Str)); } else { DragStart.GetDate(Str, sizeof(Str)); } Calendar **c = (Calendar**) (d + 2); for (int i=0; iGetField(FIELD_CAL_START_UTC, s)) { bool HasEnd = c[i]->GetField(FIELD_CAL_END_UTC, e); LDateTime Diff; if (HasEnd) Diff = e - s; else Diff.Hours(1); s.Set(Str); c[i]->SetField(FIELD_CAL_START_UTC, s); if (HasEnd) { e = s + Diff; c[i]->SetField(FIELD_CAL_END_UTC, e); } c[i]->Save(); } } SetCursor(DragStart); } } } } SelectDropTarget(); return 0; } void CalendarView::OnDragInit(bool Success) { } char *CalendarView::TypeOf() { return 0; } bool CalendarView::GetData(LArray &Data) { if (Selection.Length() <= 0) return false; bool Status = false; for (unsigned di=0; di(s); if (t) { Status |= t->GetDropFiles(Files); } } if (Status) { LMouse m; GetMouse(m, true); Status |= CreateFileDrop(&dd, m, Files); } } else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) { ssize_t Size = (2 + Selection.Length()) * sizeof(NativeInt); LArray d; if (d.Length(Size)) { d[0] = MAGIC_CALENDAR; d[1] = Selection.Length(); int n=0; Calendar **l = (Calendar**) (&d[2]); for (unsigned i=0; i 0; } class CalendarViewPrint : public LPrintEvents { CalendarView *cv; public: CalendarViewPrint(CalendarView *v) { cv = v; } bool OnPrintPage(LPrintDC *pDC, int PageIndex) { return cv->OnPrintPage(pDC, PageIndex); } }; ////////////////////////////////////////////////////////////////////////////// LMonthView::LMonthView(int Id, LDateTime *n, CalendarView *calView) : MonthView(n) { FirstDayOfWeek = CalendarView::FirstDayOfWeek; SetId(Id); rTitle.ZOff(-1, -1); rCells.ZOff(-1, -1); Cell = 1; CalView = calView; Set(n); } void LMonthView::OnCellClick(int Cx, int Cy) { SetCursor(Cx, Cy); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::SeekMonth(int Dir) { LDateTime t = Cursor; t.AddMonths(Dir); Set(&t); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { } else if (m.Left() && m.Down()) { if (rCells.Overlap(m.x, m.y)) { int x = (m.x - rCells.x1) / Cell; int y = (m.y - rCells.y1) / Cell; OnCellClick(x, y); } else if (rLeft.Overlap(m.x, m.y)) { SeekMonth(-1); } else if (rRight.Overlap(m.x, m.y)) { SeekMonth(1); } } } void LMonthView::OnPaint(LSurface *pDC) { pDC->Colour(L_WORKSPACE); pDC->Rectangle(); Cell = LView::X() / MonthView::X(); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); LRect Client = GetClient(); LDisplayString n(LSysBold, Title()); LSysBold->Colour(L_TEXT, L_WORKSPACE); LSysBold->Transparent(true); n.Draw(pDC, 6, 0); rTitle.ZOff(Client.X()-1, n.Y()*3/2-1); rRight = rTitle; rRight.x1 = rRight.x2 - LSysFont->GetHeight() + 1; rLeft = rRight; rLeft.Offset(-rLeft.Y(), 0); rCells.ZOff(Cell * MonthView::X(), Cell * MonthView::Y()); rCells.Offset(0, rTitle.Y()); LDisplayString DsLeft(LSysFont, "<"); DsLeft.Draw(pDC, rLeft.x1, rLeft.y1); LDisplayString DsRight(LSysFont, ">"); DsRight.Draw(pDC, rRight.x1, rRight.y1); LDateTime t = Start; auto Mode = CalView->GetViewMode(); auto CursorPos = MonthView::GetCursor(); for (int y=0; yColour(L_FOCUS_SEL_FORE, L_FOCUS_SEL_BACK); else { LColour Fore, Back; if (InMonth) { Fore = LColour(L_TEXT); Back.Rgb(0xf7, 0xf7, 0xf7); } else { Fore.Rgb(192, 192, 192); Back = LColour(L_WORKSPACE); } if (Mode == CAL_VIEW_WEEK && y == CursorPos.y) { Back = Back.Mix(LColour(L_FOCUS_SEL_BACK), 0.1f); } LSysFont->Colour(Fore, Back); } LRect r; r.ZOff(Cell-2, Cell-2); r.Offset(x*Cell, y*Cell+rCells.y1); int Cx = Cell - Ds.X(); int Cy = Cell - Ds.Y(); Ds.Draw(pDC, r.x1+(Cx>>1), r.y1+(Cy>>1), &r); if (!t.AddDays(1)) { LAssert(!"Add days failed."); break; } } } } ////////////////////////////////////////////////////////////////////////////// LArray CalendarViewWindows; CalendarViewWnd::CalendarViewWnd(ScribeFolder *folder) { CalendarViewWindows.Add(this); App = folder ? folder->App : 0; Cv = 0; Split = 0; Todo = 0; HorBox = NULL; VerBox = NULL; CalLst = NULL; MonthV = NULL; Name("Calendar View"); if (!SerializeState(App->GetOptions(), OPT_CalendarViewPos, true)) { LRect p(0, 0, 600, 500); SetPos(p); MoveToCenter(); } LDateTime Now; Now.SetNow(); OnOptionsChange(); #if !defined(WINDOWS) SetIcon("_cal.png"); #endif auto ToolBar = folder->App->LoadToolbar(this, folder->App->GetResourceFile(ResToolbarFile), folder->App->GetToolbarImgList()); if (ToolBar) { AddView(ToolBar); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_WEEK)), IDM_CAL_WEEK, TBT_RADIO, true, IMG_CAL_WEEK); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_MONTH)), IDM_CAL_MONTH, TBT_RADIO, true, IMG_CAL_MONTH); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_YEAR)), IDM_CAL_YEAR, TBT_RADIO, true, IMG_CAL_YEAR); ToolBar->AppendSeparator(); ToolBar->AppendButton(0, IDM_PREV, TBT_PUSH, true, IMG_CAL_PREV); ToolBar->AppendButton(0, IDM_BACK, TBT_PUSH, true, IMG_CAL_BACK); ToolBar->AppendButton(LLoadString(IDS_TODAY), IDM_TODAY, TBT_PUSH, true, IMG_CAL_TODAY); ToolBar->AppendButton(0, IDM_FORWARD, TBT_PUSH, true, IMG_CAL_FORWARD); ToolBar->AppendButton(0, IDM_NEXT, TBT_PUSH, true, IMG_CAL_NEXT); ToolBar->AppendSeparator(); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_TODO)), IDM_TODO, TBT_TOGGLE, true, IMG_CAL_TODO); // ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_CONFIGURE)), IDM_CONFIG, TBT_PUSH, true, IMG_CAL_CONFIG); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); } // Month control #if defined(WINDOWS) int ColPixels = 160; #else int ColPixels = 180; #endif LCss::Len ColPx(LCss::LenPx, (float)ColPixels); LCss::Len Auto("auto"); LCss::Len Pad("10px"); Cv = new CalendarView(folder, IDC_CALENDAR, NULL, "Calendar View"); AddView(HorBox = new LBox); HorBox->AddView(VerBox = new LBox); HorBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->SetVertical(true); VerBox->GetCss(true)->Width(ColPx); VerBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->AddView(MonthV = new LMonthView(IDC_MONTH_VIEW, &Now, Cv)); MonthV->GetCss(true)->Height(ColPx); LRect r(0, 0, ColPixels-1, ColPixels-1); MonthV->SetPos(r); // c->Padding(Pad); // List of calendar sources... VerBox->AddView(CalLst = new LList(IDC_LIST, 0, 0, 100, 100, "Calendar Sources")); CalLst->AddColumn("x", 20); CalLst->AddColumn("Calendar", 150); // Main layout view HorBox->AddView(Cv); if (Cv) { Cv->OnCursorChange(false, true, false); Cv->Visible(true); switch (Cv->GetViewMode()) { default: break; case CAL_VIEW_WEEK: SetCtrlValue(IDM_CAL_WEEK, 1); break; case CAL_VIEW_MONTH: SetCtrlValue(IDM_CAL_MONTH, 1); break; case CAL_VIEW_YEAR: SetCtrlValue(IDM_CAL_YEAR, 1); break; } } if (Cv && CalLst) { for (unsigned i=0; i(s); CalLst->Insert(Li); bool IsCreateIn = FolderCalendarSource::GetCreateIn() == s; Li->Select(IsCreateIn); } } #if WINNATIVE CreateClassW32("Calendar", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_CALENDER))); #endif if (Attach(0)) { AttachChildren(); Visible(true); LVariant ViewTodo; App->GetOptions()->GetValue(OPT_CalendarViewTodo, ViewTodo); SetCtrlValue(IDM_TODO, ViewTodo.CastInt32()); if (ViewTodo.CastInt32()) { // Layout(); } } } CalendarViewWnd::~CalendarViewWnd() { if (CalLst) CalLst->RemoveAll(); // The CalendarView owns the list items. SerializeState(App->GetOptions(), OPT_CalendarViewPos, false); LVariant s; App->GetOptions()->SetValue(OPT_CalendarViewTodo, s = (int)GetCtrlValue(IDM_TODO)); CalendarViewWindows.Delete(this); } void CalendarViewWnd::OptionsChange() { LVariant v; int FirstDayOfWeek = 0; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); if (MonthV) { MonthV->FirstDayOfWeek = CalendarView::FirstDayOfWeek; MonthV->Invalidate(); } } void CalendarViewWnd::OnOptionsChange() { if (!CalendarView::App && CalendarViewWindows.Length() > 0) { CalendarView::App = CalendarViewWindows[0]->App; } CalendarView::OnOptionsChange(); for (auto w: CalendarViewWindows) w->OptionsChange(); } bool CalendarViewWnd::OnKey(LKey &k) { switch (k.vkey) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down()) { Quit(); return true; } break; } } return LWindow::OnKey(k); } int CalendarViewWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { CalViewArrow *Va = 0; switch (Cmd) { case IDM_CAL_DAY: { Cv->SetViewMode(CAL_VIEW_DAY); break; } case IDM_CAL_WEEK: { Cv->SetViewMode(CAL_VIEW_WEEK); break; } case IDM_CAL_MONTH: { Cv->SetViewMode(CAL_VIEW_MONTH); break; } case IDM_CAL_YEAR: { Cv->SetViewMode(CAL_VIEW_YEAR); break; } case IDM_PREV: { LKey k; k.c16 = LK_PAGEUP; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_BACK: { LKey k; k.c16 = LK_PAGEUP; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_TODAY: { LDateTime Dt; Dt.SetNow(); Dt.Minutes(0); Dt.Seconds(0); Dt.Thousands(0); Cv->SetCursor(Dt); break; } case IDM_FORWARD: { LKey k; k.c16 = LK_PAGEDOWN; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_NEXT: { LKey k; k.c16 = LK_PAGEDOWN; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_PRINT: { auto *Printer = Cv && App ? App->GetPrinter() : NULL; if (Printer) { CalendarViewPrint Cvp(Cv); Printer->Print(&Cvp, NULL, "Scribe Calendar", -1, this); } break; } case IDM_HELP: { App->LaunchHelp("calendar.html"); break; } } if (Va) { LDateTime Cursor = Cv->GetCursor(); Cursor.AddHours(Va->Hours); Cursor.AddDays(Va->Days); Cursor.AddMonths(Va->Months); Cursor.AddMonths(Va->Years * 12); Cv->SetCursor(Cursor); } return 0; } LMessage::Result CalendarViewWnd::OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHANGE: { if (m->A() == IDC_LIST) { // One of the calendar source's has changed... // Check it's still in our list and 'valid' CalendarSource *s = (CalendarSource*)m->B(); LArray All; CalLst->GetAll(All); if (All.IndexOf(s) >= 0) Cv->OnContentsChanged(s); } break; } #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(m); } LString CalendarViewWnd::LoadString(int id) { return LString(LLoadString(id)).Replace("&",""); } LString CalendarViewWnd::UnusedKey() { LString Key; // Find an unused index for the new source... while (true) { Key.Printf("%s.Source-%i", OPT_CalendarSources, LRand(1000)); if (App->GetOptions()->LockTag(Key, _FL)) App->GetOptions()->Unlock(); else break; } return Key; } int CalendarViewWnd::OnNotify(LViewI *c, LNotification n) { static bool Processing = false; if (Processing) return 0; Processing = true; switch (c->GetId()) { case IDC_CALENDAR: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t = Cv->GetCursor(); MonthV->Set(&t); MonthV->Invalidate(); } break; } case IDC_MONTH_VIEW: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t; t = MonthV->Get(); Cv->SetCursor(t); } break; } case IDC_LIST: { if (!CalLst) break; switch (n.Type) { default: break; case LNotifyValueChanged: { if (Cv) { auto src = dynamic_cast(CalLst->GetSelected()); if (src) Cv->OnContentsChanged(src); else LAssert(0); } break; } case LNotifyItemContextMenu: { LMouse m; GetMouse(m); m.ToScreen(); LListItem *Sel = CalLst->GetSelected(); LSubMenu s; if (Sel) { LSubMenu *ColMenu = s.AppendSub("Colour"); if (ColMenu) { BuildMarkMenu( ColMenu, MS_None, 0); } } s.AppendItem(LLoadString(IDS_ADD_LOCAL_CAL_FOLDER), IDM_ADD_LOCAL_CAL); s.AppendItem(LLoadString(IDS_ADD_CAL_URL), IDM_ADD_CAL_URL); s.AppendSeparator(); s.AppendItem(LoadString(IDS_EDIT), IDM_EDIT, Sel != NULL); s.AppendItem(LoadString(IDS_DELETE), IDM_DELETE, Sel != NULL); int Id = s.Float(this, m); switch (Id) { case IDM_ADD_LOCAL_CAL: { auto Dlg = new FolderDlg(this, App, MAGIC_CALENDAR); Dlg->DoModal([this, Dlg](auto dlg, auto ok) { if (ok) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); // Create the source... FolderCalendarSource *cs = new FolderCalendarSource(App, Parts.Last()); if (cs) { cs->SetPath(Dlg->Get()); cs->SetColour(CalendarSource::FindUnusedColour()); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dlg; }); break; } case IDM_ADD_CAL_URL: { auto dlg = new LInput(this, "", LLoadString(IDC_CAL_URL, "Calendar URL:"), AppName); dlg->DoModal([this, dlg](auto dialog, auto ok) { if (ok) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); auto Url = dlg->GetStr(); // Create the source... RemoteCalendarSource *cs = new RemoteCalendarSource(App, Parts.Last()); if (cs) { cs->SetColour(CalendarSource::FindUnusedColour()); cs->SetUri(Url); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dialog; }); break; } case IDM_EDIT: { CalendarSource *Src = dynamic_cast(Sel); if (!Src) break; Src->EditPath(this, Cv); break; } case IDM_DELETE: { CalendarSource *Src = dynamic_cast(Sel); if (Cv && Src) { auto *Lst = Src->LListItem::GetList(); if (Lst) Lst->Remove(Src); Cv->DeleteSource(Src); } break; } default: { int Idx = Id - IDM_MARK_BASE; if (Idx >= 0 && Idx < CountOf(MarkColours32)) { LColour Mc(MarkColours32[Idx], 32); CalendarSource *Src = dynamic_cast(Sel); if (Src) { Src->SetColour(Mc); Src->Write(); } } break; } } break; } } break; } case IDC_TODO: { if (Todo && n.Type == LNotifyItemColumnClicked) { int Col = 0; LMouse m; if (Todo->GetColumnClickInfo(Col, m)) { int Sort = 0; for (int i=0; iGetColumns(); i++) { LItemColumn *c = Todo->ColumnAt(i); if (c) { if (i == Col) { if (c->DownArrow()) { c->UpArrow(true); Sort = -(i + 1); } else { c->DownArrow(true); Sort = i + 1; } } else { c->DownArrow(false); c->UpArrow(false); } } } Todo->Sort(TodoCompare, Sort); } } break; } } Processing = false; return 0; } ////////////////////////////////////////////////////////////////////////////// void OpenCalender(ScribeFolder *folder) { if (!CalendarView::CalendarViews.Length()) { new CalendarViewWnd(folder); } } void CalendarSource::FolderDelete(ScribeFolder *f) { for (auto s: AllSources) s->OnFolderDelete(f); } LColour CalendarSource::FindUnusedColour() { // MarkColours32 LArray Used; Used.Length(IDM_MARK_MAX); for (auto &s: AllSources) { auto c = s->GetColour(); uint32_t c32 = c.c32(); for (int i=0; i sources, CalendarSource::GetEventCb callback) : LView::ViewEventTarget(app, M_CALENDAR_SOURCE_STATE), Owner(owner) { *Owner = this; Sources = sources; Start = start; End = end; Callback = callback; PostEvent(M_CALENDAR_SOURCE_STATE); } CalendarSourceGetEvents::~CalendarSourceGetEvents() { *Owner = NULL; } void CalendarSourceGetEvents::OnState() { if (Sources.Length() == 0) { //LgiTrace("CalendarSourceGetEvents: finished...\n"); if (Callback) Callback(Events); delete this; } else if (auto src = Sources[0]) { Sources.DeleteAt(0); if (!App) App = src->GetApp(); //LgiTrace("CalendarSourceGetEvents: %s\n", src->ToString().Get()); src->GetEvents(Start, End, [this, src](auto events) { #ifdef _DEBUG LAssert(!GotCb.Find(src)); GotCb.Add(src, true); #endif //LgiTrace("CalendarSourceGetEvents: Callback %s %i\n", src->ToString().Get(), (int)events.Length()); Events += events; PostEvent(M_CALENDAR_SOURCE_STATE); }); } } LMessage::Result CalendarSourceGetEvents::OnEvent(LMessage *Msg) { if (Msg->Msg() == M_CALENDAR_SOURCE_STATE) OnState(); return 0; } 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,334 - PRODUCTVERSION 3,9,334 + FILEVERSION 3,9,335 + PRODUCTVERSION 3,9,335 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,334\0" + VALUE "FileVersion", "3,9,335\0" VALUE "InternalName", "Scribe" VALUE "LegalCopyright", "Copyright © 2016" VALUE "OriginalFilename", "Scribe.exe" VALUE "ProductName", "i.Scribe\0" - VALUE "ProductVersion", "3,9,334\0" + VALUE "ProductVersion", "3,9,335\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/Scribe.h b/src/Scribe.h --- a/src/Scribe.h +++ b/src/Scribe.h @@ -1,2646 +1,2647 @@ /*hdr ** FILE: Scribe.h ** AUTHOR: Matthew Allen ** DATE: 22/10/97 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998-2003 Matthew Allen ** fret@memecode.com */ // Includes #include #include #include "lgi/common/Lgi.h" #include "lgi/common/DragAndDrop.h" #include "lgi/common/DateTime.h" #include "lgi/common/Password.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/WordStore.h" #include "lgi/common/XmlTreeUi.h" #include "lgi/common/Mime.h" #include "lgi/common/OptionsFile.h" #include "lgi/common/TextLog.h" #include "lgi/common/Menu.h" #include "lgi/common/ToolBar.h" #include "lgi/common/Combo.h" #include "lgi/common/Printer.h" // Gui controls #include "lgi/common/Panel.h" #include "lgi/common/DocView.h" #include "lgi/common/List.h" #include "lgi/common/Tree.h" #include "lgi/common/ListItemCheckBox.h" // Storage #include "lgi/common/Store3.h" // App Includes #include "ScribeInc.h" #include "ScribeUtils.h" #include "ScribeDefs.h" #include "DomType.h" // Defines // All functions in a LWindow or LView should use one of these at the top. // Until such time as a function has been checked for thread safety it should // use THREAD_UNSAFE([returnValue]) at the top. Otherwise THREAD_SAFE denotes // that the function IS safe to use in any thread. #define THREAD_UNSAFE(...) if (!InThread()) \ { \ LStackTrace("%s call out of thread.\n", __func__); \ LAssert(!"not thread safe"); \ return __VA_ARGS__; \ } #define THREAD_SAFE(...) //////////////////////////////////////////////////////////////////////////////////////////// // Classes class MailTree; class LMailStore; class ScribeWnd; class Thing; class Mail; class Contact; class ThingUi; class MailUi; class ScribeFolder; class ContactUi; class FolderPropertiesDlg; class ScribeAccount; class Filter; class Attachment; class Calendar; class CalendarSource; class AttachmentList; class FolderDlg; class Filter; class ContactGroup; class ScribeBehaviour; class AccountletThread; class ThingList; class LSpellCheck; class ListAddr; // The field definition type struct ItemFieldDef { const char *DisplayText; ScribeDomType Dom; LVariantType Type; int FieldId; // Was 'Id' int CtrlId; const char *Option; bool UtcConvert; }; //////////////////////////////////////////////////////////////////////// // Scripting support #include "lgi/common/Scripting.h" /// Script callback types. See 'api.html' in the Scripts folder for more details. enum LScriptCallbackType { LCallbackNull, LToolsMenu, LThingContextMenu, LThingUiToolbar, LApplicationToolbar, LMailOnBeforeSend, // "OnBeforeMailSend" LMailOnAfterReceive, LBeforeInstallBar, LInstallComponent, LFolderContextMenu, LOnTimer, LRenderMail, LOnLoad }; struct LScript; struct LScriptCallback { static constexpr int INVALID_CALLBACK = 0; LScriptCallbackType Type = LCallbackNull; LScript *Script = NULL; LFunctionInfo *Func = NULL; int Uid = INVALID_CALLBACK; int Param = 0; double fParam = 0.0; LVariant Data; uint64 PrevTs = 0; bool OnSecond = false; }; struct LScript { LAutoPtr Code; LArray Callbacks; }; typedef void (*ConsoleClosingCallback)(class LScriptConsole *Console, void *user_data); class LScriptConsole : public LWindow { ScribeWnd *App; LTextLog *Txt; ConsoleClosingCallback Callback; void *CallbackData; bool OnViewKey(LView *v, LKey &k); public: LScriptConsole(ScribeWnd *app, ConsoleClosingCallback callback, void *callback_data); ~LScriptConsole(); void Write(const char *s, int64 Len); bool OnRequestClose(bool OsShuttingDown); }; /// This class is a wrapper around a user interface element used for /// Scripting. The script engine needs to be able to store information /// pertaining to the menu item's callbacks along with the sub menu. class LScriptUi : public LDom { public: LScriptUi *Parent; LSubMenu *Sub; LToolBar *Toolbar; LArray Callbacks; LArray Subs; LScriptUi() { Parent = 0; Sub = 0; Toolbar = 0; } LScriptUi(LSubMenu *s) { Parent = 0; Toolbar = 0; Sub = s; } LScriptUi(LToolBar *t) { Parent = 0; Toolbar = t; Sub = 0; } ~LScriptUi() { Subs.DeleteObjects(); } const char *GetClass() override { return "LScriptUi"; } bool GetVariant(const char *Name, LVariant &Value, const char *Arr = NULL) override { if (Sub) return Sub->GetVariant(Name, Value, Arr); LDomProperty Method = LStringToDomProp(Name); if (Method == ObjLength) { if (Toolbar) Value = (int64)Toolbar->Length(); } else return false; return true; } bool CallMethod(const char *MethodName, LScriptArguments &Args) override { if (Sub) return Sub->CallMethod(MethodName, Args); return false; } bool SetupCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, LScriptCallbackType Type); bool ExecuteCallbacks(ScribeWnd *App, ThingUi *Parent, Thing *t, int Cmd); }; class LScribeScript : public LScriptContext { LScriptEngine *Eng; struct LScribeScriptPriv *d; public: ScribeWnd *App; static LScribeScript *Inst; LScribeScript(ScribeWnd *app); ~LScribeScript(); void ShowScriptingWindow(bool show); LAutoString GetDataFolder() override; LStream *GetLog() override; LHostFunc *GetCommands() override; LString GetIncludeFile(const char *FileName) override; // System void SetEngine(LScriptEngine *eng); bool MsgBox(LScriptArguments &Args); // Paths bool GetSystemPath(LScriptArguments &Args); bool GetScribeTempPath(LScriptArguments &Args); bool JoinPath(LScriptArguments &Args); // Folders bool GetFolder(LScriptArguments &Args); bool GetSourceFolders(LScriptArguments &Args); bool CreateSubFolder(LScriptArguments &Args); bool LoadFolder(LScriptArguments &Args); bool FolderSelect(LScriptArguments &Args); bool BrowseFolder(LScriptArguments &Args); // Things bool CreateThing(LScriptArguments &Args); bool MoveThing(LScriptArguments &Args); bool SaveThing(LScriptArguments &Args); bool DeleteThing(LScriptArguments &Args); bool ShowThingWindow(LScriptArguments &Args); bool FilterDoActions(LScriptArguments &Args); bool LookupContact(LScriptArguments &Args); // Callbacks bool AddToolsMenuItem(LScriptArguments &Args); bool AddCallback(LScriptArguments& Args); bool RemoveCallback(LScriptArguments& Args); // UI bool MenuAddItem(LScriptArguments &Args); bool MenuAddSubmenu(LScriptArguments &Args); bool ToolbarAddItem(LScriptArguments &Args); }; //////////////////////////////////////////////////////////////////////// class ScribePassword { class ScribePasswordPrivate *d; public: ScribePassword(LOptionsFile *p, const char *opt, int check, int pwd, int confirm); ~ScribePassword(); bool IsOk(); bool Load(LView *dlg); bool Save(); void OnNotify(LViewI *c, LNotification &n); }; class ImportExportDlg : public LDialog { ScribeWnd *App = NULL; int Type = 0; bool Export = false; LList *Src = NULL; LEdit *Dst = NULL; void InsertFile(const char *f); public: LString DestFolder; LString::Array SrcFiles; bool IncSubFolders = false; ImportExportDlg ( ScribeWnd *parent, bool IsExport, const char *Title, const char *Msg, LString::Array *SrcFiles = NULL, const char *DefFolder = NULL, int FolderType = MAGIC_MAIL ); int OnNotify(LViewI *Ctrl, LNotification n); }; #define IoProgressImplArgs LAutoPtr stream, const char *mimeType, IoProgressCallback cb #define IoProgressFnArgs IoProgressImplArgs = NULL #define IoProgressError(err) \ { \ IoProgress p(Store3Error, err); \ if (cb) cb(&p, stream); \ return p; \ } #define IoProgressSuccess() \ { \ IoProgress p(Store3Success); \ if (cb) cb(&p, stream); \ return p; \ } #define IoProgressNotImpl() \ { \ IoProgress p(Store3NotImpl); \ if (cb) cb(&p, stream); \ return p; \ } class ScribeClass ThingType : public LDom, public LDataUserI { bool Dirty = false; bool WillDirty = true; bool Loaded = false; protected: bool OnError(const char *File, int Line) { _lgi_assert(false, "Object Missing", File, Line); return false; } // Callbacks struct ThingEventInfo { const char *File = NULL; int Line = 0; std::function Callback; }; LArray OnLoadCallbacks; public: #ifdef _DEBUG bool _debug = false; #endif struct IoProgress; typedef std::function IoProgressCallback; struct IoProgress { // This is the main result to look at: // Store3NotImpl - typically means the mime type is wrong. // Store3Error - an error occurred. // Store3Delayed - means the operation will take a long time. // However progress is report via 'prog' if not NULL. // And the 'onComplete' handler will be called at the end. // Store3Success - the operation successfully completed. Store3Status status = Store3NotImpl; // Optional progress for the operation. Really only relevant for // status == Store3Delayed. Progress *prog = NULL; // Optional error message for the operation. Relevant if // status == Store3Error. LString errMsg; IoProgress(Store3Status s, const char *err = NULL) { status = s; if (err) errMsg = err; } operator bool() { return status > Store3Error; } bool IsCancelled() { return prog ? prog->IsCancelled() : false; } }; template LAutoPtr AutoCast(LAutoPtr ap) { return LAutoPtr(ap.Release()); } static LArray DirtyThings; ScribeWnd *App = NULL; ThingType(); virtual ~ThingType(); virtual Store3ItemTypes Type() { return MAGIC_NONE; } bool GetDirty() { return Dirty; } virtual bool SetDirty(bool b = true); void SetWillDirty(bool c) { WillDirty = c; } virtual bool Save(ScribeFolder *Into) { return false; } virtual void OnProperties(int Tab = -1) {} virtual ScribeFolder *GetFolder() = 0; virtual void SetFolder(ScribeFolder *f, std::function callback) = 0; virtual bool IsPlaceHolder() { return false; } // Events void WhenLoaded(const char *file, int line, std::function Callback, int index = -1, bool waitForResults = true); bool IsLoaded(int Set = -1); // Printing virtual void OnPrintHeaders(struct ScribePrintContext &Context) { LAssert(!"Impl me."); } virtual void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { LAssert(!"Impl me."); } virtual int OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) { LAssert(!"Impl me."); return 0; } }; class MailContainerIter; class ScribeClass MailContainer { friend class MailContainerIter; List Iters; public: virtual ~MailContainer(); virtual size_t Length() { return 0; } virtual ssize_t IndexOf(Mail *m) { return -1; } virtual Mail *operator [](size_t i) { return NULL; } }; class ScribeClass MailContainerIter { friend class MailContainer; protected: MailContainer *Container; public: MailContainerIter(); ~MailContainerIter(); void SetContainer(MailContainer *c); }; class ScribeClass ThingStorage // External storage information { public: int Data; ThingStorage() { Data = 0; } virtual ~ThingStorage() {} }; class ThingUi; class ScribeClass Thing : public ThingType, public LListItem, public LDragDropSource, public LRefCount { friend class ScribeWnd; friend class ScribeFolder; ScribeFolder *_ParentFolder = NULL; protected: LArray FieldArray; LAutoString DropFileName; // This structure allows the app to move objects between // mail stores. After a delayed write to the new mail store // the old item needs to be removed. This keeps track of // where that old item is. Don't assume that the Obj pointer // is valid... check in the folder's items first. struct ThingReference { LString Path; Thing *Obj = NULL; std::function Callback; } DeleteOnAdd; public: + LString ErrMsg; ThingStorage *Data = NULL; Thing(ScribeWnd *app, LDataI *object = NULL); ~Thing(); // Dom bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // D'n'd bool GetData(LArray &Data) override; bool GetFormats(LDragFormats &Formats) override; bool OnBeginDrag(LMouse &m) override; // Import / Export virtual bool GetFormats(bool Export, LString::Array &MimeTypes) { return false; } // Anything implementing 2 functions should mostly be using one of these // to "return" an IoProgress and process any callback: // IoProgressError(msg) // IoProgressNotImpl() // IoProgressSuccess() virtual IoProgress Import(IoProgressFnArgs) = 0; virtual IoProgress Export(IoProgressFnArgs) = 0; /// This exports all the selected items void ExportAll(LViewI *Parent, const char *ExportMimeType, std::function Callback); void ExportAllProcess(LFileSelect *Select, LViewI *Parent, LArray Sel, LString ExportMimeType, std::function Callback); // UI bool OnKey(LKey &k) override; // Thing ScribeFolder *GetFolder() override { return _ParentFolder; } void SetParentFolder(ScribeFolder *f); void SetFolder(ScribeFolder *f, std::function callback) override; LDataI *DefaultObject(LDataI *arg = NULL); virtual ThingUi *DoUI(MailContainer *c = NULL) { return NULL; } virtual ThingUi *GetUI() { return NULL; } virtual bool SetUI(ThingUi *ui = NULL) { return false; } virtual uint32_t GetFlags() { return 0; } virtual void OnCreate() override; virtual bool OnDelete(); virtual int *GetDefaultFields() { return NULL; } virtual const char *GetFieldText(int Field) { return 0; } virtual void DoContextMenu(LMouse &m, LView *Parent = NULL) {} virtual Thing &operator =(Thing &c) { LAssert(0); return *this; } virtual char *GetDropFileName() = 0; virtual bool GetDropFiles(LString::Array &Files) { return false; } virtual void OnSerialize(bool Write) {} virtual void Reparse(); // Interfaces virtual Mail *IsMail() { return 0; } virtual Contact *IsContact() { return 0; } virtual ContactGroup *IsGroup() { return 0; } virtual Filter *IsFilter() { return 0; } virtual Attachment *IsAttachment() { return 0; } virtual Calendar *IsCalendar() { return 0; } void SetFieldArray(LArray &i) { FieldArray = i; } void OnMove(); bool SetField(int Field, int n); bool SetField(int Field, double n); bool SetField(int Field, char *n); bool SetField(int Field, LDateTime &n); bool SetDateField(int Field, LVariant &v); bool GetField(int Field, int &n); bool GetField(int Field, double &n); bool GetField(int Field, const char *&n); bool GetField(int Field, LDateTime &n); bool GetDateField(int Field, LVariant &v); bool DeleteField(int Field); }; class ThingUi : public LWindow { friend class MailUiGpg; bool _Dirty = false; LString _Name; protected: Thing *_Item = NULL; bool _Running = false; void SetItem(Thing *i); public: ScribeWnd *App = NULL; static LArray All; ThingUi(Thing *item, const char *name); ~ThingUi(); virtual bool SetDirty(bool d, bool ui = true); bool IsDirty() { return _Dirty; } bool OnRequestClose(bool OsShuttingDown); bool OnViewKey(LView *v, LKey &k); virtual void OnDirty(bool Dirty) {} virtual void OnLoad() = 0; virtual void OnSave() = 0; virtual void OnChange() {} virtual AttachmentList *GetAttachments() { return 0; } virtual bool AddRecipient(AddressDescriptor *Addr) { return false; } }; class ThingFilter { public: virtual bool TestThing(Thing *Thing) = 0; }; class ScribeClass Attachment : public Thing { friend class Mail; protected: Mail *Msg; Mail *Owner; bool IsResizing; LString Buf; // D'n'd LAutoString DropSourceFile; bool GetFormats(LDragFormats &Formats) override; bool GetData(LArray &Data) override; void _New(LDataI *object); void DoSave(LFileSelect *Select, const LArray Files); public: enum Encoding { OCTET_STREAM, PLAIN_TEXT, BASE64, QUOTED_PRINTABLE, }; Attachment(ScribeWnd *App, Attachment *import = NULL); Attachment(ScribeWnd *App, LDataI *object, const char *import = NULL); ~Attachment(); const char *GetClass() override { return "Attachment"; } bool ImportFile(const char *FileName); bool ImportStream(const char *FileName, const char *MimeType, LAutoStreamI Stream); Thing &operator =(Thing &c) override; LDATA_INT64_PROP(Size, FIELD_SIZE); LDATA_STR_PROP(Name, FIELD_NAME); LDATA_STR_PROP(MimeType, FIELD_MIME_TYPE); LDATA_STR_PROP(ContentId, FIELD_CONTENT_ID); LDATA_STR_PROP(Charset, FIELD_CHARSET); LDATA_STR_PROP(InternetHeaders, FIELD_INTERNET_HEADER); // LDom support bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; void OnOpen(LView *Parent, char *Dest = NULL); void OnDeleteAttachment(LView *Parent, bool Ask); void OnSaveAs(LView *Parent); void OnMouseClick(LMouse &m) override; bool OnKey(LKey &k) override; Store3ItemTypes Type() override { return MAGIC_ATTACHMENT; } bool Get(char **ptr, ssize_t *size); bool Set(char *ptr, ssize_t size); bool Set(LAutoStreamI Stream); Attachment *IsAttachment() override { return this; } LString MakeFileName(); bool GetIsResizing(); void SetIsResizing(bool b); bool IsMailMessage(); bool IsVCalendar(); bool IsVCard(); // The owner is the mail that this is attached to Mail *GetOwner() { return Owner; } void SetOwner(Mail *msg); // The msg is the mail that this message/rfc822 attachment is rendered into Mail *GetMsg(); void SetMsg(Mail *m); LStreamI *GotoObject(const char *file, int line); IoProgress Import(IoProgressFnArgs) override { return Store3Error; } IoProgress Export(IoProgressFnArgs) override { return Store3Error; } bool SaveTo(char *FileName, bool Quite = false, LView *Parent = NULL); const char *GetText(int i) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; }; class ScribeClass Contact : public Thing { friend class ContactUi; protected: class ContactPriv *d = NULL; ContactUi *Ui = NULL; public: static List Everyone; static Contact *LookupEmail(const char *Email); static LHashTbl, int> PropMap; static int DefaultContactFields[]; Contact(ScribeWnd *app, LDataI *object = NULL); ~Contact(); const char *GetClass() override { return "Contact"; } LDATA_STR_PROP(First, FIELD_FIRST_NAME); LDATA_STR_PROP(Last, FIELD_LAST_NAME); LDATA_STR_PROP(Email, FIELD_EMAIL); bool Get(const char *Opt, const char *&Value); bool Set(const char *Opt, const char *Value); bool Get(const char *Opt, int &Value); bool Set(const char *Opt, int Value); // operators Thing &operator =(Thing &c) override; Contact *IsContact() override { return this; } // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // Events void OnMouseClick(LMouse &m) override; // Printing void OnPrintHeaders(struct ScribePrintContext &Context) override; void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) override; // Misc Store3ItemTypes Type() override { return MAGIC_CONTACT; } ThingUi *DoUI(MailContainer *c = NULL) override; int Compare(LListItem *Arg, ssize_t Field) override; bool IsAssociatedWith(char *PluginName); LString GetLocalTime(const char *TimeZone = NULL); // Email address int GetAddrCount(); LString::Array GetEmails(); LString GetAddrAt(int i); bool HasEmail(LString email); // Serialization bool Save(ScribeFolder *Into = NULL) override; // ListItem const char *GetText(int i) override; int *GetDefaultFields() override; const char *GetFieldText(int Field) override; int GetImage(int Flags = 0) override { return ICON_CONTACT; } // Import/Export bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; }; #define ContactGroupObj "ContactGroup" #define ContactGroupName "Name" #define ContactGroupList "List" #define ContactGroupDateModified "DateModified" extern ItemFieldDef GroupFieldDefs[]; class ContactGroup : public Thing { friend class GroupUi; class GroupUi *Ui; class ContactGroupPrivate *d; LString DateCache; public: LDateTime UsedTs; LDATA_STR_PROP(Name, FIELD_GROUP_NAME); ContactGroup(ScribeWnd *app, LDataI *object = NULL); ~ContactGroup(); const char *GetClass() override { return "ContactGroup"; } // operators Thing &operator =(Thing &c) override; ContactGroup *IsGroup() override { return this; } // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // Events void OnMouseClick(LMouse &m) override; void OnSerialize(bool Write) override; // Misc Store3ItemTypes Type() override { return MAGIC_GROUP; } ThingUi *DoUI(MailContainer *c = NULL) override; int Compare(LListItem *Arg, ssize_t Field) override; bool GetAddresses(List &a); LString::Array GetAddresses(); // Serialization bool Save(ScribeFolder *Into = NULL) override; // ListItem const char *GetText(int i) override; int *GetDefaultFields() override; const char *GetFieldText(int Field) override; int GetImage(int Flags = 0) override { return ICON_CONTACT_GROUP; } // Import / Export char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; }; struct LGroupMapArray : public LArray { LString toString() { LString::Array a; for (auto i: *this) a.Add(i->GetName()); return LString(",").Join(a); } }; class LGroupMap : public LHashTbl,LGroupMapArray*> { ScribeWnd *App; void Index(ContactGroup *grp); public: LGroupMap(ScribeWnd *app); ~LGroupMap(); }; ///////////////////////////////////////////////////////////// // Mail threading // // See: http://www.jwz.org/doc/threading.html struct ThingSortParams { int SortAscend; int SortField; }; class MContainer { int Lines; int Depth; bool Open; bool Next; void Dump(LStream &s, int Depth = 0); public: typedef LHashTbl, MContainer*> ContainerHash; // Container data Mail *Message; MContainer *Parent; LArray Children; List Refs; int Index; // Cache data LString::Array RefCache; // Debug #ifdef _DEBUG LAutoString MsgId; #endif // Methods MContainer(const char *Id, Mail *m = NULL); ~MContainer(); void SetMail(Mail *m); Mail *GetTop(); bool HasChild(MContainer *m); void AddChild(MContainer *m); void RemoveChild(MContainer *m); int CountMessages(); void Pour(int &index, int depth, int tree, bool next, ThingSortParams *folder); void OnPaint(LSurface *pDC, LRect &r, LItemColumn *c, LColour Fore, LColour Back, LFont *Font, const char *Txt); static void Prune(int &ParentIndex, LArray &L); static void Thread(List &In, LArray &Out); }; extern int ListItemCompare(LListItem *a, LListItem *b, NativeInt Data); extern int ContainerIndexer(Thing *a, Thing *b, NativeInt Data); extern int GetFolderVersion(const char *Path); extern bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol); extern void Base36(char *Out, uint64 In); //////////////////////////////////////////////////////////// // Thing sorting // The old way extern int ContainerCompare(MContainer **a, MContainer **b); extern int ThingCompare(Thing *a, Thing *b, NativeInt Data); // The new way extern int ContainerSorter(MContainer *&a, MContainer *&b, ThingSortParams *Params); extern int ThingSorter(Thing *a, Thing *b, ThingSortParams *Data); ///////////////////////////////////////////////////////////// class MailViewOwner : public LCapabilityTarget { public: virtual LDocView *GetDoc(const char *MimeType) = 0; virtual bool SetDoc(LDocView *v, const char *MimeType) = 0; }; ///////////////////////////////////////////////////////////// // The core mail object struct MailPrintContext; class ScribeClass Mail : public Thing, public MailContainer, public LDefaultDocumentEnv { friend class MailUi; friend class MailPropDlg; friend class ScribeFolder; friend class Attachment; friend class ScribeWnd; private: class MailPrivate *d; static LHashTbl,Mail*> MessageIdMap; // List item preview int PreviewCacheX; List PreviewCache; int64_t TotalSizeCache = 0; int64_t FlagsCache = 0; MailUi *Ui = NULL; int Cursor; // Stores the cursor position in reply/forward format until the UI needs it Attachment *ParentFile = NULL; List Attachments; Mail *PreviousMail = NULL; // the mail we are replying to / forwarding void _New(); void _Delete(); bool _GetListItems(List &l, bool All); // All=false is just the selected items void SetListRead(bool Read); void SetFlagsCache(int64_t NewFlags, bool IgnoreReceipt, bool UpdateScreen); // LDocumentEnv impl List Actions; bool OnNavigate(LDocView *Parent, const char *Uri) override; bool AppendItems(LSubMenu *Menu, const char *Param, int Base = 1000) override; bool OnMenu(LDocView *View, int Id, void *Context) override; LoadType GetContent(LAutoPtr &j) override; public: static bool PreviewLines; static bool AdjustDateTz; static int DefaultMailFields[]; static bool RunMailPipes; static List NewMailLst; constexpr static float MarkColourMix = 0.9f; uint8_t SendAttempts = 0; enum NewEmailState { NewEmailNone, NewEmailLoading, NewEmailFilter, NewEmailBayes, NewEmailGrowl, NewEmailTray }; NewEmailState NewEmail; Mail(ScribeWnd *app, LDataI *object = NULL); ~Mail(); const char *GetClass() override { return "Mail"; } bool SetObject(LDataI *o, bool InDataDestuctor, const char *File, int Line) override; LDATA_STR_PROP(Label, FIELD_LABEL); LDATA_STR_PROP(FwdMsgId, FIELD_FWD_MSG_ID); LDATA_STR_PROP(BounceMsgId, FIELD_BOUNCE_MSG_ID); LDATA_STR_PROP(Subject, FIELD_SUBJECT); LDATA_STR_PROP(Body, FIELD_TEXT); LDATA_STR_PROP(BodyCharset, FIELD_CHARSET); LDATA_STR_PROP(Html, FIELD_ALTERNATE_HTML); LDATA_STR_PROP(HtmlCharset, FIELD_HTML_CHARSET); LDATA_STR_PROP(InternetHeader, FIELD_INTERNET_HEADER); LDATA_INT_TYPE_PROP(EmailPriority, Priority, FIELD_PRIORITY, MAIL_PRIORITY_NORMAL); LDATA_INT32_PROP(AccountId, FIELD_ACCOUNT_ID); LDATA_INT64_PROP(MarkColour, FIELD_COLOUR); LDATA_STR_PROP(References, FIELD_REFERENCES); LDATA_DATE_PROP(DateReceived, FIELD_DATE_RECEIVED); LDATA_DATE_PROP(DateSent, FIELD_DATE_SENT); LDATA_INT_TYPE_PROP(Store3State, Loaded, FIELD_LOADED, Store3Loaded); LVariant GetServerUid(); bool SetServerUid(LVariant &v); const char *GetFromStr(int id) { LDataPropI *From = GetObject() ? GetObject()->GetObj(FIELD_FROM) : 0; return From ? From->GetStr(id) : NULL; } LDataPropI *GetFrom() { return GetObject() ? GetObject()->GetObj(FIELD_FROM) : 0; } LDataPropI *GetReply() { return GetObject() ? GetObject()->GetObj(FIELD_REPLY) : 0; } LDataIt GetTo() { return GetObject() ? GetObject()->GetList(FIELD_TO) : 0; } bool GetAttachmentObjs(LArray &Objs); LDataI *GetFileAttachPoint(); // Operators Mail *IsMail() override { return this; } Thing &operator =(Thing &c) override; OsView Handle(); ThingUi *GetUI() override; bool SetUI(ThingUi *ui) override; // References and ID's MContainer *Container; const char *GetMessageId(bool Create = false); bool SetMessageId(const char *val); LAutoString GetThreadIndex(int TruncateChars = 0); static Mail *GetMailFromId(const char *Id); bool MailMessageIdMap(bool Add = true); bool GetReferences(LString::Array &Ids); void GetThread(List &Thread); LString GetMailRef(); bool ResizeImage(Attachment *a); // MailContainer size_t Length() override; ssize_t IndexOf(Mail *m) override; Mail *operator [](size_t i) override; // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // Events void OnCreate() override; bool OnBeforeSend(struct ScribeEnvelope *Out); void OnAfterSend(); bool OnBeforeReceive(); bool OnAfterReceive(LStreamI *Msg); void OnMouseClick(LMouse &m) override; void OnProperties(int Tab = -1) override; void OnInspect(); void OnReply(Mail *m, bool All, bool MarkOriginal); bool OnForward(Mail *m, bool MarkOriginal, int WithAttachments = -1); bool OnBounce(Mail *m, bool MarkOriginal, int WithAttachments = -1); void OnReceipt(Mail *m); int OnNotify(LViewI *Ctrl, LNotification n) override; // Printing void OnPrintHeaders(ScribePrintContext &Context) override; void OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) override; int OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) override; // Misc uint32_t GetFlags() override; void SetFlags(ulong i, bool IgnoreReceipt = false, bool Update = true); void DeleteAsSpam(LView *View); const char *GetFieldText(int Field) override; LAutoString GetCharSet(); char *GetNewText(int Max = 64 << 10, const char *AsCp = "utf-8"); int *GetDefaultFields() override; Store3ItemTypes Type() override { return MAGIC_MAIL; } void DoContextMenu(LMouse &m, LView *Parent = NULL) override; int Compare(LListItem *Arg, ssize_t Field) override; char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; LAutoString GetSig(bool HtmlVersion, ScribeAccount *Account = NULL); bool LoadFromFile(char *File); void PrepSend(); void NewRecipient(char *Email, char *Name = NULL); void ClearCachedItems(); bool Send(bool Now); void CreateMailHeaders(); bool AddCalendarEvent(LViewI *Parent, bool AddPopupReminder, LString *Msg); LArray GetCalendarAttachments(); void Reparse() override; // UI LDocView *CreateView(MailViewOwner *Owner, LString MimeType, bool Sunken, size_t MaxBytes, bool NoEdit = false); ThingUi *DoUI(MailContainer *c = NULL) override; // Alt HTML bool HasAlternateHtml(Attachment **Attach = NULL); char *GetAlternateHtml(List *Refs = NULL); // dynamically allocated ptr bool WriteAlternateHtml(char *File = NULL, int FileLen = 0); // defaults to TEMP dir // Account stuff void ProcessTextForResponse(Mail *From, LOptionsFile *Options, ScribeAccount *Account); void WrapAndQuote(LStringPipe &Pipe, const char *QuoteStr, int WrapAt = -1); ScribeAccount *GetAccountSentTo(); // Access int64 TotalSizeof(); bool Save(ScribeFolder *Into = NULL) override; // Attachments Attachment *AttachFile(LView *Parent, const char *FileName); bool AttachFile(Attachment *File); bool DeleteAttachment(Attachment *File); LArray GetAttachments(); bool GetAttachments(List *Attachments); bool HasAttachments() { return Attachments.Length() > 0; } bool UnloadAttachments(); // Import / Export bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; // ListItem void Update() override; const char *GetText(int i) override; int GetImage(int SelFlags = 0) override; void OnMeasure(LPoint *Info) override; void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) override; void OnPaint(LItem::ItemPaintCtx &Ctx) override; }; inline const char *toString(Mail::NewEmailState s) { #define _(s) case Mail::s: return #s; switch (s) { _(NewEmailNone) _(NewEmailLoading) _(NewEmailFilter) _(NewEmailBayes) _(NewEmailGrowl) _(NewEmailTray) } #undef _ LAssert(0); return "#invalidNewEmailState"; } // this is where the items reside // and it forms a leaf on the mail box tree // to the user it looks like a folder class ScribeClass ScribeFolder : public ThingType, public LTreeItem, public LDragDropSource, public MailContainer { friend class MailTree; friend class FolderPropertiesDlg; friend class ThingList; friend class ScribeWnd; friend class AsyncOperationState; friend struct MboxExportTask; protected: class ScribeFolderPriv *d; ThingList *View(); LString DropFileName; LString GetDropFileName(); // UI cache LAutoString NameCache; int ChildUnRead = 0; LTreeItem *LoadOnDemand = NULL; LAutoPtr Loading; LArray FieldArray; void SerializeFieldWidths(bool Write = false); void EmptyFieldList(); void SetLoadFolder(Thing *t) { if (t) t->SetParentFolder(this); } bool HasFieldId(int Id); void ContinueLoading(int OldUnread, std::function Callback, bool waitForResults); // Tree item stuff void _PourText(LPoint &Size) override; void _PaintText(LItem::ItemPaintCtx &Ctx) override; int _UnreadChildren(); void UpdateOsUnread(); // Debugging state enum FolderState { FldState_Idle, FldState_Loading, FldState_Populating, } CurState = FldState_Idle; public: List Items; ScribeFolder(); ~ScribeFolder(); // Object const char *GetClass() override { return "ScribeFolder"; } LArray &GetFieldArray() { return FieldArray; } LDataFolderI *GetFldObj() { return dynamic_cast(GetObject()); } bool SetObject(LDataI *o, bool InDataDestuctor, const char *File, int Line) override; // ThingType Store3ItemTypes Type() override { return GetObject() ? (Store3ItemTypes)GetObject()->Type() : MAGIC_NONE; } ScribeFolder *GetFolder() override { return dynamic_cast(LTreeItem::GetParent()); } ScribeFolder *GetChildFolder() { return dynamic_cast(LTreeItem::GetChild()); } ScribeFolder *GetNextFolder() { return dynamic_cast(LTreeItem::GetNext()); } void SetFolder(ScribeFolder *f, std::function callback) override; ScribeFolder *IsFolder() { return this; } Store3Status CopyTo(ScribeFolder *NewParent, int NewIndex = -1); // MailContainer size_t Length() override; ssize_t IndexOf(Mail *m) override; Mail *operator [](size_t i) override; /// Update the unread count void OnUpdateUnRead ( /// Increments the count, or zero if a child folder is changing. int Offset, /// Re-scan the folder bool ScanItems ); // Methods LDATA_INT32_PROP(UnRead, FIELD_UNREAD); LDATA_INT_TYPE_PROP(Store3ItemTypes, ItemType, FIELD_FOLDER_TYPE, MAGIC_MAIL); LDATA_INT32_PROP(Open, FIELD_FOLDER_OPEN); LDATA_INT32_PROP(SortIndex, FIELD_FOLDER_INDEX); LDATA_INT64_PROP(Items, FIELD_FOLDER_ITEMS); // Cached item count LDATA_INT_TYPE_PROP(ScribePerm, ReadAccess, FIELD_FOLDER_PERM_READ, PermRequireNone); LDATA_INT_TYPE_PROP(ScribePerm, WriteAccess, FIELD_FOLDER_PERM_WRITE, PermRequireNone); LDATA_ENUM_PROP(SystemFolderType, FIELD_SYSTEM_FOLDER, Store3SystemFolder); void SetSort(int Col, bool Ascend, bool CanDirty = true); int GetSortAscend() { return GetObject()->GetInt(FIELD_SORT) > 0; } int GetSortCol() { return abs((int)GetObject()->GetInt(FIELD_SORT)) - 1; } int GetSortField(); void ReSort(); bool Save(ScribeFolder *Into = NULL) override; bool ReindexField(int OldIndex, int NewIndex); void CollectSubFolderMail(ScribeFolder *To = NULL); bool InsertThing(Thing *Item); void MoveTo(LArray &Items, bool CopyOnly, std::function&)> Callback = NULL); bool Delete(LArray &Items, bool ToTrash); void SetDefaultFields(bool Force = false); bool Thread(); ScribePerm GetFolderPerms(ScribeAccessType Access); void SetFolderPerms(LView *Parent, ScribeAccessType Access, ScribePerm Perm, std::function Callback); bool GetThreaded(); void SetThreaded(bool t); // void Update(); void GetMessageById(const char *Id, std::function Callback); void SetLoadOnDemand(); void SortSubfolders(); void DoContextMenu(LMouse &m); void OnItemType(); bool IsInTrash(); bool SortItems(); /// /// These methods can be used in a synchronous or asynchronous manner: /// sync: Call with 'Callback=NULL' and use the return value. /// If the function needs to show a dialog (like to get permissions from /// the user) then it'll return Store3Delayed immediately. /// async: Call with a valid callback, and the method will possibly wait /// for the user and then either return Store3Error or Store3Success. /// /// If waitForResults is false, LoadThings won't wait for things to load over the network, but /// return Store3Delayed in the status immediately. Currently this is used by CalendarSourceGetEvents /// to return a combined list of events available immediately. Some remote calendar or Webdav sources /// can take a while to get their content. Store3Status LoadThings(LViewI *Parent = NULL, std::function Callback = NULL, bool waitForResults = true); Store3Status WriteThing(Thing *t, std::function Callback = NULL); Store3Status DeleteThing(Thing *t, std::function Callback = NULL); Store3Status DeleteAllThings( std::function Callback = NULL); bool LoadFolders(); bool UnloadThings(); bool IsWriteable() { return true; } bool IsPublicFolders() { return false; } void OnProperties(int Tab = -1) override; ScribeFolder *GetSubFolder(const char *Path); ScribeFolder *CreateSubFolder(const char *Name, int Type); void OnRename(char *NewName); void OnDelete(); LString GetPath(); void Populate(ThingList *List); bool CanHaveSubFolders(Store3ItemTypes Type = MAGIC_MAIL) { return GetItemType() != MAGIC_ANY; } void OnRethread(); // Name void SetName(const char *Name, bool Encode); LString GetName(bool Decode); // Tree Item const char *GetText(int i=0) override; int GetImage(int Flags = 0) override; void OnExpand(bool b) override; bool OnKey(LKey &k) override; void Update() override; // Drag'n'drop bool GetFormats(LDragFormats &Formats) override; bool OnBeginDrag(LMouse &m) override; void OnEndData() override; bool GetData(LArray &Data) override; void OnReceiveFiles(LArray &Files); // Import/Export bool GetFormats(bool Export, LString::Array &MimeTypes); IoProgress Import(IoProgressFnArgs); IoProgress Export(IoProgressFnArgs); void ExportAsync(LAutoPtr f, const char *MimeType, std::function Callback = NULL); const char *GetStorageMimeType(); // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; }; ////////////////////////////////////////////////////////////// class Filter; class FilterCondition { protected: bool TestData(Filter *F, LVariant &v, LStream *Log); public: // Data LString Source; // Data Source (used to be "int Field") LString Value; // Constant char Op = 0; uint8_t Not = false; // Methods FilterCondition(); bool Set(class LXmlTag *t); // Test condition against email bool Test(Filter *F, Mail *m, LStream *Log); FilterCondition &operator =(const FilterCondition &c); // Object ThingUi *DoUI(MailContainer *c = NULL); }; class FilterAction : public LListItem, public LDataPropI { LAutoPtr TypeCbo; LAutoPtr ArgEdit; LAutoPtr Btn; Filter *owner = NULL; LArray iconPos; LImageList *GetIcons(); public: // Data FilterActionTypes Type = ACTION_MOVE_TO_FOLDER; LString Arg1; // Methods FilterAction(Filter *Owner, LDataStoreI *Store); ~FilterAction(); const char *GetClass() override { return "FilterAction"; } bool Set(LXmlTag *t); bool Get(LXmlTag *t); bool Do(Filter *F, ScribeWnd *App, Mail *&m, LStream *log); void Browse(ScribeWnd *App, LView *Parent); void DescribeHtml(Filter *Flt, LStream &s); LDataPropI &operator =(LDataPropI &p); // List item const char *GetText(int Col = 0) override; void OnMeasure(LPoint *Info) override; bool Select() override; void Select(bool b) override; void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) override; int OnNotify(LViewI *c, LNotification n) override; void OnMouseClick(LMouse &m) override; void OnIconClick(int icon); // Object ThingUi *DoUI(MailContainer *c = NULL); }; class ScribeClass Filter : public Thing { friend class FilterUi; protected: class FilterUi *Ui; class FilterPrivate *d; static int MaxIndex; // Ui LListItemCheckBox *ChkIncoming; LListItemCheckBox *ChkOutgoing; LListItemCheckBox *ChkInternal; bool IgnoreCheckEvents; // Current Mail **Current; // XML I/O LAutoPtr ConditionsCache; LAutoPtr Parse(bool Actions); // Methods bool EvaluateTree(LXmlTag *n, Mail *m, bool &Stop, LStream *Log); bool EvaluateXml(Mail *m, bool &Stop, LStream *Log); public: Filter(ScribeWnd *app, LDataI *object = NULL); ~Filter(); const char *GetClass() override { return "Filter"; } LDATA_STR_PROP(Name, FIELD_FILTER_NAME); LDATA_STR_PROP(ConditionsXml, FIELD_FILTER_CONDITIONS_XML); LDATA_STR_PROP(ActionsXml, FIELD_FILTER_ACTIONS_XML); LDATA_STR_PROP(Script, FIELD_FILTER_SCRIPT); LDATA_INT32_PROP(Index, FIELD_FILTER_INDEX); LDATA_INT32_PROP(StopFiltering, FIELD_STOP_FILTERING); LDATA_INT32_PROP(Incoming, FIELD_FILTER_INCOMING); LDATA_INT32_PROP(Outgoing, FIELD_FILTER_OUTGOING); LDATA_INT32_PROP(Internal, FIELD_FILTER_INTERNAL); int Compare(LListItem *Arg, ssize_t Field) override; Thing &operator =(Thing &c) override; Filter *IsFilter() override { return this; } static void Reindex(ScribeFolder *Folder); int *GetDefaultFields() override; const char *GetFieldText(int Field) override; LImageList *GetIcons(); // Methods void Empty(); LAutoString DescribeHtml(); // Import / Export char *GetDropFileName() override; bool GetDropFiles(LString::Array &Files) override; bool GetFormats(bool Export, LString::Array &MimeTypes) override; IoProgress Import(IoProgressFnArgs) override; IoProgress Export(IoProgressFnArgs) override; // Dom bool Evaluate(char *s, LVariant &v); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // Filter bool Test(Mail *m, bool &Stop, LStream *Log = NULL); bool DoActions(Mail *&m, bool &Stop, LStream *Log = NULL); Mail *GetCurrent() { return Current?*Current:0; } /// This filters all the mail in 'Email'. Anything that is handled by a filter /// is removed from the list, leaving just the unfiltered mail. static int ApplyFilters ( /// [In] The window of the filtering caller LView *Parent, /// [In] List of all the filter to test and/or apply List &Filters, /// [In/Out] The email to filter. After the call anything that has been /// acted on by a filter will be removed from the list. List &Email ); // Object Store3ItemTypes Type() override { return MAGIC_FILTER; } ThingUi *DoUI(MailContainer *c = NULL) override; bool Serialize(LFile &f, bool Write); bool Save(ScribeFolder *Into = NULL) override; void OnMouseClick(LMouse &m) override; void AddAction(FilterAction *a); // List item const char *GetText(int i) override; int GetImage(int Flags) override; void OnPaint(ItemPaintCtx &Ctx) override; void OnColumnNotify(int Col, int64 Data) override; // Index Filter *GetFilterAt(size_t Index); }; ////////////////////////////////////////////////////////////////////// class Accountlet; enum AccountThreadState { ThreadIdle, ThreadSetup, ThreadConnecting, ThreadTransfer, ThreadWaiting, ThreadDeleting, ThreadDone, ThreadCancel, ThreadError }; enum ReceiveAction { MailNoop, MailDelete, MailDownloadAndDelete, MailDownload, MailUpload, MailHeaders, }; enum ReceiveStatus { MailReceivedNone, MailReceivedWaiting, // This has been given to the main thread MailReceivedOk, // and one of "Ok" or "Error" has to be MailReceivedError, // set to continue. MailReceivedMax, }; ScribeFunc const char *AccountThreadStateName(AccountThreadState i); ScribeFunc const char *ReceiveActionName(ReceiveAction i); ScribeFunc const char *ReceiveStatusName(ReceiveStatus i); class LScribeMime : public LMime { public: LScribeMime() : LMime(ScribeTempPath()) { } }; class LMimeStream : public LTempStream, public LScribeMime { public: LMimeStream(); bool Parse(); }; class MailTransferEvent { public: // Message LAutoPtr Rfc822Msg; ReceiveAction Action = MailNoop; ReceiveStatus Status = MailReceivedNone; int Index = 0; bool Explicit = false; LString Uid; int64 Size = 0; int64 StartWait = 0; // Header Listing class AccountMessage *Msg = NULL; LList *GetList(); // Sending ScribeEnvelope *Send = NULL; LString OutgoingHeaders; // Other class Accountlet *Account = NULL; MailTransferEvent() { } ~MailTransferEvent() { #ifndef LGI_STATIC // LStackTrace("%p::~MailTransferEvent\n", this); #endif } }; #define RoProp(Type, Name) \ protected: \ Type Name; \ public: \ Type Get##Name() { return Name; } class AccountThread : public LThread, public LCancel { protected: Accountlet *Acc; void OnAfterMain(); public: // Object AccountThread(Accountlet *acc); ~AccountThread(); // Api Accountlet *GetAccountlet() { return Acc; } // 1st phase virtual void Disconnect(); // 2nd phase virtual bool Kill(); }; #define AccStrOption(func, opt) \ LVariant func(const char *Set = NULL) { LVariant v; StrOption(opt, v, Set); return v; } #define AccIntOption(name, opt) \ int name(int Set = -1) { LVariant v; IntOption(opt, v, Set); return v.CastInt32(); } class ScribeClass Accountlet : public LStream { friend class ScribeAccount; friend class AccountletThread; friend class AccountThread; public: struct AccountletPriv { LArray Log; }; class AccountletLock { LMutex *l; public: bool Locked; AccountletPriv *d; AccountletLock(AccountletPriv *data, LMutex *lck, const char *file, int line) { d = data; l = lck; Locked = lck->Lock(file, line); } ~AccountletLock() { if (Locked) l->Unlock(); } }; typedef LAutoPtr I; private: AccountletPriv d; LMutex PrivLock; protected: // Data ScribeAccount *Account = NULL; LAutoPtr Thread; bool ConnectionStatus = true; MailProtocol *Client = NULL; uint64 LastOnline = 0; LString TempPsw; bool Quiet = false; LView *Parent = NULL; // Pointers ScribeFolder *Root = NULL; LDataStoreI *DataStore = NULL; LMailStore *MailStore = NULL; // this memory is owned by ScribeWnd // Options const char *OptPassword = NULL; // Members LSocketI *CreateSocket(bool Sending, LCapabilityClient *Caps, bool RawLFCheck); bool WaitForTransfers(List &Files); void StrOption(const char *Opt, LVariant &v, const char *Set); void IntOption(const char *Opt, LVariant &v, int Set); // LStringPipe Line; ssize_t Write(const void *buf, ssize_t size, int flags); public: MailProtocolProgress Group; MailProtocolProgress Item; Accountlet(ScribeAccount *a); ~Accountlet(); Accountlet &operator =(const Accountlet &a) { LAssert(0); return *this; } I Lock(const char *File, int Line) { I a(new AccountletLock(&d, &PrivLock, File, Line)); if (!a->Locked) a.Reset(); return a; } // Methods bool Connect(LView *Parent, bool Quiet); bool Lock(); void Unlock(); virtual bool IsConfigured() { return false; } bool GetStatus() { return ConnectionStatus; } uint64 GetLastOnline() { return LastOnline; } ScribeAccount* GetAccount() { return Account; } bool IsCancelled(); void IsCancelled(bool b); ScribeWnd* GetApp(); const char* GetStateName(); ScribeFolder* GetRootFolder() { return Root; } RoProp(AccountThreadState, State); bool IsOnline() { if (DataStore) return DataStore->GetInt(FIELD_IS_ONLINE) != 0; return Thread != 0; } void OnEndSession() { if (DataStore) DataStore->SetInt(FIELD_IS_ONLINE, false); } // Commands void Disconnect(); void Kill(); // Data char *OptionName(const char *Opt, char *Dest, int DestLen); void Delete(); LThread *GetThread() { return Thread; } LMailStore *GetMailStore() { return MailStore; } LDataStoreI *GetDataStore() { return DataStore; } // General options virtual int UseSSL(int Set = -1) = 0; AccStrOption(Name, OPT_AccountName); AccIntOption(Disabled, OPT_AccountDisabled); AccIntOption(Id, OPT_AccountUID); AccIntOption(Expanded, OPT_AccountExpanded); bool GetPassword(LPassword *p); void SetPassword(LPassword *p); bool IsCheckDialup(); // Events void OnThreadDone(); void OnOnlineChange(bool Online); virtual void OnBeforeDelete(); // LDom impl bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); // Virtuals virtual void Main(AccountletThread *Thread) = 0; virtual LVariant Server(const char *Set = NULL) = 0; virtual LVariant UserName(const char *Set = NULL) = 0; virtual void Enabled(bool b) = 0; virtual void OnPulse(char *s, int s_len) {} virtual bool IsReceive() { return false; } virtual bool InitMenus() { return false; } virtual void CreateMaps() = 0; virtual ScribeAccountletStatusIcon GetStatusIcon() { return STATUS_ERROR; } }; #undef RoProp class AccountletThread; class AccountIdentity : public Accountlet { public: AccountIdentity(ScribeAccount *a); AccStrOption(Name, OPT_AccIdentName); AccStrOption(Email, OPT_AccIdentEmail); AccStrOption(ReplyTo, OPT_AccIdentReply); AccStrOption(TextSig, OPT_AccIdentTextSig); AccStrOption(HtmlSig, OPT_AccIdentHtmlSig); AccIntOption(Sort, OPT_AccountSort); int UseSSL(int Set = -1) { return 0; } void Main(AccountletThread *Thread) {} LVariant Server(const char *Set = NULL) { return LVariant(); } LVariant UserName(const char *Set = NULL) { return LVariant(); } void Enabled(bool b) {} void CreateMaps(); bool IsValid(); bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; struct ScribeEnvelope { LString MsgId; LString SourceFolder; LString From; LArray To; LString References; LString FwdMsgId; LString BounceMsgId; LString Rfc822; }; class SendAccountlet : public Accountlet { friend class ScribeAccount; friend class ScribeWnd; LMenuItem *SendItem; public: LArray Outbox; SendAccountlet(ScribeAccount *a); ~SendAccountlet(); // Methods void Main(AccountletThread *Thread); void Enabled(bool b); bool InitMenus(); void CreateMaps(); ScribeAccountletStatusIcon GetStatusIcon(); // Sending options AccStrOption(Server, OPT_SmtpServer); AccIntOption(Port, OPT_SmtpPort); AccStrOption(Domain, OPT_SmtpDomain); AccStrOption(UserName, OPT_SmtpName); AccIntOption(RequireAuthentication, OPT_SmtpAuth); AccIntOption(AuthType, OPT_SmtpAuthType); AccStrOption(PrefCharset1, OPT_SendCharset1); AccStrOption(PrefCharset2, OPT_SendCharset2); AccStrOption(HotFolder, OPT_SendHotFolder); AccIntOption(OnlySendThroughThisAccount, OPT_OnlySendThroughThis); /// Get/Set the SSL mode /// \sa #SSL_NONE, #SSL_STARTTLS or #SSL_DIRECT AccIntOption(UseSSL, OPT_SmtpSSL); bool IsConfigured() { LVariant hot = HotFolder(); if (hot.Str()) { bool Exists = LDirExists(hot.Str()); printf("%s:%i - '%s' exists = %i\n", _FL, hot.Str(), Exists); return Exists; } return ValidStr(Server().Str()); } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; typedef LHashTbl, LXmlTag*> MsgListHash; class MsgList : protected MsgListHash { LOptionsFile *Opts; LString Tag; bool Loaded; bool Load(); LXmlTag *LockId(const char *id, const char *file, int line); void Unlock(); public: typedef MsgListHash Parent; bool Dirty; MsgList(LOptionsFile *Opts, char *Tag); ~MsgList(); // Access methods bool Add(const char *id); bool Delete(const char *id); int Length(); bool Find(const char *id); void Empty(); LString::Array CopyKeys(); // Dates bool SetDate(char *id, LDateTime *dt); bool GetDate(char *id, LDateTime *dt); }; class ReceiveAccountlet : public Accountlet { friend class ScribeAccount; friend class ScribeWnd; friend class ImapThread; friend class ScpThread; LArray Actions; LList *Items; int SecondsTillOnline; LMenuItem *ReceiveItem; LMenuItem *PreviewItem; List *IdTemp; LAutoPtr SettingStore; LAutoPtr Msgs; LAutoPtr Spam; public: ReceiveAccountlet(ScribeAccount *a); ~ReceiveAccountlet(); // Props LList *GetItems() { return Items; } bool SetItems(LList *l); bool SetActions(LArray *a = NULL); bool IsReceive() { return true; } bool IsPersistant(); // Methods void Main(AccountletThread *Thread); void OnPulse(char *s, int s_len); bool OnIdle(); void Enabled(bool b); bool InitMenus(); int GetCheckTimeout(); void CreateMaps(); ScribeAccountletStatusIcon GetStatusIcon(); // Message list bool HasMsg(const char *Id); void AddMsg(const char *Id); void RemoveMsg(const char *Id); void RemoveAllMsgs(); int GetMsgs(); // Spam list void DeleteAsSpam(const char *Id); bool RemoveFromSpamIds(const char *Id); bool IsSpamId(const char *Id, bool Delete = false); // Receive options AccStrOption(Protocol, OPT_Pop3Protocol); ScribeProtocol ProtocolType() { return ProtocolStrToEnum(Protocol().Str()); } AccStrOption(Server, OPT_Pop3Server); AccIntOption(Port, OPT_Pop3Port); AccStrOption(UserName, OPT_Pop3Name); AccStrOption(DestinationFolder, OPT_Pop3Folder); AccIntOption(AutoReceive, OPT_Pop3AutoReceive); AccStrOption(CheckTimeout, OPT_Pop3CheckEvery); AccIntOption(LeaveOnServer, OPT_Pop3LeaveOnServer); AccIntOption(DeleteAfter, OPT_DeleteAfter); AccIntOption(DeleteDays, OPT_DeleteDays); AccIntOption(DeleteLarger, OPT_DeleteIfLarger); AccIntOption(DeleteSize, OPT_DeleteIfLargerSize); AccIntOption(DownloadLimit, OPT_MaxEmailSize); AccStrOption(Assume8BitCharset, OPT_Receive8BitCs); AccStrOption(AssumeAsciiCharset, OPT_ReceiveAsciiCs); AccIntOption(AuthType, OPT_ReceiveAuthType); AccStrOption(HotFolder, OPT_ReceiveHotFolder); AccIntOption(SecureAuth, OPT_ReceiveSecAuth); /// Get/Set the SSL mode /// \sa #SSL_NONE, #SSL_STARTTLS or #SSL_DIRECT AccIntOption(UseSSL, OPT_Pop3SSL); bool IsConfigured() { LVariant hot = HotFolder(); if (hot.Str() && LDirExists(hot.Str())) { return true; } LVariant v = Server(); bool s = ValidStr(v.Str()); v = Protocol(); if (!v.Str() || _stricmp(v.Str(), PROTOCOL_POP_OVER_HTTP) != 0) { v = UserName(); s &= ValidStr(v.Str()); } return s; } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL); bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL); }; class ScribeAccount : public LDom, public LXmlTreeUi, public LCapabilityClient { friend class ScribeWnd; friend class ScribePopViewer; friend class AccountStatusPanel; friend class Accountlet; friend class SendAccountlet; friend class ReceiveAccountlet; protected: class ScribeAccountPrivate *d; ScribeWnd *Parent; ScribeFolder *&GetRoot(); void SetIndex(int i); public: // Data AccountIdentity Identity; SendAccountlet Send; ReceiveAccountlet Receive; LArray Views; // Object ScribeAccount(ScribeWnd *parent, int index); ~ScribeAccount(); const char *GetClass() override { return "ScribeAccount"; } // Lifespan bool IsValid(); bool Create(); bool Delete(); // Properties ScribeWnd *GetApp() { return Parent; } int GetIndex(); bool IsOnline(); void SetCheck(bool c); LMenuItem *GetMenuItem(); void SetMenuItem(LMenuItem *i); void OnEndSession() { Send.OnEndSession(); Receive.OnEndSession(); } // Commands void Stop(); bool Disconnect(); void Kill(); void SetDefaults(); // User interface void InitUI(LView *Parent, int Tab, std::function callback); bool InitMenus(); void SerializeUi(LView *Wnd, bool Load); int OnNotify(LViewI *Ctrl, LNotification &n); // Worker void OnPulse(char *s = NULL, int s_len = 0); void ReIndex(int i); void CreateMaps(); // LDom interface bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool SetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; }; ////////////////////////////////////////////////////////////////////// class ScribeClass ScribeDom : public LDom { ScribeWnd *App; public: Mail *Email; Contact *Con; ContactGroup *Grp; Calendar *Cal; Filter *Fil; ScribeDom(ScribeWnd *a); const char *GetClass() override { return "ScribeDom"; } bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; }; ////////////////////////////////////////////////////////////////////// #include "BayesianFilter.h" #include "Components.h" class LMailStore { public: bool Default, Expanded; LString Name; LString Path; LDataStoreI *Store; ScribeFolder *Root; LMailStore() { Expanded = true; Default = false; Store = NULL; Root = NULL; } bool IsOk() { return Store != NULL && Root != NULL; } int Priority() { int Ver = Store ? (int)Store->GetInt(FIELD_VERSION) : 0; return Ver; } void Empty() { DeleteObj(Store); Name.Empty(); Path.Empty(); Root = NULL; } LMailStore &operator =(LMailStore &a) { LAssert(0); return *this; } }; struct OptionsInfo { LString File; char *Leaf; int Score; uint64 Mod; bool Usual; OptionsInfo(); OptionsInfo &operator =(char *p); LAutoPtr Load(); }; class ScribeClass ScribeWnd : public LWindow, public LDom, public LDataEventsI, public BayesianFilter, public CapabilityInstaller, public LCapabilityTarget { friend class ScribeAccount; friend class Accountlet; friend class SendAccountlet; friend class ReceiveAccountlet; friend class AccountStatusPanel; friend class ScribeFolder; friend class OptionsDlg; friend class LoadWordStoreThread; friend struct ScribeReplicator; friend struct LoadMailStoreState; friend struct UnitTestState; public: enum LayoutMode { OptionsLayout = 0, ///-------------------- /// | | /// Folders | List | /// | | /// |---------| /// | Preview | ///-------------------- FoldersListAndPreview = 1, ///----------------- /// | | /// Folders | List | /// | | ///----------------- /// Preview | ///----------------- PreviewOnBottom, ///----------------- /// | | /// Folders | List | /// | | ///----------------- FoldersAndList, ///--------------------------- /// | | | /// Folders | List | Preview | /// | | | ///--------------------------- ThreeColumn, }; enum AppState { ScribeConstructing, // In Construct1 + Construct2 ScribeConstructed, // Finished Construct2 and ready for Construct3 ScribeInitializing, // In Construct3 ScribeRunning, ScribeExiting, ScribeLoadingFolders, ScribeUnloadingFolders, }; AppState GetScribeState() { return ScribeState; } protected: class ScribeWndPrivate *d = NULL; // Ipc class ScribeIpc *Ipc = NULL; // Accounts List Accounts; // New Mail stuff class LNewMailDlg *NewMailDlg = NULL; static AppState ScribeState; DoEvery Ticker; int64 LastDrop = 0; // Static LSubMenu *File = NULL; LSubMenu *ContactsMenu = NULL; LSubMenu *Edit = NULL; LSubMenu *Help = NULL; LToolBar *Commands = NULL; // Dynamic LSubMenu *IdentityMenu = NULL; LMenuItem *DefaultIdentityItem = NULL; LSubMenu *MailMenu = NULL; LSubMenu *SendMenu = NULL, *ReceiveMenu = NULL, *PreviewMenu = NULL; LMenuItem *SendItem = NULL, *ReceiveItem = NULL, *PreviewItem = NULL; LSubMenu *NewTemplateMenu = NULL; LMenuItem *WorkOffline = NULL; // Commands LCommand CmdSend; LCommand CmdReceive; LCommand CmdPreview; // Storage LArray Folders; LArray FolderTasks; List PostValidateFree; // Main view LAutoPtr ImageList; LAutoPtr ToolbarImgs; class LBox *Splitter = NULL; ThingList *MailList = NULL; class DynamicHtml *TitlePage = NULL; class LSearchView *SearchView = NULL; MailTree *Tree = NULL; class LPreviewPanel *PreviewPanel = NULL; class AccountStatusPanel *StatusPanel = NULL; // Security ScribePerm CurrentAuthLevel = PermRequireNone; // Methods void SetupUi(); void SetupAccounts(); int AdjustAllObjectSizes(LDataI *Item); bool CleanFolders(ScribeFolder *f); void LoadFolders(std::function Callback); void LoadMailStores(std::function Callback); bool ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName); bool UnLoadFolders(); void AddFolderToMru(char *FileName); void AddContactsToMenu(LSubMenu *Menu); bool FindWordDb(char *Out, int OutSize, char *Name); void OnFolderChanged(LDataFolderI *folder); bool ValidateFolder(LMailStore *s, int Id); void GrowlOnMail(Mail *m); void GrowlInfo(LString title, LString text); bool OnTransfer(); ScribeDomType StrToDom(const char *Var) { return ::StrToDom(Var); } const char* DomToStr(ScribeDomType p) { return ::DomToStr(p); } void LoadImageResources(); void DoOnTimer(LScriptCallback *c); enum CmdLineEvent { IpcEvent, StartupEvent }; void OnCommandLineEvent(CmdLineEvent event); public: ScribeWnd(); void Construct0(LOptionsFile::PortableType Type); void Construct1(); void Construct2(); void Construct3(); void SetLanguage(); ~ScribeWnd(); const char *GetClass() override { return "ScribeWnd"; } void DoDebug(char *s); void Validate(LMailStore *s); // Unit testing. static bool IsUnitTest; #ifdef _DEBUG void UnitTests(std::function Callback); #endif // Dom bool GetVariant(const char *Name, LVariant &Value, const char *Array = NULL) override; bool CallMethod(const char *MethodName, LScriptArguments &Args) override; // --------------------------------------------------------------------- // Methods LAutoString GetDataFolder(); LDataStoreI *CreateDataStore(const char *Full, bool CreateIfMissing); Thing *CreateThingOfType(Store3ItemTypes Type, LDataI *obj = NULL); Thing *CreateItem(int Type, ScribeFolder *Folder = NULL, bool Ui = true); Mail *CreateMail(Contact *c = NULL, const char *Email = NULL, const char *Name = NULL); Mail *LookupMailRef(const char *MsgRef, bool TraceAllUids = false); bool CreateFolders(LAutoString &FileName); bool CompactFolders(LMailStore &Store, bool Interactive = true); void Send(int Which = -1, bool Quiet = false); void Receive(int Which); void Preview(int Which); void OnBeforeConnect(ScribeAccount *Account, bool Receive); void OnAfterConnect(ScribeAccount *Account, bool Receive); bool NeedsCapability(const char *Name, const char *Param = NULL) override; void OnInstall(CapsHash *Caps, bool Status) override; void OnCloseInstaller() override; bool HasFolderTasks() { return FolderTasks.Length() > 0; } int GetEventHandle(); void Update(int What = 0); void UpdateUnRead(ScribeFolder *Folder, int Delta); void ThingPrint(std::function Callback, ThingType *m, LPrinter *Info = NULL, LView *Parent = NULL, int MaxPage = -1); bool OpenAMail(ScribeFolder *Folder); void BuildDynMenus(); LDocView *CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m = NULL); void SetLastDrop() { LastDrop = LCurrentTime(); } void SetListPane(LView *v); void SetLayout(LayoutMode Mode = OptionsLayout); bool IsMyEmail(const char *Email); bool SetItemPreview(LView *v); LOptionsFile::PortableType GetPortableType(); ScribeRemoteContent RemoteContent_GetSenderStatus(const char *Addr); void RemoteContent_ClearCache(); void RemoteContent_AddSender(const char *Addr, bool WhiteList); void SetDefaultHandler(); void OnSetDefaultHandler(bool Error, bool OldAssert); void SetCurrentIdentity(int i=-1); int GetCurrentIdentity(); bool MailReplyTo(Mail *m, bool All = false); bool MailForward(Mail *m); bool MailBounce(Mail *m); void MailMerge(LArray &Recip, const char *FileName, Mail *Source); void OnNewMail(List *NewMailObjs, bool Add = true); void OnNewMailSound(); void OnTrayClick(LMouse &m) override; void OnTrayMenu(LSubMenu &m) override; void OnTrayMenuResult(int MenuId) override; void OnFolderSelect(ScribeFolder *f); // This will be called for the first time when the following events have happened: // - Startup of the application has completed // - The ScribeIpc::OnLoad callback has been called with true // // And also potentially when a new (temporary) instance of Scribe has sent this // instance a command line. // // See also OnCommandLineEvent. void OnCommandLine(); void AddThingSrc(ScribeFolder *src); void RemoveThingSrc(ScribeFolder *src); LArray GetThingSources(Store3ItemTypes Type); bool GetContacts(List &Contacts, ScribeFolder *f = NULL, bool Deep = true); List *GetEveryone(); void HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder = NULL, bool Deep = true); // CapabilityInstaller impl LString GetHttpProxy() override; InstallProgress *StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *Action) override; class HttpImageThread *GetImageLoader(); int GetMaxPages(); ScribeWnd::LayoutMode GetEffectiveLayoutMode(); ThingList *GetMailList() { return MailList; } // Gets the matching mail store for a given identity LMailStore *GetMailStoreForIdentity ( /// If this is NULL, assume the current identity const char *IdEmail = NULL ); ScribeFolder *GetFolder(int Id, LMailStore *s = NULL, bool Quiet = false); ScribeFolder *GetFolder(int Id, LDataI *s); ScribeFolder *GetFolder(const char *Name, LMailStore *s = NULL); ScribeFolder *GetCurrentFolder(); int GetFolderType(ScribeFolder *f); LImageList *GetIconImgList() { return ImageList; } LAutoPtr &GetToolbarImgList() { return ToolbarImgs; } LString GetResourceFile(SribeResourceType Type); DoEvery *GetTicker() { return &Ticker; } ScribeAccount *GetSendAccount(); ScribeAccount *GetCurrentAccount(); ThingFilter *GetThingFilter(); List *GetAccounts() { return &Accounts; } ScribeAccount *GetAccountById(int Id); ScribeAccount *GetAccountByEmail(const char *Email); LPrinter *GetPrinter(); int GetActiveThreads(); int GetToolbarHeight(); void GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal); bool OnFolderTask(LEventTargetI *Ptr, bool Add); LArray &GetStorageFolders() { return Folders; } LMailStore *GetDefaultMailStore(); LMailStore *GetMailStoreForPath(const char *Path); bool OnMailStore(LMailStore **MailStore, bool Add); ThingList *GetItemList() { return MailList; } LColour GetColour(int i); LFont *GetPreviewFont(); LFont *GetBoldFont() { return LSysBold; } LToolBar *LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img); class LVmCallback *GetDebuggerCallback(); class GpgConnector *GetGpgConnector(); void GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback); int GetCalendarSources(LArray &Sources); Store3Status GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback); void GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback); const char* EditCtrlMimeType(); LAutoString GetReplyXml(const char *MimeType); LAutoString GetForwardXml(const char *MimeType); bool GetHelpFilesPath(char *Path, int PathSize); bool LaunchHelp(const char *File); LAutoString ProcessSig(Mail *m, char *Xml, const char *MimeType); LString ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType); bool LogFilterActivity(); ScribeFolder *FindContainer(LDataFolderI *f); bool SaveDirtyObjects(int TimeLimitMs = 100); class LSpellCheck *CreateSpellObject(); class LSpellCheck *GetSpellThread(bool OverrideOpt = false); bool SetSpellThreadParams(LSpellCheck *Thread); void OnSpellerSettingChange(); bool OnMailTransferEvent(MailTransferEvent *e); LViewI *GetView() { return this; } const char *GetUiTags(); struct MailStoreUpgradeParams { LAutoString OldFolders; LAutoString NewFolders; bool Quiet; LStream *Log; MailStoreUpgradeParams() { Quiet = false; Log = 0; } }; // Scripting support bool GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks); LScriptCallback GetCallback(const char *CallbackMethodName); int RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args); bool RemoveCallback(int Uid); LStream *ShowScriptingConsole(); bool ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs = false); LScriptEngine *GetScriptEngine(); // Options LOptionsFile *GetOptions(bool Create = false) override; bool ScanForOptionsFiles(LArray &Inf, LSystemPath PathType); bool LoadOptions(); bool SaveOptions(); bool IsSending() { return false; } bool ShowToolbarText(); bool IsValid(); // Data events from storage back ends bool GetSystemPath(int Folder, LVariant &Path) override; void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) override; bool OnDelete(LDataFolderI *parent, LArray &items) override; bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &Items) override; void SetContext(const char *file, int line) override; bool OnChange(LArray &items, int FieldHint) override; void Post(LDataStoreI *store, void *Param) override { PostEvent(M_STORAGE_EVENT, store->Id, (LMessage::Param)Param); } void OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) override; bool Match(LDataStoreI *store, LDataPropI *Addr, int Type, LArray &Matches) override; ContactGroup *FindGroup(char *Name); bool AddStore3EventHandler(LDataEventsI *callback); bool RemoveStore3EventHandler(LDataEventsI *callback); // --------------------------------------------------------------------- // Events void OnDelete(); int OnNotify(LViewI *Ctrl, LNotification n) override; void OnPaint(LSurface *pDC) override; LMessage::Result OnEvent(LMessage *Msg) override; int OnCommand(int Cmd, int Event, OsView Handle) override; void OnPulse() override; void OnPulseSecond(); bool OnRequestClose(bool OsShuttingDown) override; void OnSelect(List *l = NULL, bool ChangeEvent = false); void OnReceiveFiles(LArray &Files) override; void OnUrl(const char *Url) override; void OnZoom(LWindowZoom Action) override; void OnCreate() override; void OnMinute(); void OnHour(); bool OnIdle(); void OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) override; /// \returns true if spam bool OnBayesResult(const char *MailRef, double Rating) override; /// \returns true if spam bool OnBayesResult(Mail *m, double Rating); void OnScriptCompileError(const char *Source, Filter *f); }; //////////////////////////////////////////////////////////////////////////////////// #ifdef SCRIBE_APP #include "ScribePrivate.h" #endif