diff --git a/src/Calendar.cpp b/src/Calendar.cpp --- a/src/Calendar.cpp +++ b/src/Calendar.cpp @@ -1,3334 +1,3334 @@ /*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 1 #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(); LDateTime Then; 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'\n", Subj); #endif continue; } LDataI *Obj = c->GetObject(); LDateTime LastCheck = *Obj->GetDate(FIELD_CAL_LAST_CHECK); if (LastCheck.IsValid()) LastCheck.ToLocal(); 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]; uint64 n, t; Now.Get(n); Then.Get(t); int64_t Diff = (int64)t - (int64)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 = (int64)t - (int64)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) { uint64 remaining; i.Get(remaining); Diff = (int64_t)t - (int64_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 / DAY_1; auto ThenDay = t / 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); } 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; new CalendarSourceGetEvents(App, 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); Source = 0; 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); } LArray Dst; LDateTime::GetDaylightSavingsInfo(Dst, BaseUtc.s, &EndUtc); LAssert(Dst.Length() > 0); 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; const char *FilterYear = 0; const char *FilterPos = 0; int EndType = 0; LDateTime EndDate; int EndCount = 0; 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 = 0; int Count = 0; while (!Error) { // 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; } 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 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 Base = GetColour(); LColour Qtr = GdcMixColour(Base, LColour(L_WORKSPACE), 0.1f); LColour Half = GdcMixColour(Base, LColour(L_WORKSPACE), 0.4f); // not selected LColour f, b; if (Period && Now < Period->s) { // future entry (full colour) f = Base; b = Half; } else { // historical entry (half strength colour) f = Half; b = Qtr; } if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(Text, b); } pDC->Rectangle(&p); p.Inset((int)SX(1), (int)SY(1)); Font->Transparent(false); LDisplayString ds(Font, Title); float Ht = p.Y() > 0 ? (float)ds.Y() / p.Y() : 1.0f; if (Ht < 0.75f) p.Inset((int)SX(3), (int)SY(3)); else if (Ht < 0.95f) p.Inset((int)SX(1), (int)SY(1)); ds.Draw(pDC, p.x1, p.y1, &p); ViewPos.Union(Pos); } Thing &Calendar::operator =(Thing &Obj) { if (Obj.GetObject() && GetObject()) { GetObject()->CopyProps(*Obj.GetObject()); } return *this; } bool Calendar::operator ==(Thing &t) { Calendar *c = t.IsCalendar(); if (!c) return false; { LDateTime a, b; if (GetField(FIELD_CAL_START_UTC, a) && c->GetField(FIELD_CAL_START_UTC, b) && a != b) return false; if (GetField(FIELD_CAL_END_UTC, a) && c->GetField(FIELD_CAL_END_UTC, b) && a != b) return false; } { const char *a, *b; if (GetField(FIELD_CAL_SUBJECT, a) && c->GetField(FIELD_CAL_SUBJECT, b) && Stricmp(a, b)) return false; if (GetField(FIELD_CAL_LOCATION, a) && c->GetField(FIELD_CAL_LOCATION, b) && Stricmp(a, b)) return false; } return true; } int Calendar::Compare(LListItem *Arg, ssize_t FieldId) { Calendar *c1 = this; Calendar *c2 = dynamic_cast(Arg); if (c1 && c2) { switch (FieldId) { case FIELD_CAL_START_UTC: case FIELD_CAL_END_UTC: { LDateTime d1, d2; if (!c1->GetField((int)FieldId, d1)) d1.SetNow(); if (!c2->GetField((int)FieldId, d2)) d2.SetNow(); return d1.Compare(&d2); break; } case FIELD_CAL_SUBJECT: case FIELD_CAL_LOCATION: { const char *s1 = "", *s2 = ""; if (c1->GetField((int)FieldId, s1) && c2->GetField((int)FieldId, s2)) { return _stricmp(s1, s2); } break; } } } return 0; } uint32_t Calendar::GetFlags() { return 0; } ThingUi *Calendar::DoUI(MailContainer *c) { if (!Ui) { Ui = new CalendarUi(this); } return Ui; } ThingUi *Calendar::GetUI() { return Ui; } bool Calendar::SetUI(ThingUi *ui) { if (Ui) Ui->Item = NULL; Ui = dynamic_cast(ui); if (Ui) Ui->Item = this; return true; } void Calendar::OnMouseClick(LMouse &m) { if (m.Down()) { if (m.Left()) { if (m.Double()) { DoUI(); } } else if (m.Right()) { auto View = GetView(); DoContextMenu(m, View ? (LView*)View : (LView*)App); } } } void Calendar::OnCreate() { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } bool Calendar::OnDelete() { bool IsTodo = GetCalType() == CalTodo; bool Status = Thing::OnDelete(); if (IsTodo) DeleteObj(TodoView); for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); return Status; } bool Calendar::GetParentSelection(LList *Lst, List &s) { if (Lst) { return Lst->GetSelection(s); } if (auto View = GetView()) { LArray &a = View->GetSelection(); for (unsigned i=0; i(a[i])); } return true; } return false; } #define IDM_MOVE_TO 4000 void Calendar::DoContextMenu(LMouse &m, LView *Parent) { LSubMenu Sub; LScriptUi s(&Sub); if (s.Sub) { s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_INSPECT), IDM_INSPECT); CalendarView *Cv = dynamic_cast(Parent); if (Cv) { s.Sub->AppendSeparator(); for (unsigned n = 0; nGetName() && Cs->IsWritable()) { sprintf_s(m, sizeof(m), "Move to '%s'\n", Cs->GetName()); s.Sub->AppendItem(m, IDM_MOVE_TO + n, Source != Cs); } } } LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (unsigned i=0; iExecuteScriptCallback(*Callbacks[i], Args); Args.DeleteObjects(); } m.ToScreen(); int Result = s.Sub->Float(App, m.x, m.y); switch (Result) { default: { if (Cv) { int Idx = Result - IDM_MOVE_TO; if (Idx >= 0 && Idx < (int)CalendarSource::GetSources().Length()) { CalendarSource *Dst = CalendarSource::GetSources().ItemAt(Idx); if (Dst) { FolderCalendarSource *Fsrc = dynamic_cast(Dst); if (Fsrc) { const char *Path = Fsrc->GetPath(); ScribeFolder *DstFolder = App->GetFolder(Path); if (DstFolder) { LArray Items; Items.Add(this); DstFolder->MoveTo(Items, false); // FIXME: Impl moveto callback properly. if (Items[0] != (Thing*)this) return; Source = Dst; Cv->Invalidate(); } } else LAssert(!"Impl me."); } } } // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } break; } case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(Parent, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *PList = dynamic_cast(Parent); if (GetParentSelection(PList ? PList : GetList(), Del)) { for (auto i: Del) { Thing *t = dynamic_cast(i); if (t) { t->OnDelete(); } else { CalendarTodoItem *Todo = dynamic_cast(i); if (Todo) { Calendar *c = Todo->GetTodo(); c->OnDelete(); } } } } else { Thing *t = this; t->OnDelete(); } } break; } case IDM_EXPORT: { ExportAll(GetList(), sMimeVCalendar, NULL); break; } case IDM_INSPECT: { new ObjectInspector(App, this); break; } } } } const char *Calendar::GetText(int i) { static char s[64]; if (!GetObject()) { LAssert(!"No storage object"); return 0; } int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int) FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultCalenderFields)) { Field = DefaultCalenderFields[i]; } if (!Field) return 0; ItemFieldDef *Def = GetFieldDefById(Field); if (!Def) { LAssert(!"Where is the field def?"); return 0; } switch (Def->Type) { case GV_STRING: { return GetObject()->GetStr(Field); break; } case GV_INT32: { sprintf_s(s, sizeof(s), LPrintfInt64, GetObject()->GetInt(Field)); return s; break; } case GV_DATETIME: { // Get any effective timezone for this event. LString Tz = GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto dt = GetObject()->GetDate(Field); if (dt && dt->IsValid()) { LDateTime tmp = *dt; LOG_DEBUG("%s:%i - GetText.UTC %i = %s\n", _FL, i, tmp.Get().Get()); bool UseLocal = true; tmp.SetTimeZone(0, false); if (Tz.Get()) { bool HasPt = false, HasDigit = false; char *e = Tz.Get(); while (strchr(" \t\r\n-.+", *e) || IsDigit(*e)) { if (*e == '.') HasPt = true; if (IsDigit(*e)) HasDigit = true; e++; } if (HasDigit) { if (HasPt) { double TzHrs = Tz.Float(); double i, f = modf(TzHrs, &i); int Mins = (int) ((i * 60) + (f * 60)); tmp.AddMinutes(Mins); } else { int64 i = Tz.Int(); int a = (int)ABS(i); int Mins = (int) (((a / 100) * 60) + (a % 100)); tmp.AddMinutes(i < 0 ? -Mins : Mins); } UseLocal = false; } } if (UseLocal) tmp.ToLocal(); LOG_DEBUG("%s:%i - GetText.Local %i = %s\n", _FL, i, tmp.Get().Get()); tmp.Get(s, sizeof(s)); return s; } break; } default: { LAssert(0); break; } } return 0; } int *Calendar::GetDefaultFields() { return DefaultCalenderFields; } const char *Calendar::GetFieldText(int Field) { return 0; } bool Calendar::Overlap(Calendar *c) { if (c) { LDateTime Ts, Te, Cs, Ce; if (GetField(FIELD_CAL_START_UTC, Ts) && c->GetField(FIELD_CAL_START_UTC, Cs)) { if (!GetField(FIELD_CAL_END_UTC, Te)) { Te = Ts; Te.AddHours(1); } if (!c->GetField(FIELD_CAL_END_UTC, Ce)) { Ce = Cs; Ce.AddHours(1); } if ((Ce <= Ts) || (Cs >= Te)) { return false; } return true; } } return false; } bool Calendar::Save(ScribeFolder *Folder) { bool Status = false; // Check the dates are the right way around LDateTime Start, End; if (GetField(FIELD_CAL_START_UTC, Start) && GetField(FIELD_CAL_END_UTC, End)) { if (End < Start) { SetField(FIELD_CAL_START_UTC, End); SetField(FIELD_CAL_END_UTC, Start); } } auto ChangeEvent = [this](bool Status) { auto View = GetView(); if (View && Status) { View->OnContentsChanged(Source); OnSerialize(true); } }; if (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 { if (!Folder) { Folder = GetFolder(); } if (!Folder && App) { Folder = App->GetFolder(FOLDER_CALENDAR); } // FIXME: This can't wait for WriteThing to finish it's call back... Status = true; if (Folder) { LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow().ToUtc()); Folder->WriteThing(this, [this, ChangeEvent](auto Status) { if (Status > Store3Error) SetDirty(false); ChangeEvent(Status); }); } else ChangeEvent(Status); } return Status; } // Import/Export bool Calendar::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeVCalendar); return MimeTypes.Length() > 0; } Thing::IoProgress Calendar::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar) && Stricmp(mimeType, sMimeICalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Import(GetObject(), stream)) IoProgressError("vCal import failed."); IoProgressSuccess(); } Thing::IoProgress Calendar::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Export(GetObject(), stream)) IoProgressError("vCal export failed."); IoProgressSuccess(); } char *Calendar::GetDropFileName() { if (!DropFileName) { const char *Name = 0; GetField(FIELD_CAL_SUBJECT, Name); DropFileName.Reset(MakeFileName(Name ? Name : "Cal", "ics")); } return DropFileName; } bool Calendar::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); Export(AutoCast(F), sMimeVCalendar); } } if (LFileExists(DropFileName)) { Files.Add(DropFileName.Get()); Status = true; } } return Status; } void Calendar::OnPrintHeaders(ScribePrintContext &Context) { LDisplayString *ds = Context.Text(LLoadString(IDS_CAL_EVENT)); LRect &r = Context.MarginPx; int Line = ds->Y(); LDrawListSurface *Page = Context.Pages.Last(); Page->Rectangle(r.x1, Context.CurrentY + (Line * 5 / 10), r.x2, Context.CurrentY + (Line * 6 / 10)); Context.CurrentY += Line; } void Calendar::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Print document for (ItemFieldDef *Fld = CalendarFields; Fld->FieldId; Fld++) { LString Value; switch (Fld->Type) { case GV_STRING: { Value = GetObject()->GetStr(Fld->FieldId); break; } case GV_DATETIME: { auto Dt = GetObject()->GetDate(Fld->FieldId); if (Dt) { char s[64] = ""; Dt->Get(s, sizeof(s)); Value = s; } break; } default: break; } if (Value) { LString f; const char *Name = LLoadString(Fld->FieldId); f.Printf("%s: %s", Name ? Name : Fld->DisplayText, Value.Get()); // LDisplayString *ds = Context.Text(f); } } } bool Calendar::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: // Type: String Value = GetObject()->GetStr(FIELD_CAL_SUBJECT); break; case SdTo: // Type: String Value = GetObject()->GetStr(FIELD_TO); break; case SdLocation: // Type: String Value = GetObject()->GetStr(FIELD_CAL_LOCATION); break; case SdUid: // Type: String Value = GetObject()->GetStr(FIELD_UID); break; case SdReminders: // Type: String Value = GetObject()->GetStr(FIELD_CAL_REMINDERS); break; case SdNotes: // Type: String Value = GetObject()->GetStr(FIELD_CAL_NOTES); break; case SdStatus: // Type: String Value = GetObject()->GetStr(FIELD_CAL_STATUS); break; // Int variables case SdType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_TYPE); break; case SdCompleted: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_COMPLETED); break; case SdShowTimeAs: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_SHOW_TIME_AS); break; case SdRecur: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR); break; case SdRecurFreq: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FREQ); break; case SdRecurInterval: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_INTERVAL); break; case SdRecurEndCount: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_COUNT); break; case SdRecurEndType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_TYPE); break; case SdRecurFilterDays: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); break; case SdRecurFilterMonths: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_MONTHS); break; case SdPrivacy: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_PRIVACY); break; case SdAllDay: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_ALL_DAY); break; case SdColour: // Type: Int64 Value = GetObject()->GetInt(FIELD_COLOUR); break; // Date time fields case SdStart: // Type: DateTime return GetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: // Type: DateTime return GetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: // Type: DateTime return GetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: Value = GetObject()->SetStr(FIELD_CAL_SUBJECT, Value.Str()); break; case SdTo: Value = GetObject()->SetStr(FIELD_TO, Value.Str()); break; case SdLocation: Value = GetObject()->SetStr(FIELD_CAL_LOCATION, Value.Str()); break; case SdUid: Value = GetObject()->SetStr(FIELD_UID, Value.Str()); break; case SdReminders: Value = GetObject()->SetStr(FIELD_CAL_REMINDERS, Value.Str()); break; case SdNotes: Value = GetObject()->SetStr(FIELD_CAL_NOTES, Value.Str()); break; case SdStatus: Value = GetObject()->SetStr(FIELD_CAL_STATUS, Value.Str()); break; // Int variables case SdType: Value = GetObject()->SetInt(FIELD_CAL_TYPE, Value.CastInt32()); break; case SdCompleted: Value = GetObject()->SetInt(FIELD_CAL_COMPLETED, Value.CastInt32()); break; case SdShowTimeAs: Value = GetObject()->SetInt(FIELD_CAL_SHOW_TIME_AS, Value.CastInt32()); break; case SdRecur: Value = GetObject()->SetInt(FIELD_CAL_RECUR, Value.CastInt32()); break; case SdRecurFreq: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FREQ, Value.CastInt32()); break; case SdRecurInterval: Value = GetObject()->SetInt(FIELD_CAL_RECUR_INTERVAL, Value.CastInt32()); break; case SdRecurEndCount: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_COUNT, Value.CastInt32()); break; case SdRecurEndType: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_TYPE, Value.CastInt32()); break; case SdRecurFilterDays: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, Value.CastInt32()); break; case SdRecurFilterMonths: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, Value.CastInt32()); break; case SdPrivacy: Value = GetObject()->SetInt(FIELD_CAL_PRIVACY, Value.CastInt32()); break; case SdColour: Value = GetObject()->SetInt(FIELD_COLOUR, Value.CastInt32()); break; case SdAllDay: Value = GetObject()->SetInt(FIELD_CAL_ALL_DAY, Value.CastInt32()); break; // Date time fields case SdStart: return SetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: return SetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::CallMethod(const char *MethodName, LVariant *ReturnValue, LArray &Args) { // ScribeDomType Fld = StrToDom(MethodName); return Thing::CallMethod(MethodName, ReturnValue, 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->ShowColumnHeader(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->ShowColumnHeader(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) { 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) { return 0; } void CalendarUi::OnPosChange() { LWindow::OnPosChange(); } void CalendarUi::CheckConsistancy() { 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; dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(GetCtrlName(IDC_START_TIME)); return dt; } LDateTime CalendarUi::CurrentEnd() { LDateTime dt; dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(GetCtrlName(IDC_END_TIME)); return dt; } void CalendarUi::UpdateStartRelative() { LDateTime dt = CurrentStart(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_START_REL, s); } } void CalendarUi::UpdateEndRelative() { LDateTime dt = CurrentEnd(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_END_REL, s); } } void CalendarUi::UpdateRelative() { CheckConsistancy(); UpdateStartRelative(); UpdateEndRelative(); } int CalendarUi::OnNotify(LViewI *Ctrl, LNotification n) { 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(WhiteSpace, *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, 0); + 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 cancelling 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() { // 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(NULL)) { 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() { // 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,3490 +1,3507 @@ #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 "CalendarView.h" #include "ScribePageSetup.h" #include "Store3Webdav/WebdavStore.h" #include "resdefs.h" #include "resource.h" char 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++) { LItemColumn *c = GetList()->ColumnAt(i); if (c) { if (c->Mark() == GLI_MARK_UP_ARROW) { Sort = -(i + 1); break; } else if (c->Mark() == GLI_MARK_DOWN_ARROW) { 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 (unsigned i=0; i(GetWindow()); LArray all; if (w->CalLst->GetAll(all)) { for (auto a: all) a->OnPulse(); } } 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) { // LgiTrace("%s\n", c.ToString().Get()); InCur.Add(c.c, true); } for (unsigned i=0; iSource = 0; } new CalendarSourceGetEvents(App, s, e, CalendarSource::GetSources(), [this](auto Events) { Current = Events; Invalidate(); }); 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); uint64 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 (uint32_t i=0; i 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, Tomorrow)) { 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, Tomorrow)) { 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::GDstInfo *CalendarView::GetDstForDate(LDateTime t) { uint64 ts = t; for (uint32_t i=0; i= Prev && ts < Next) { return &Dst[i]; } } else if (ts > Dst[i].UtcTimeStamp) { 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) { CalendarSource *Src = FolderCalendarSource::GetCreateIn(); if (!Src) return NULL; Calendar *c = Src->NewEvent(); if (!c) return NULL; LDateTime Start, End; if (dtStart < dtEnd) { Start = dtStart; End = dtEnd; } else { Start = dtEnd; End = dtStart; } LDateTime n = Start; LDateTime::GDstInfo *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); LgiTrace("Start=%s, End=%s\n", Start.Get().Get(), End.Get().Get()); 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(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } 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(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } else if (Selection.Length()) { DragMode = DragMoveSelection; DragEnd = *Hit; 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); } // 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; int64 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)) { auto c = First->NewEvent(); if (c) c->Import(c->AutoCast(in), type); } } } else LgiTrace("%s:%i - Do data sources?\n", _FL); } else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) { 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) { FolderCalendarSource *Src = dynamic_cast(CalLst->GetSelected()); if (Src) Cv->OnContentsChanged(Src); } 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 ctrlId) { if (ctrlId) { 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->Mark() == GLI_MARK_DOWN_ARROW) { c->Mark(GLI_MARK_UP_ARROW); Sort = -(i + 1); } else { c->Mark(GLI_MARK_DOWN_ARROW); Sort = i + 1; } } else { c->Mark(GLI_MARK_NONE); } } } 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