diff --git a/src/Calendar.cpp b/src/Calendar.cpp --- a/src/Calendar.cpp +++ b/src/Calendar.cpp @@ -1,3715 +1,3719 @@ /*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 "lgi/common/FileSelect.h" #include "CalendarView.h" #include "PrintContext.h" #include "resdefs.h" #include "resource.h" #include "AddressSelect.h" #include "ObjectInspector.h" #define MAX_RECUR 1024 #define DEBUG_REMINDER 0 #define DEBUG_DATES 0 #if DEBUG_DATES #define LOG_DEBUG(...) LgiTrace(__VA_ARGS__) #else #define LOG_DEBUG(...) #endif ////////////////////////////////////////////////////////////////////////////// ItemFieldDef CalendarFields[] = { {"Start", SdStart, GV_DATETIME, FIELD_CAL_START_UTC, IDC_START_DATE, 0, true}, {"End", SdEnd, GV_DATETIME, FIELD_CAL_END_UTC, IDC_END_DATE, 0, true}, {"Subject", SdSubject, GV_STRING, FIELD_CAL_SUBJECT, IDC_SUBJECT}, {"Location", SdLocation, GV_STRING, FIELD_CAL_LOCATION, IDC_LOCATION}, {"Show Time As", SdShowTimeAs, GV_INT32, FIELD_CAL_SHOW_TIME_AS, IDC_AVAILABLE_TYPE}, {"All Day", SdAllDay, GV_BOOL, FIELD_CAL_ALL_DAY, IDC_ALL_DAY}, // TRUE if the calendar event recurs {"Recur", SdRecur, GV_INT32, FIELD_CAL_RECUR, -1}, // Base time unit of recurring event. See enum CalRecurFreq: days, weeks, months, years. {"Recur Freq", SdRecurFreq, GV_INT32, FIELD_CAL_RECUR_FREQ, -1}, // Number of FIELD_CAL_RECUR_FREQ units of time between recurring events. (Minimum is '1') {"Recur Interval", SdRecurInterval, GV_INT32, FIELD_CAL_RECUR_INTERVAL, -1}, // Bitfield of days, Bit 0 = Sunday, Bit 1 = Monday, Bit 2 = Teusday etc. {"Filter Days", SdFilterDays, GV_INT32, FIELD_CAL_RECUR_FILTER_DAYS, -1}, // Bitfield of months, Bit 0 = Janurary, Bit 1 = feburary, Bit 2 = March etc. {"Filter Months", SdFilterMonths, GV_INT32, FIELD_CAL_RECUR_FILTER_MONTHS, -1}, // String of year numbers separated by commas: "2010,2014,2018" {"Filter Years", SdFilterYears, GV_STRING, FIELD_CAL_RECUR_FILTER_YEARS, -1}, // Position in month, "1" means the first matching day of the month. Multiple indexes can be joined with ',' // like: "1,3" {"Filter Pos", SdFilterPos, GV_STRING, FIELD_CAL_RECUR_FILTER_POS, -1}, // See the CalRecurEndType enumeration {"Recur End Type", SdRecurEndType, GV_INT32, FIELD_CAL_RECUR_END_TYPE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnDate then this specifies the date to end {"Recur End Date", SdRecurEndDate, GV_DATETIME, FIELD_CAL_RECUR_END_DATE, -1}, // If FIELD_CAL_RECUR_END_TYPE==CalEndOnCount then this specifies the count of events {"Recur End Count", SdRecurEndCount, GV_INT32, FIELD_CAL_RECUR_END_COUNT, -1}, // The timezone the start and end are referencing {"Timezone", SdTimeZone, GV_STRING, FIELD_CAL_TIMEZONE, IDC_TIMEZONE}, // User defined notes for the object {"Notes", SdNotes, GV_STRING, FIELD_CAL_NOTES, IDC_DESCRIPTION}, // See the CalendarType enumeration {"Type", SdType, GV_INT32, FIELD_CAL_TYPE, -1}, {"Reminders", SdReminders, GV_STRING, FIELD_CAL_REMINDERS, -1}, {"LastCheck", SdLastCheck, GV_DATETIME, FIELD_CAL_LAST_CHECK, -1}, {"DateModified", SdDateModified, GV_DATETIME, FIELD_DATE_MODIFIED, -1}, {0} }; ////////////////////////////////////////////////////////////////////////////// const char *sReminderType[] = { "Email", "Popup", "ScriptCallback" }; const char *sReminderUnits[] = { "Minutes", "Hours", "Days", "Weeks" }; class ReminderItem : public LListItem { CalendarReminderType Type; float Value; CalendarReminderUnits Units; LString Param; public: ReminderItem(CalendarReminderType type, float value, CalendarReminderUnits units, LString param) { Type = type; Value = value; Units = units; Param = param; Update(); SetText("x", 1); } ReminderItem(LString s) { if (!SetString(s)) { // Default Type = CalPopup; Value = 1.0; Units = CalMinutes; Param.Empty(); } Update(); SetText("x", 1); } CalendarReminderType GetType() { return Type; } float GetValue() { return Value; } CalendarReminderUnits GetUnits() { return Units; } LString GetParam() { return Param; } LString GetString() { // See also FIELD_CAL_REMINDERS LString s; s.Printf("%g,%i,%i,%s", Value, Units, Type, Param?LUrlEncode(Param, ",\r\n").Get():""); return s; } bool SetString(LString s) { // See also FIELD_CAL_REMINDERS LString::Array a = s.Split(","); if (a.Length() < 3) return false; Value = (float) a[0].Float(); Units = (CalendarReminderUnits) a[1].Int(); Type = (CalendarReminderType) a[2].Int(); if (a.Length() > 3) Param = LUrlDecode(a[3]); return true; } void Update() { char s[300]; if (Param) sprintf_s(s, sizeof(s), "%s '%s' @ %g %s", sReminderType[Type], Param.Get(), Value, sReminderUnits[Units]); else sprintf_s(s, sizeof(s), "%s @ %g %s", sReminderType[Type], Value, sReminderUnits[Units]); SetText(s); } void OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LColour old = Ctx.Fore; if (i == 1) Ctx.Fore.Rgb(255, 0, 0); LListItem::OnPaintColumn(Ctx, i, c); Ctx.Fore = old; } void OnMouseClick(LMouse &m) { if (m.Down() && m.Left()) { if (GetList()->ColumnAtX(m.x) == 1) { delete this; return; } } LListItem::OnMouseClick(m); } }; //////////////////////////////////////////////////////////////////////////////////// static int DefaultCalenderFields[] = { FIELD_CAL_SUBJECT, FIELD_CAL_START_UTC, FIELD_CAL_END_UTC, 0, }; #define MINUTE 1 // we're in minutes... #define HOUR (60 * MINUTE) #define DAY (24 * HOUR) int ReminderOffsets[] = { 0, 15 * MINUTE, 30 * MINUTE, 1 * HOUR, 2 * HOUR, 3 * HOUR, 4 * HOUR, 5 * HOUR, 6 * HOUR, 7 * HOUR, 8 * HOUR, 9 * HOUR, 10 * HOUR, 11 * HOUR, 12 * HOUR, 1 * DAY, 2 * DAY }; ////////////////////////////////////////////////////////////////////////////// List Calendar::Reminders; int Calendar::DayStart = -1; int Calendar::DayEnd = -1; int Calendar::WorkDayStart = -1; int Calendar::WorkDayEnd = -1; int Calendar::WorkWeekStart = -1; int Calendar::WorkWeekEnd = -1; void InitCalendarView() { Calendar::DayStart = 6; Calendar::DayEnd = 23; Calendar::WorkDayStart = -1; Calendar::WorkDayEnd = -1; Calendar::WorkWeekStart = -1; Calendar::WorkWeekEnd = -1; auto s = LAppInst->GetConfig("Scribe.Calendar.WorkDayStart"); if (s) Calendar::WorkDayStart = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkDayEnd"); if (s) Calendar::WorkDayEnd = (int)s.Int(); s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekStart"); if (s) Calendar::WorkWeekStart = (int)s.Int() + 1; s = LAppInst->GetConfig("Scribe.Calendar.WorkWeekEnd"); if (s) Calendar::WorkWeekEnd = (int)s.Int() + 1; if (Calendar::WorkDayStart < 0) Calendar::WorkDayStart = 9; if (Calendar::WorkDayEnd < 0) Calendar::WorkDayEnd = 18; if (Calendar::WorkWeekStart < 0) Calendar::WorkWeekStart = 1; if (Calendar::WorkWeekEnd < 0) Calendar::WorkWeekEnd = 5; } void Calendar::CheckReminders() { LDateTime Now; Now.SetNow(); #if DEBUG_REMINDER // Get all the times for today, including recent ones Now.SetTime("0:0:0"); #endif LDateTime Then = Now; Then.AddDays(1); #if DEBUG_REMINDER LgiTrace("%s:%i - Reminders.Len=%i, Now=%s, Then=%s\n", _FL, Reminders.Length(), Now.Get().Get(), Then.Get().Get()); #endif for (auto c: Reminders) { auto App = c->App; LHashTbl,bool> Added; Thing *ReminderThing = NULL; Mail *ReminderEmail = NULL; // Is a reminder on the entry? if (!c->GetObject()) continue; LString Rem = c->GetObject()->GetStr(FIELD_CAL_REMINDERS); if (!Rem) continue; const char *Subj = NULL, *Notes = NULL; c->GetField(FIELD_CAL_SUBJECT, Subj); c->GetField(FIELD_CAL_NOTES, Notes); LArray Times; if (!c->GetTimes(Now, Then, Times)) { #if DEBUG_REMINDER // LgiTrace(" No times for '%s', now=%s, then=%s\n", Subj, Now.Get().Get(), Then.Get().Get()); #endif continue; } auto Obj = c->GetObject(); if (!Obj) continue; LDateTime LastCheck = *Obj->GetDate(FIELD_CAL_LAST_CHECK); if (LastCheck.IsValid()) { LastCheck.ToLocal(); #if DEBUG_REMINDER // Helps with debugging... LastCheck.AddDays(-1); #endif } LString::Array r = Rem.SplitDelimit("\n"); for (unsigned i=0; i LastCheck; bool b = ts <= Now; LgiTrace(" last check = %s\n", LastCheck.Get().Get()); LgiTrace(" now = %s\n", Now.Get().Get()); LgiTrace(" %i %i\n", a, b); #endif if ( ( !LastCheck.IsValid() || ts > LastCheck ) && ts <= Now ) { // Save the last check field now auto NowUtc = Now.Utc(); Obj->SetDate(FIELD_CAL_LAST_CHECK, &NowUtc); c->SetDirty(); // Fire the event switch (ri.GetType()) { case CalEmail: { ScribeAccount *Acc = App->GetCurrentAccount(); if (!Acc || !Acc->Identity.IsValid()) { LView *Ui = c->GetUI(); LgiMsg(Ui ? Ui : c->App, "No from account to use for sending event notifications.", AppName, MB_OK); break; } auto Email = Acc->Identity.Email(); auto Name = Acc->Identity.Name(); LgiTrace("%s:%i - Using account ('%s' '%s') for the event reminder 'from' address.\n", _FL, Email.Str(), Name.Str()); if (!ReminderThing) ReminderThing = App->CreateThingOfType(MAGIC_MAIL); if (!ReminderEmail) { ReminderEmail = ReminderThing ? ReminderThing->IsMail() : NULL; if (ReminderEmail) { LString s; s.Printf("Calendar Notification: %s", Subj); ReminderEmail->SetSubject(s); s.Printf("The event '%s' is due at: %s\n" "\n" "%s\n" "\n" "(This email was generated by Scribe)", Subj, t.s.Get().Get(), Notes ? Notes : ""); ReminderEmail->SetBody(s); auto Frm = ReminderEmail->GetFrom(); Frm->SetStr(FIELD_EMAIL, Email.Str()); Frm->SetStr(FIELD_NAME, Name.Str()); } } if (ReminderEmail) { auto To = ReminderEmail->GetTo(); // Add any custom parameter email address: LString Param = ri.GetParam(); if (LIsValidEmail(Param)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { Added.Add(Param, true); Recip->SetStr(FIELD_EMAIL, Param); To->Insert(Recip); } } // Add all the guests LString sGuests = c->GetObject()->GetStr(FIELD_TO); LString::Array Guests = sGuests.SplitDelimit(","); for (auto Guest: Guests) { LAutoString e, n; DecodeAddrName(Guest, n, e, NULL); #if DEBUG_REMINDER LgiTrace("Attendee=%s,%s\n", n.Get(), e.Get()); #endif if (LIsValidEmail(e.Get()) && !Added.Find(e)) { auto Recip = To->Create(c->GetObject()->GetStore()); if (Recip) { if (n) Recip->SetStr(FIELD_NAME, n); Recip->SetStr(FIELD_EMAIL, e); To->Insert(Recip); Added.Add(e, true); } } else { #if DEBUG_REMINDER LgiTrace("Attendee not valid or added already: %s\n", e.Get()); #endif } } } break; } case CalPopup: { if (LgiMsg( 0, // this causes the dialog to be ontop of everything else LLoadString(IDS_EVENT_DUE), AppName, MB_YESNO | MB_SYSTEMMODAL, Subj) == IDYES) { // Open the calendar entry c->DoUI(); } break; } case CalScriptCallback: { break; } default: { LgiTrace("%s:%i - Unknown reminder type.\n", _FL); break; } } } } } } if (ReminderEmail) { ReminderEmail->CreateMailHeaders(); ReminderEmail->Update(); ReminderEmail->Send(true); } } } #define MIN_1 ((int64)LDateTime::Second64Bit * 60) #define HOUR_1 (MIN_1 * 60) #define DAY_1 (HOUR_1 * 24) #define YEAR_1 (DAY_1 * 365) const char *RelativeTime(LDateTime &Then) { static char s[256]; static const int Id[] = { IDS_CAL_LDAY_SUN, IDS_CAL_LDAY_MON, IDS_CAL_LDAY_TUE, IDS_CAL_LDAY_WED, IDS_CAL_LDAY_THU, IDS_CAL_LDAY_FRI, IDS_CAL_LDAY_SAT, }; LDateTime Now; Now.SetNow(); int64_t tzSeconds = Now.GetTimeZone() * MIN_1; char Val[64]; LTimeStamp n, t; Now.Get(n); Then.Get(t); auto Diff = t - n; int Yrs = 0; int Months = 0; int Days = 0; int Hrs = 0; int Mins = 0; LDateTime i = Now; int Inc = Then > Now ? 1 : -1; char DirIndcator = Then > Now ? '+' : '-'; while (ABS(Diff) > YEAR_1) { Yrs++; i.Year(i.Year()+Inc); if (!i.IsValid()) break; i.Get(n); Diff = t - n; } int TotalDays = 0; if (ABS(Diff) > DAY_1) { TotalDays = Days = (int) (Diff / DAY_1); while (true) { LDateTime first = i; first.AddMonths(Inc); if ( (Inc < 0 && first > Then) // Tracking back in time.. || (Inc > 0 && Then > first) // Forward in time.. ) { Months += Inc; i = first; } else break; } if (Months) { LTimeStamp remaining; i.Get(remaining); Diff = t - remaining; Days = (int) (Diff / DAY_1); } Diff -= (int64) Days * DAY_1; } if (ABS(Diff) > HOUR_1) { Hrs = (int) (Diff / HOUR_1); Diff -= (int64) Hrs * HOUR_1; } if (ABS(Diff) > MIN_1) { Mins = (int) (Diff / MIN_1); Diff -= (int64) Mins * MIN_1; } if (Yrs) { // Years + months sprintf_s(Val, sizeof(Val), "%c%iy %im", DirIndcator, abs(Yrs), abs(Months)); } else if (Months) { // Months + days sprintf_s(Val, sizeof(Val), "%c%im %id", DirIndcator, abs(Months), abs(Days)); } else if (Days) { if (abs(Days) >= 7) { // Weeks + days... sprintf_s(Val, sizeof(Val), "%c%iw %id", DirIndcator, abs(Days)/7, abs(Days)%7); } else { // Days + hours... sprintf_s(Val, sizeof(Val), "%c%id %ih", DirIndcator, abs(Days), abs(Hrs)); } } else if (Hrs) { // Hours + min sprintf_s(Val, sizeof(Val), "%c%ih %im", DirIndcator, abs(Hrs), abs(Mins)); } else { // Mins sprintf_s(Val, sizeof(Val), "%c%im", DirIndcator, abs(Mins)); } if (Yrs != 0 || Months != 0) { sprintf_s(s, sizeof(s), "%s", Val); return s; } auto NowDay = (n.Get() + tzSeconds) / DAY_1; auto ThenDay = (t.Get() + tzSeconds) / DAY_1; auto DaysDiff = (int64_t)ThenDay - (int64_t)NowDay; int Ch = 0; if (NowDay == ThenDay) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TODAY)); else if (DaysDiff == -1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_YESTERDAY)); else if (DaysDiff == 1) Ch = sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_TOMORROW)); else if (DaysDiff > 1 && DaysDiff < 7) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_THIS_WEEK), LLoadString(Id[Then.DayOfWeek()])); else if (DaysDiff >= 7 && DaysDiff < 14) Ch = sprintf_s(s, sizeof(s), LLoadString(IDS_NEXT_WEEK), LLoadString(Id[Then.DayOfWeek()])); else { sprintf_s(s, sizeof(s), "%s", Val); return s; } sprintf_s(s+Ch, sizeof(s)-Ch, ", %s", Val); return s; } int CalSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } CalendarSourceGetEvents *Calendar::GetEvents = NULL; void Calendar::SummaryOfToday(ScribeWnd *App, std::function Callback) { LDateTime Now; Now.SetNow(); LDateTime Next = Now; Next.AddMonths(1); LArray Sources; if (!App || !Callback || !App->GetCalendarSources(Sources)) return; if (GetEvents) return; new CalendarSourceGetEvents( App, &GetEvents, Now, Next, Sources, [Callback](auto e) { if (!e.Length()) { char s[256]; sprintf_s(s, sizeof(s), "%s", LLoadString(IDS_NO_EVENTS)); Callback(s); } else { e.Sort(CalSorter); LStringPipe p; p.Print("\n"); for (unsigned n=0; nGetField(FIELD_CAL_SUBJECT, Subject); LColour Base32 = c->GetColour(); auto Edge = Base32.Mix(LColour(L_WORKSPACE), 0.85f); sprintf_s(Back, sizeof(Back), "#%2.2x%2.2x%2.2x", Edge.r(), Edge.g(), Edge.b()); sprintf_s(Fore, sizeof(Fore), "#%2.2x%2.2x%2.2x", Base32.r(), Base32.g(), Base32.b()); p.Print("\t
\n" "\t\t%s\n", Fore, Back, Fore, Fore, (Thing*)c, Subject); char Str[256]; const char *Rel = RelativeTime(tp.s); if (Rel) { p.Print("\t\t
%s\n", Rel); } tp.s.Get(Str, sizeof(Str)); p.Print("\t\t
%s\n", Str); tp.e.Get(Str, sizeof(Str)); p.Print("\t\t-
%s\n", Str); } p.Print("
\n"); Callback(p.NewLStr()); } }); } void Calendar::OnSerialize(bool Write) { // Update reminder list.. Reminders.Delete(this); const char *Rem = NULL; if (GetField(FIELD_CAL_REMINDERS, Rem) && ValidStr(Rem)) { Reminders.Insert(this); } SetImage(GetCalType() == CalTodo ? ICON_TODO : ICON_CALENDAR); if (Write && TodoView) { TodoView->Update(); TodoView->Resort(); } } LArray Calendar::GetAttachments() { LArray attachments; if (GetObject()) { auto iter = GetObject()->GetList(FIELD_CAL_ATTACHMENTS); for (auto i = iter->First(); i; i = iter->Next()) { auto data = dynamic_cast(i); if (data) attachments.Add(data); else LAssert(!"Wrong object type."); } } return attachments; } LDataI *Calendar::ImportAttachment(LString Path) { if (!GetObject()) { LgiTrace("%s:%i - No object.\n", _FL); return NULL; } auto lst = GetObject()->GetList(FIELD_CAL_ATTACHMENTS); if (!lst) { LgiTrace("%s:%i - No FIELD_CAL_ATTACHMENTS.\n", _FL); return NULL; } LFile f(Path, O_READ); if (!f) { LgiTrace("%s:%i - Failed to open '%s' for reading.\n", _FL, Path.Get()); return NULL; } auto store = GetObject()->GetStore(); auto attachment = store->Create(MAGIC_CALENDAR_FILE); if (!attachment) { LgiTrace("%s:%i - Store didn't create a MAGIC_CALENDAR_FILE.\n", _FL); return NULL; } attachment->SetStr(FIELD_NAME, LGetLeaf(Path)); attachment->SetStr(FIELD_MIME_TYPE, LGetFileMimeType(Path)); auto now = LDateTime::Now(); attachment->SetDate(FIELD_DATE_MODIFIED, &now); attachment->SetStr(FIELD_ATTACHMENTS_DATA, f.Read()); auto status = attachment->Save(GetObject()); if (status > Store3Error) { // Check attachment is in list... if (lst->IndexOf(attachment) < 0) { LAssert(!"Save needs to add object to list..."); LgiTrace("%s:%i - save didn't add file to list.\n", _FL); } } else DeleteObj(attachment); return attachment; } bool Calendar::DeleteAttachment(LDataI *attachment) { if (!attachment) { LgiTrace("%s:%i - No object.\n", _FL); return false; } auto result = attachment->Delete(false); return result > Store3Error; } ////////////////////////////////////////////////////////////////////////////// void TimePeriod::Set(CalendarSource *source, Calendar *cal, LDateTime start, LDateTime end) { src = source; c = cal; s = start; e = end; ToLocal(); } LString TimePeriod::ToString() { LString str; auto subj = c->GetObject()->GetStr(FIELD_CAL_SUBJECT); str.Printf("TimePeriod(%p, %s, %s, %s, %s)", c, subj, src ? src->ToString().Get() : NULL, s.Get().Get(), e.Get().Get()); return str; } ////////////////////////////////////////////////////////////////////////////// Calendar::Calendar(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); SetImage(ICON_CALENDAR); } Calendar::~Calendar() { CalendarView::OnDelete(this); DeleteObj(TodoView); Reminders.Delete(this); } bool Calendar::GetTimes(LDateTime StartLocal, LDateTime EndLocal, LArray &Times) { if (Calendar::DayStart < 0) InitCalendarView(); ssize_t StartLen = Times.Length(); LDateTime StartUtc = StartLocal; LDateTime EndUtc = EndLocal; StartUtc.ToUtc(); EndUtc.ToUtc(); TimePeriod w; w.s = StartUtc; w.e = EndUtc; const char *Subj = NULL; GetField(FIELD_CAL_SUBJECT, Subj); TimePeriod BaseUtc; LArray Periods; if (!GetField(FIELD_CAL_START_UTC, BaseUtc.s)) return false; if (!GetField(FIELD_CAL_END_UTC, BaseUtc.e)) { BaseUtc.e = BaseUtc.s; BaseUtc.e.AddHours(1); } if (BaseUtc.s > EndUtc) return true; - LArray Dst; - LDateTime::GetDaylightSavingsInfo(Dst, BaseUtc.s, &EndUtc); + LArray Dst; + LTimeZone::GetDaylightSavingsInfo(Dst, BaseUtc.s, &EndUtc); if (Dst.Length() < 2) { LgiTrace("%s:%i - GetDaylightSavingsInfo(%s, %s)\n", _FL, BaseUtc.s.Get().Get(), EndUtc.Get().Get()); LAssert(!"Need 2 dst points."); } Periods.Add(BaseUtc); LDateTime BaseS = BaseUtc.s; - LDateTime::DstToLocal(Dst, BaseS); + LTimeZone::DstToLocal(Dst, BaseS); auto BaseTz = BaseS.GetTimeZone(); int AllDay = false; GetField(FIELD_CAL_ALL_DAY, AllDay); // Process recur rules int Recur = 0; if (GetField(FIELD_CAL_RECUR, Recur) && Recur) { LDateTime Diff = BaseUtc.e - BaseUtc.s; int FilterFreq = -1; int FilterInterval = 0; int FilterDay = 0; int FilterMonth = 0; int EndType = 0; int EndCount = 0; const char *FilterYear = NULL; const char *FilterPos = NULL; LDateTime EndDate; GetField(FIELD_CAL_RECUR_FREQ, FilterFreq); GetField(FIELD_CAL_RECUR_INTERVAL, FilterInterval); GetField(FIELD_CAL_RECUR_FILTER_DAYS, FilterDay); GetField(FIELD_CAL_RECUR_FILTER_MONTHS, FilterMonth); GetField(FIELD_CAL_RECUR_FILTER_YEARS, FilterYear); GetField(FIELD_CAL_RECUR_FILTER_POS, FilterPos); GetField(FIELD_CAL_RECUR_END_TYPE, EndType); GetField(FIELD_CAL_RECUR_END_DATE, EndDate); GetField(FIELD_CAL_RECUR_END_COUNT, EndCount); LDateTime CurUtc = BaseUtc.s; const char *Error = NULL; int Count = 0; while (!Error) { LAssert(CurUtc.GetTimeZone() == 0); // Advance the current date by interval * freq switch (FilterFreq) { case CalFreqDays: CurUtc.AddDays(FilterInterval); break; case CalFreqWeeks: CurUtc.AddDays(FilterInterval * 7); break; case CalFreqMonths: CurUtc.AddMonths(FilterInterval); break; case CalFreqYears: CurUtc.Year(CurUtc.Year() + FilterInterval); break; default: Error = "Invalid freq."; break; } LAssert(CurUtc.GetTimeZone() == 0); if (Error || CurUtc > EndUtc) break; // Check against end conditions bool IsEnded = CurUtc > EndUtc; switch (EndType) { case CalEndNever: break; case CalEndOnCount: // count IsEnded = Count >= EndCount - 1; break; case CalEndOnDate: // date IsEnded = CurUtc > EndDate; break; default: // error IsEnded = true; break; } if (IsEnded) break; // Check against filters LAssert(CurUtc.GetTimeZone() == 0); LDateTime CurLocal = CurUtc; - LDateTime::DstToLocal(Dst, CurLocal); + LTimeZone::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; + int Type = CalEvent; + GetField(FIELD_CAL_TYPE, Type); + return (CalendarType)Type; } void Calendar::SetCalType(CalendarType Type) { SetField(FIELD_CAL_TYPE, (int)Type); SetImage(Type == CalTodo ? ICON_TODO : ICON_CALENDAR); } LString Calendar::ToString() { LString s; auto obj = GetObject(); s.Printf("Calendar(%s,%s,%s)", obj ? obj->GetStr(FIELD_CAL_SUBJECT) : "#NoObject", obj ? obj->GetDate(FIELD_CAL_START_UTC)->Get().Get() : NULL, obj ? obj->GetDate(FIELD_CAL_END_UTC)->Get().Get() : NULL); return s; } LColour Calendar::GetColour() { if (GetObject()) { int64 c = GetObject()->GetInt(FIELD_COLOUR); if (c >= 0) return LColour((uint32_t)c, 32); } if (Source) return Source->GetColour(); return LColour(L_LOW); } void Calendar::OnPaintView(LSurface *pDC, LFont *Font, LRect *Pos, TimePeriod *Period) { LRect p = *Pos; const char *Title = "..."; LDateTime Now; float Sx = 1.0; float Sy = 1.0; if (pDC->IsPrint()) { auto DcDpi = pDC->GetDpi(); auto SrcDpi = LScreenDpi(); Sx = (float)DcDpi.x / SrcDpi.x; Sy = (float)DcDpi.y / SrcDpi.y; } float Scale = Sx < Sy ? Sx : Sy; Now.SetNow(); GetField(FIELD_CAL_SUBJECT, Title); bool Delayed = GetObject() ? GetObject()->GetInt(FIELD_STATUS) == Store3Delayed : false; LColour Grey(192, 192, 192); auto View = GetView(); if (View && View->Selection.HasItem(this)) { // selected LColour f = L_FOCUS_SEL_FORE; LColour b = L_FOCUS_SEL_BACK; if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(f, b); } else { LColour Text(0x80, 0x80, 0x80); LColour Ws(L_WORKSPACE); auto Base = GetColour(); auto Qtr = Ws.Mix(Base, 0.1f); auto Half = Ws.Mix(Base, 0.4f); // not selected LColour f, b; if (Period && Now < Period->s) { // future entry (full colour) f = Base; b = Half; } else { // historical entry (half strength colour) f = Half; b = Qtr; } if (Delayed) { f = f.Mix(Grey); b = b.Mix(Grey); } pDC->Colour(f); pDC->Box(&p); p.Inset(1, 1); pDC->Colour(b); Font->Colour(Text, b); } pDC->Rectangle(&p); p.Inset((int)SX(1), (int)SY(1)); Font->Transparent(false); LDisplayString ds(Font, Title); float Ht = p.Y() > 0 ? (float)ds.Y() / p.Y() : 1.0f; if (Ht < 0.75f) p.Inset((int)SX(3), (int)SY(3)); else if (Ht < 0.95f) p.Inset((int)SX(1), (int)SY(1)); ds.Draw(pDC, p.x1, p.y1, &p); ViewPos.Union(Pos); } Thing &Calendar::operator =(Thing &Obj) { if (Obj.GetObject() && GetObject()) { GetObject()->CopyProps(*Obj.GetObject()); } return *this; } bool Calendar::operator ==(Thing &t) { Calendar *c = t.IsCalendar(); if (!c) return false; { LDateTime a, b; if (GetField(FIELD_CAL_START_UTC, a) && c->GetField(FIELD_CAL_START_UTC, b) && a != b) return false; if (GetField(FIELD_CAL_END_UTC, a) && c->GetField(FIELD_CAL_END_UTC, b) && a != b) return false; } { const char *a, *b; if (GetField(FIELD_CAL_SUBJECT, a) && c->GetField(FIELD_CAL_SUBJECT, b) && Stricmp(a, b)) return false; if (GetField(FIELD_CAL_LOCATION, a) && c->GetField(FIELD_CAL_LOCATION, b) && Stricmp(a, b)) return false; } return true; } int Calendar::Compare(LListItem *Arg, ssize_t FieldId) { Calendar *c1 = this; Calendar *c2 = dynamic_cast(Arg); if (c1 && c2) { switch (FieldId) { case FIELD_CAL_START_UTC: case FIELD_CAL_END_UTC: { LDateTime d1, d2; if (!c1->GetField((int)FieldId, d1)) d1.SetNow(); if (!c2->GetField((int)FieldId, d2)) d2.SetNow(); return d1.Compare(&d2); break; } case FIELD_CAL_SUBJECT: case FIELD_CAL_LOCATION: { const char *s1 = "", *s2 = ""; if (c1->GetField((int)FieldId, s1) && c2->GetField((int)FieldId, s2)) { return _stricmp(s1, s2); } break; } } } return 0; } uint32_t Calendar::GetFlags() { return 0; } ThingUi *Calendar::DoUI(MailContainer *c) { if (!Ui) { Ui = new CalendarUi(this); } return Ui; } ThingUi *Calendar::GetUI() { return Ui; } bool Calendar::SetUI(ThingUi *ui) { if (Ui) Ui->Item = NULL; Ui = dynamic_cast(ui); if (Ui) Ui->Item = this; return true; } void Calendar::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { auto View = GetView(); DoContextMenu(m, View ? (LView*)View : (LView*)App); } else if (m.Down() && m.Left() && m.Double()) { DoUI(); } } void Calendar::OnCreate() { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } bool Calendar::OnDelete() { bool IsTodo = GetCalType() == CalTodo; bool Status = Thing::OnDelete(); if (IsTodo) DeleteObj(TodoView); for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); return Status; } bool Calendar::GetParentSelection(LList *Lst, List &s) { if (Lst) { return Lst->GetSelection(s); } if (auto View = GetView()) { LArray &a = View->GetSelection(); for (auto cal: a) { auto str = cal->ToString(); s.Insert(cal); } return true; } return false; } #define IDM_MOVE_TO 4000 void Calendar::DoContextMenu(LMouse &m, LView *Parent) { LSubMenu Sub; LScriptUi s(&Sub); s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_INSPECT), IDM_INSPECT); auto Cv = dynamic_cast(Parent); if (Cv) { s.Sub->AppendSeparator(); for (unsigned n = 0; nGetName() && Cs->IsWritable()) { auto msg = LString::Fmt("Move to '%s'\n", Cs->GetName()); s.Sub->AppendItem(msg, IDM_MOVE_TO + n, Source != Cs); } } } LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto cb: Callbacks) App->ExecuteScriptCallback(*cb, Args); Args.DeleteObjects(); } int Result = s.Sub->Float(Parent, m); switch (Result) { default: { auto Idx = Result - IDM_MOVE_TO; if (Cv && Idx >= 0 && Idx < (int)CalendarSource::GetSources().Length()) { auto Dst = CalendarSource::GetSources().ItemAt(Idx); if (!Dst) { LAssert(!"No dst?"); return; } auto Fsrc = dynamic_cast(Dst); if (!Fsrc) { LAssert(!"No cal src?"); break; } auto Path = Fsrc->GetPath(); auto DstFolder = App->GetFolder(Path); if (!DstFolder) { LAssert(!"Path doesn't exist?"); return; } LArray Items{ this }; DstFolder->MoveTo( Items, false, [this, Cv, Dst](auto result, auto itemStatus) { if (result) { Source = Dst; Cv->Invalidate(); } } ); return; } // Handle any installed callbacks for menu items for (auto &cb: s.Callbacks) { if (cb.Param == Result) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(cb.Param); App->ExecuteScriptCallback(cb, Args); Args.DeleteObjects(); } } break; } case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(Parent, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; auto PList = dynamic_cast(Parent); if (GetParentSelection(PList ? PList : GetList(), Del)) { for (auto i: Del) { if (auto t = dynamic_cast(i)) { t->OnDelete(); } else if (auto Todo = dynamic_cast(i)) { if (auto c = Todo->GetTodo()) c->OnDelete(); } } } else { OnDelete(); } } break; } case IDM_EXPORT: { ExportAll(GetList(), sMimeVCalendar, NULL); break; } case IDM_INSPECT: { new ObjectInspector(App, this); break; } } } const char *Calendar::GetText(int i) { static char s[64]; if (!GetObject()) { LAssert(!"No storage object"); return 0; } int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int) FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultCalenderFields)) { Field = DefaultCalenderFields[i]; } if (!Field) return 0; ItemFieldDef *Def = GetFieldDefById(Field); if (!Def) { LAssert(!"Where is the field def?"); return 0; } switch (Def->Type) { case GV_STRING: { return GetObject()->GetStr(Field); break; } case GV_INT32: { sprintf_s(s, sizeof(s), LPrintfInt64, GetObject()->GetInt(Field)); return s; break; } case GV_DATETIME: { // Get any effective timezone for this event. LString Tz = GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto dt = GetObject()->GetDate(Field); if (dt && dt->IsValid()) { LDateTime tmp = *dt; LOG_DEBUG("%s:%i - GetText.UTC %i = %s\n", _FL, i, tmp.Get().Get()); bool UseLocal = true; tmp.SetTimeZone(0, false); - if (Tz.Get()) + if (Tz) { bool HasPt = false, HasDigit = false; - char *e = Tz.Get(); - while (strchr(" \t\r\n-.+", *e) || IsDigit(*e)) + auto Parts = Tz.SplitDelimit(",", 1); + auto First = Parts[0]; + char *e = First.Get(); + + while (e && (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 TzHrs = First.Float(); double i, f = modf(TzHrs, &i); int Mins = (int) ((i * 60) + (f * 60)); tmp.AddMinutes(Mins); } else { - int64 i = Tz.Int(); + int64 i = First.Int(); int a = (int)ABS(i); int Mins = (int) (((a / 100) * 60) + (a % 100)); tmp.AddMinutes(i < 0 ? -Mins : Mins); } UseLocal = false; } } if (UseLocal) tmp.ToLocal(); LOG_DEBUG("%s:%i - GetText.Local %i = %s\n", _FL, i, tmp.Get().Get()); tmp.Get(s, sizeof(s)); return s; } break; } default: { LAssert(0); break; } } return 0; } int *Calendar::GetDefaultFields() { return DefaultCalenderFields; } const char *Calendar::GetFieldText(int Field) { return 0; } bool Calendar::Overlap(Calendar *c) { if (c) { LDateTime Ts, Te, Cs, Ce; if (GetField(FIELD_CAL_START_UTC, Ts) && c->GetField(FIELD_CAL_START_UTC, Cs)) { if (!GetField(FIELD_CAL_END_UTC, Te)) { Te = Ts; Te.AddHours(1); } if (!c->GetField(FIELD_CAL_END_UTC, Ce)) { Ce = Cs; Ce.AddHours(1); } if ((Ce <= Ts) || (Cs >= Te)) { return false; } return true; } } return false; } bool Calendar::Save(ScribeFolder *Folder) { bool Status = false; // Check the dates are the right way around LDateTime Start, End; if (GetField(FIELD_CAL_START_UTC, Start) && GetField(FIELD_CAL_END_UTC, End)) { if (End < Start) { SetField(FIELD_CAL_START_UTC, End); SetField(FIELD_CAL_END_UTC, Start); } } auto ChangeEvent = [this](bool Status) { auto View = GetView(); if (View && Status) { View->OnContentsChanged(Source); OnSerialize(true); } }; if (!Folder) Folder = GetFolder(); if (!Folder && App) Folder = App->GetFolder(FOLDER_CALENDAR); if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Webdav) { auto ParentObj = Folder ? Folder->GetObject() : NULL; Store3Status s = GetObject()->Save(ParentObj); Status = s > Store3Error; if (Status) SetDirty(false); ChangeEvent(Status); } else { // FIXME: This can't wait for WriteThing to finish it's call back... Status = true; if (Folder) { LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow().ToUtc()); Folder->WriteThing(this, [this, ChangeEvent](auto Status) { if (Status > Store3Error) SetDirty(false); ChangeEvent(Status); }); } else ChangeEvent(Status); } return Status; } // Import/Export bool Calendar::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeVCalendar); return MimeTypes.Length() > 0; } Thing::IoProgress Calendar::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar) && Stricmp(mimeType, sMimeICalendar)) { ErrMsg.Printf("Unknown mimetype '%s'", mimeType); IoProgressNotImpl(); } VCal vCal; if (!vCal.Import(GetObject(), stream)) { ErrMsg.Printf("vCal import failed"); IoProgressError(ErrMsg); } IoProgressSuccess(); } Thing::IoProgress Calendar::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCalendar)) IoProgressNotImpl(); VCal vCal; if (!vCal.Export(GetObject(), stream)) IoProgressError("vCal export failed."); IoProgressSuccess(); } char *Calendar::GetDropFileName() { if (!DropFileName) { const char *Name = 0; GetField(FIELD_CAL_SUBJECT, Name); DropFileName.Reset(MakeFileName(Name ? Name : "Cal", "ics")); } return DropFileName; } bool Calendar::GetDropFiles(LString::Array &Files) { bool Status = false; if (GetDropFileName()) { if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); Export(AutoCast(F), sMimeVCalendar); } } if (LFileExists(DropFileName)) { Files.Add(DropFileName.Get()); Status = true; } } return Status; } void Calendar::OnPrintHeaders(ScribePrintContext &Context) { LDisplayString *ds = Context.Text(LLoadString(IDS_CAL_EVENT)); LRect &r = Context.MarginPx; int Line = ds->Y(); LDrawListSurface *Page = Context.Pages.Last(); Page->Rectangle(r.x1, Context.CurrentY + (Line * 5 / 10), r.x2, Context.CurrentY + (Line * 6 / 10)); Context.CurrentY += Line; } void Calendar::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Print document for (ItemFieldDef *Fld = CalendarFields; Fld->FieldId; Fld++) { LString Value; switch (Fld->Type) { case GV_STRING: { Value = GetObject()->GetStr(Fld->FieldId); break; } case GV_DATETIME: { auto Dt = GetObject()->GetDate(Fld->FieldId); if (Dt) { char s[64] = ""; Dt->Get(s, sizeof(s)); Value = s; } break; } default: break; } if (Value) { LString f; const char *Name = LLoadString(Fld->FieldId); f.Printf("%s: %s", Name ? Name : Fld->DisplayText, Value.Get()); // LDisplayString *ds = Context.Text(f); } } } bool Calendar::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: // Type: String Value = GetObject()->GetStr(FIELD_CAL_SUBJECT); break; case SdTo: // Type: String Value = GetObject()->GetStr(FIELD_TO); break; case SdLocation: // Type: String Value = GetObject()->GetStr(FIELD_CAL_LOCATION); break; case SdUid: // Type: String Value = GetObject()->GetStr(FIELD_UID); break; case SdReminders: // Type: String Value = GetObject()->GetStr(FIELD_CAL_REMINDERS); break; case SdNotes: // Type: String Value = GetObject()->GetStr(FIELD_CAL_NOTES); break; case SdStatus: // Type: String Value = GetObject()->GetStr(FIELD_CAL_STATUS); break; // Int variables case SdType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_TYPE); break; case SdCompleted: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_COMPLETED); break; case SdShowTimeAs: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_SHOW_TIME_AS); break; case SdRecur: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR); break; case SdRecurFreq: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FREQ); break; case SdRecurInterval: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_INTERVAL); break; case SdRecurEndCount: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_COUNT); break; case SdRecurEndType: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_END_TYPE); break; case SdRecurFilterDays: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); break; case SdRecurFilterMonths: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_RECUR_FILTER_MONTHS); break; case SdPrivacy: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_PRIVACY); break; case SdAllDay: // Type: Int32 Value = GetObject()->GetInt(FIELD_CAL_ALL_DAY); break; case SdColour: // Type: Int64 Value = GetObject()->GetInt(FIELD_COLOUR); break; // Date time fields case SdStart: // Type: DateTime return GetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: // Type: DateTime return GetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: // Type: DateTime return GetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { // String variables case SdSubject: Value = GetObject()->SetStr(FIELD_CAL_SUBJECT, Value.Str()); break; case SdTo: Value = GetObject()->SetStr(FIELD_TO, Value.Str()); break; case SdLocation: Value = GetObject()->SetStr(FIELD_CAL_LOCATION, Value.Str()); break; case SdUid: Value = GetObject()->SetStr(FIELD_UID, Value.Str()); break; case SdReminders: Value = GetObject()->SetStr(FIELD_CAL_REMINDERS, Value.Str()); break; case SdNotes: Value = GetObject()->SetStr(FIELD_CAL_NOTES, Value.Str()); break; case SdStatus: Value = GetObject()->SetStr(FIELD_CAL_STATUS, Value.Str()); break; // Int variables case SdType: Value = GetObject()->SetInt(FIELD_CAL_TYPE, Value.CastInt32()); break; case SdCompleted: Value = GetObject()->SetInt(FIELD_CAL_COMPLETED, Value.CastInt32()); break; case SdShowTimeAs: Value = GetObject()->SetInt(FIELD_CAL_SHOW_TIME_AS, Value.CastInt32()); break; case SdRecur: Value = GetObject()->SetInt(FIELD_CAL_RECUR, Value.CastInt32()); break; case SdRecurFreq: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FREQ, Value.CastInt32()); break; case SdRecurInterval: Value = GetObject()->SetInt(FIELD_CAL_RECUR_INTERVAL, Value.CastInt32()); break; case SdRecurEndCount: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_COUNT, Value.CastInt32()); break; case SdRecurEndType: Value = GetObject()->SetInt(FIELD_CAL_RECUR_END_TYPE, Value.CastInt32()); break; case SdRecurFilterDays: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, Value.CastInt32()); break; case SdRecurFilterMonths: Value = GetObject()->SetInt(FIELD_CAL_RECUR_FILTER_MONTHS, Value.CastInt32()); break; case SdPrivacy: Value = GetObject()->SetInt(FIELD_CAL_PRIVACY, Value.CastInt32()); break; case SdColour: Value = GetObject()->SetInt(FIELD_COLOUR, Value.CastInt32()); break; case SdAllDay: Value = GetObject()->SetInt(FIELD_CAL_ALL_DAY, Value.CastInt32()); break; // Date time fields case SdStart: return SetDateField(FIELD_CAL_START_UTC, Value); case SdEnd: return SetDateField(FIELD_CAL_END_UTC, Value); case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Calendar::CallMethod(const char *MethodName, LScriptArguments &Args) { return Thing::CallMethod(MethodName, Args); } ////////////////////////////////////////////////////////////////////////////// bool SerializeUi(ItemFieldDef *Defs, LDataI *Object, LViewI *View, bool ToUi) { if (!Object || !Defs || !View) return false; if (ToUi) { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { auto s = Object->GetStr(d->FieldId); View->SetCtrlName(d->CtrlId, s); break; } case GV_INT32: { int64 i = Object->GetInt(d->FieldId); View->SetCtrlValue(d->CtrlId, i); break; } case GV_DATETIME: { char s[64] = ""; auto dt = Object->GetDate(d->FieldId); if (dt && dt->Year()) dt->Get(s, sizeof(s)); View->SetCtrlName(d->CtrlId, s); break; } default: { LAssert(0); break; } } } } else { for (ItemFieldDef *d = Defs; d->FieldId; d++) { if (d->CtrlId <= 0) continue; switch (d->Type) { case GV_STRING: { const char *s = View->GetCtrlName(d->CtrlId); Object->SetStr(d->FieldId, s); break; } case GV_INT32: { int i = (int)View->GetCtrlValue(d->CtrlId); Object->SetInt(d->FieldId, i); break; } case GV_DATETIME: { const char *s = View->GetCtrlName(d->CtrlId); LDateTime dt; dt.Set(s); Object->SetDate(d->FieldId, &dt); break; } default: { LAssert(0); break; } } } } return true; } ////////////////////////////////////////////////////////////////////////////// class LEditDropDown : public LEdit { public: enum DropType { DropNone, DropDate, DropTime, }; protected: DropType Type; LAutoPtr Popup; public: LEditDropDown(int id) : LEdit(id, 0, 0, 60, 20, NULL) { Type = DropNone; SetObjectName(Res_Custom); } void SetType(DropType type) { Type = type; } const char *GetClass() { return "LEditDropDown"; } void OnMouseClick(LMouse &m) { LEdit::OnMouseClick(m); if (Focus() && Popup && !Popup->Visible()) { Popup->Visible(true); } } void OnFocus(bool f) { if (f) { if (!Popup) { if (Type == DropDate) Popup.Reset(new LDatePopup(this)); else if (Type == DropTime) Popup.Reset(new LTimePopup(this)); if (Popup) { Popup->TakeFocus(false); LPoint p(0, Y()); PointToScreen(p); LRect r = Popup->GetPos(); r.Offset(p.x - r.x1, p.y - r.y1); Popup->SetPos(r); } } if (Popup) Popup->Visible(true); } } int OnNotify(LViewI *Wnd, LNotification n) { /* LgiTrace("OnNotify %s, %i\n", Wnd->GetClass(), Flags); if (Wnd == (LViewI*)Popup) { GDatePopup *Date; if ((Date = dynamic_cast(Popup.Get()))) { LDateTime Ts = Date->Get(); char s[256]; Ts.GetDate(s, sizeof(s)); Name(s); } else LgiTrace("%s:%i - Incorrect pop up type.\n", _FL); } */ return 0; } void OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)Popup.Get() && !Attaching) { // This gets called in the destructor of the Popup, so we should // lose the pointer to it. Popup.Release(); } } }; struct LEditDropDownFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "LEditDropDown")) { return new LEditDropDown(-1); } return NULL; } } EditDropDownFactory; ////////////////////////////////////////////////////////////////////////////// class LRecurDlg : public LDialog { CalendarUi *Ui = NULL; LEditDropDown *EndOnDate = NULL; LCombo *Repeats = NULL; bool AcceptNotify = true; public: LRecurDlg(CalendarUi *ui) { Ui = ui; SetParent(ui); if (LoadFromResource(IDD_CAL_RECUR)) { if (GetViewById(IDC_ON_DATE, EndOnDate)) { EndOnDate->SetType(LEditDropDown::DropDate); LDateTime dt; dt.SetNow(); char s[64]; dt.GetDate(s, sizeof(s)); EndOnDate->Name(s); } if (GetViewById(IDC_REPEATS, Repeats)) { Repeats->Insert(LLoadString(IDS_DAY)); Repeats->Insert(LLoadString(IDS_WEEK)); Repeats->Insert(LLoadString(IDS_MONTH)); Repeats->Insert(LLoadString(IDS_YEAR)); Repeats->Value(1); } Radio(IDC_NEVER); SetCtrlValue(IDC_EVERY, 1); Serialize(false); } } ~LRecurDlg() { } void Serialize(bool Write) { int DayCtrls[] = { IDC_SUNDAY, IDC_MONDAY, IDC_TUESDAY, IDC_WEDNESDAY, IDC_THURSDAY, IDC_FRIDAY, IDC_SATURDAY }; Calendar *c = Ui->GetCal(); LDataI *o = c->GetObject(); if (Write) { // Dlg -> Object int64 v = Repeats->Value(); o->SetInt(FIELD_CAL_RECUR_FREQ, v); v = GetCtrlValue(IDC_EVERY); o->SetInt(FIELD_CAL_RECUR_INTERVAL, v); int DayFilter = 0; for (int i=0; i<7; i++) { if (GetCtrlValue(DayCtrls[i])) DayFilter |= 1 << i; } o->SetInt(FIELD_CAL_RECUR_FILTER_DAYS, DayFilter); if (GetCtrlValue(IDC_NEVER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndNever); } else if (GetCtrlValue(IDC_AFTER)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnCount); o->SetInt(FIELD_CAL_RECUR_END_COUNT, GetCtrlValue(IDC_AFTER_COUNT)); } else if (GetCtrlValue(IDC_ON)) { o->SetInt(FIELD_CAL_RECUR_END_TYPE, CalEndOnDate); LDateTime e; e.SetDate(GetCtrlName(IDC_ON_DATE)); e.SetTime("11:59:59"); o->SetDate(FIELD_CAL_RECUR_END_DATE, &e); } else LAssert(0); } else { // Object -> Dlg int64 v = o->GetInt(FIELD_CAL_RECUR_FREQ); Repeats->Value(v); OnRepeat(v); v = o->GetInt(FIELD_CAL_RECUR_INTERVAL); SetCtrlValue(IDC_EVERY, MAX(v, 1)); int DayFilter = (int)o->GetInt(FIELD_CAL_RECUR_FILTER_DAYS); for (int i=0; i<7; i++) { SetCtrlValue(DayCtrls[i], (DayFilter & (1 << i)) ? 1 : 0); } CalRecurEndType EndType = (CalRecurEndType) o->GetInt(FIELD_CAL_RECUR_END_TYPE); if (EndType == CalEndOnCount) { Radio(IDC_AFTER); SetCtrlValue(IDC_AFTER_COUNT, o->GetInt(FIELD_CAL_RECUR_END_COUNT)); } else if (EndType == CalEndOnDate) { Radio(IDC_ON); auto e = o->GetDate(FIELD_CAL_RECUR_END_DATE); if (e) SetCtrlName(IDC_ON_DATE, e->GetDate()); } else // Default to never... { Radio(IDC_NEVER); } } } void Radio(int Id) { AcceptNotify = false; SetCtrlValue(IDC_NEVER, Id == IDC_NEVER); SetCtrlValue(IDC_AFTER, Id == IDC_AFTER); SetCtrlEnabled(IDC_AFTER_COUNT, Id == IDC_AFTER); SetCtrlEnabled(IDC_OCCURRENCES, Id == IDC_AFTER); SetCtrlValue(IDC_ON, Id == IDC_ON); SetCtrlEnabled(IDC_ON_DATE, Id == IDC_ON); AcceptNotify = true; } void OnRepeat(int64 Val) { LViewI *v; if (!GetViewById(IDC_TIME_TYPE, v)) return; switch (Val) { case 0: v->Name(LLoadString(IDS_DAYS)); break; case 1: v->Name(LLoadString(IDS_WEEKS)); break; case 2: v->Name(LLoadString(IDS_MONTHS)); break; case 3: v->Name(LLoadString(IDS_YEARS)); break; } v->SendNotify(LNotifyTableLayoutRefresh); } int OnNotify(LViewI *Ctrl, LNotification n) { if (!AcceptNotify) return 0; switch (Ctrl->GetId()) { case IDC_NEVER: case IDC_AFTER: case IDC_ON: Radio(Ctrl->GetId()); break; case IDC_REPEATS: OnRepeat(Ctrl->Value()); break; case IDOK: Serialize(true); // Fall through case IDCANCEL: EndModal(Ctrl->GetId() == IDOK); break; } return 0; } }; /////////////////////////////////////////////////////////////////////////////// struct CalendarUiPriv { CalendarUi *Ui; LEditDropDown *StartDate, *StartTime; LEditDropDown *EndDate, *EndTime; LEdit *Entry; AddressBrowse *Browse; LList *Guests; LList *Reminders; LCombo *ReminderType; LCombo *ReminderUnit; LCombo *CalSelect; LColourSelect *Colour; CalendarUiPriv(CalendarUi *ui) { Ui = ui; StartDate = StartTime = NULL; EndDate = EndTime = NULL; CalSelect = NULL; Entry = NULL; Browse = NULL; Guests = NULL; Colour = NULL; Reminders = NULL; ReminderType = NULL; ReminderUnit = NULL; } void OnGuest() { const char *g = Ui->GetCtrlName(IDC_GUEST_ENTRY); if (!g) return; Mailto mt(Ui->App, g); for (auto a: mt.To) { ListAddr *la = new ListAddr(Ui->App, a); if (la) { Guests->Insert(la); } } Ui->SetCtrlName(IDC_GUEST_ENTRY, ""); } }; ////////////////////////////////////////////////////////////////////////////// class CalendarAttachmentItem : public LListItem { CalendarUi *Ui = NULL; LDataI *Obj = NULL; LString Cache; public: enum Columns { CFileName, CMimeType, CDateMod, CSize }; CalendarAttachmentItem(CalendarUi *ui, LDataI *obj) : Ui(ui), Obj(obj) { } LDataI *GetObject() { return Obj; } bool IsRemote() { return Obj->GetStr(FIELD_URI) != NULL; } void SaveAs() { auto sel = new LFileSelect(GetList()); sel->Name(Obj->GetStr(FIELD_NAME)); sel->Save([this, sel](auto dlg, auto ok) { if (ok) Save(sel->Name()); delete dlg; }); } bool Save(LString path) { auto in = Obj->GetStream(_FL); if (!in) return false; auto inSize = in->GetSize(); LFile out(path, O_WRITE); if (!out) return false; LCopyStreamer cp; ssize_t copied = cp.Copy(in, &out); return copied == inSize; } void Open() { if (IsRemote()) { auto uri = Obj->GetStr(FIELD_URI); LExecute(uri); } else // Local: save and open { auto file = Obj->GetStr(FIELD_NAME); LFile::Path p(ScribeTempPath(), LGetLeaf(file)); if (Save(p.GetFull())) { LExecute(p.GetFull()); } } } void Delete() { Ui->PostEvent(M_DELETE_ATTACHMENT, (LMessage::Param)this); } void OnMouseClick(LMouse &m) override { if (m.IsContextMenu()) { LSubMenu sub; sub.AppendItem("Save As", IDM_SAVEAS, !IsRemote()); sub.AppendItem("Open Uri", IDM_OPEN, IsRemote()); sub.AppendSeparator(); sub.AppendItem("Delete", IDM_DELETE); switch (sub.Float(GetList(), m)) { case IDM_SAVEAS: SaveAs(); break; case IDM_OPEN: Open(); break; case IDM_DELETE: Delete(); break; } } else if (m.Left()) { if (m.Down() && m.Double()) Open(); } } bool OnKey(LKey &k) override { switch (k.vkey) { case LK_DELETE: if (k.Down()) Delete(); return true; default: break; } return false; } const char *GetText(int Col = 0) { switch (Col) { case CFileName: return Obj->GetStr(FIELD_NAME); case CMimeType: return Obj->GetStr(FIELD_MIME_TYPE); case CDateMod: { auto mod = Obj->GetDate(FIELD_DATE_MODIFIED); if (mod) Cache = mod->Get(); return Cache; } case CSize: return Obj->GetStr(FIELD_SIZE); } return NULL; } }; ////////////////////////////////////////////////////////////////////////////// CalendarUi::CalendarUi(Calendar *item) : ThingUi(item, LLoadString(IDS_CAL_EVENT)) { Item = item; d = new CalendarUiPriv(this); #if WINNATIVE CreateClassW32("Event", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_EVENT))); #endif if (!Attach(NULL)) LAssert(0); else { LoadFromResource(IDD_CAL, this); AttachChildren(); if (!SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, true)) { LRect p(100, 100, 900, 700); SetPos(p); MoveToCenter(); } if (GetViewById(IDC_START_DATE, d->StartDate)) { d->StartDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_START_TIME, d->StartTime)) { d->StartTime->SetType(LEditDropDown::DropTime); } if (GetViewById(IDC_END_DATE, d->EndDate)) { d->EndDate->SetType(LEditDropDown::DropDate); } if (GetViewById(IDC_END_TIME, d->EndTime)) { d->EndTime->SetType(LEditDropDown::DropTime); } GetViewById(IDC_GUEST_ENTRY, d->Entry); if (GetViewById(IDC_GUESTS, d->Guests)) { d->Guests->AddColumn(LLoadString(IDS_ADDRESS), 120); d->Guests->AddColumn(LLoadString(IDS_NAME), 120); d->Guests->ColumnHeaders(false); } if (GetViewById(IDC_REMINDERS, d->Reminders)) { d->Reminders->AddColumn(LLoadString(FIELD_CAL_REMINDER_TIME), 200); d->Reminders->AddColumn(LLoadString(IDC_DELETE), 20); d->Reminders->ColumnHeaders(false); } if (GetViewById(IDC_REMINDER_TYPE, d->ReminderType)) { d->ReminderType->Insert(LLoadString(IDS_EMAIL)); d->ReminderType->Insert(LLoadString(IDS_POPUP)); d->ReminderType->Insert("Script"); d->ReminderType->Value(1); } SetCtrlValue(IDC_REMINDER_VALUE, 10); if (GetViewById(IDC_REMINDER_UNIT, d->ReminderUnit)) { d->ReminderUnit->Insert(LLoadString(IDS_MINUTES)); d->ReminderUnit->Insert(LLoadString(IDS_HOURS)); d->ReminderUnit->Insert(LLoadString(IDS_DAYS)); d->ReminderUnit->Insert(LLoadString(IDS_WEEKS)); d->ReminderUnit->Value(0); } SetCtrlValue(IDC_AVAILABLE_TYPE, 1); if (GetViewById(IDC_COLOUR, d->Colour)) { LArray Colours; Colours.Add(LColour(84, 132, 237, 255)); Colours.Add(LColour(164, 189, 252, 255)); Colours.Add(LColour(70, 214, 219, 255)); Colours.Add(LColour(122, 231, 191, 255)); Colours.Add(LColour(81, 183, 73, 255)); Colours.Add(LColour(251, 215, 91, 255)); Colours.Add(LColour(255, 184, 120, 255)); Colours.Add(LColour(255, 136, 124, 255)); Colours.Add(LColour(220, 33, 39, 255)); Colours.Add(LColour(219, 173, 255, 255)); Colours.Add(LColour(225, 225, 225, 255)); d->Colour->SetColourList(&Colours); d->Colour->Value(0); } if (GetViewById(IDC_CALENDAR, d->CalSelect)) { LArray Sources; if (Item->App->GetCalendarSources(Sources)) { for (unsigned i=0; iCalSelect->Insert(cs->GetName()); } } } LView *s; if (GetViewById(IDC_SUBJECT, s)) s->Focus(true); LButton *btn; if (GetViewById(IDC_SAVE, btn)) btn->Default(true); OnLoad(); Visible(true); NotifyOn = true; } LViewI *v; if (GetViewById(IDC_REMINDER_TYPE, v)) { LNotification note; OnNotify(v, note); } RegisterHook(this, LKeyEvents); } CalendarUi::~CalendarUi() { if (Item) { if (Item->App) SerializeState(Item->App->GetOptions(), OPT_CalendarEventPos, false); Item->SetUI(NULL); } } bool CalendarUi::OnViewKey(LView *v, LKey &k) { THREAD_UNSAFE(false); if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } if (k.vkey == LK_RETURN) { if (!k.Down()) { OnSave(); Quit(); } return true; } } return false; } int CalendarUi::OnCommand(int Cmd, int Event, OsView Window) { THREAD_UNSAFE(0); return 0; } void CalendarUi::OnPosChange() { THREAD_UNSAFE(); LWindow::OnPosChange(); if (FirstLayout && NotifyOn) { FirstLayout = false; LList *attachLst; if (GetViewById(IDC_ATTACHMENTS, attachLst)) attachLst->ResizeColumnsToContent(); } } LMessage::Result CalendarUi::OnEvent(LMessage *Msg) { THREAD_UNSAFE(0); switch (Msg->Msg()) { case M_DELETE_ATTACHMENT: { auto item = (CalendarAttachmentItem*)Msg->A(); if (!item) break; auto lst = item->GetList(); lst->Remove(item); Item->DeleteAttachment(item->GetObject()); delete item; break; } } return ThingUi::OnEvent(Msg); } void CalendarUi::CheckConsistancy() { THREAD_UNSAFE(); auto St = CurrentStart(); auto En = CurrentEnd(); if (St.IsValid() && En.IsValid() && En < St) { En = St; En.AddMinutes(30); SetCtrlName(IDC_END_DATE, En.GetDate().Get()); SetCtrlName(IDC_END_TIME, En.GetTime().Get()); } } LDateTime CalendarUi::CurrentStart() { LDateTime dt; THREAD_UNSAFE(dt); dt.SetDate(GetCtrlName(IDC_START_DATE)); dt.SetTime(GetCtrlName(IDC_START_TIME)); return dt; } LDateTime CalendarUi::CurrentEnd() { LDateTime dt; THREAD_UNSAFE(dt); dt.SetDate(GetCtrlName(IDC_END_DATE)); dt.SetTime(GetCtrlName(IDC_END_TIME)); return dt; } void CalendarUi::UpdateStartRelative() { THREAD_UNSAFE(); LDateTime dt = CurrentStart(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_START_REL, s); } } void CalendarUi::UpdateEndRelative() { THREAD_UNSAFE(); LDateTime dt = CurrentEnd(); if (dt.IsValid()) { const char *s = RelativeTime(dt); SetCtrlName(IDC_END_REL, s); } } void CalendarUi::UpdateRelative() { THREAD_UNSAFE(); CheckConsistancy(); UpdateStartRelative(); UpdateEndRelative(); } int CalendarUi::OnNotify(LViewI *Ctrl, LNotification n) { THREAD_UNSAFE(0); if (!NotifyOn) return false; switch (Ctrl->GetId()) { case IDC_START_DATE: case IDC_START_TIME: { CheckConsistancy(); UpdateStartRelative(); break; } case IDC_END_DATE: case IDC_END_TIME: { CheckConsistancy(); UpdateEndRelative(); break; } case IDC_REMINDER_TYPE: { LViewI *Label, *Param; if (GetViewById(IDC_PARAM_LABEL, Label) && GetViewById(IDC_REMINDER_PARAM, Param)) { Param->Name(NULL); switch (Ctrl->Value()) { case CalEmail: Label->Name("optional email:"); Param->Enabled(true); break; default: case CalPopup: Label->Name(NULL); Param->Enabled(false); break; case CalScriptCallback: Label->Name("callback fn:"); Param->Enabled(true); break; } } break; } case IDC_ALL_DAY: { int64 AllDay = Ctrl->Value(); LDataI *o = Item->GetObject(); char s[256] = ""; auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_START_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt) { LDateTime tmp = *dt; tmp.ToLocal(true); tmp.GetTime(s, sizeof(s)); SetCtrlName(IDC_END_TIME, AllDay ? "" : s); SetCtrlEnabled(IDC_END_TIME, !AllDay); } break; } case IDC_SUBJECT: { SetCtrlEnabled(IDC_SAVE, ValidStr(Ctrl->Name())); break; } case IDC_SHOW_LOCATION: { LString Loc = GetCtrlName(IDC_LOCATION); if (ValidStr(Loc)) { for (char *c = Loc; *c; c++) { if (Strchr(LWhiteSpace, *c)) *c = '+'; } LString Uri; Uri.Printf("http://maps.google.com/?q=%s", Loc.Get()); LExecute(Uri); } break; } case IDC_GUESTS: { if (n.Type == LNotifyItemInsert) { d->Guests->ResizeColumnsToContent(); } else if (n.Type == LNotifyDeleteKey) { List la; d->Guests->GetSelection(la); la.DeleteObjects(); } break; } case IDC_GUEST_ENTRY: { if (d->Entry) { if (ValidStr(d->Entry->Name())) { if (!d->Browse) { d->Browse = new AddressBrowse(Item->App, d->Entry, d->Guests, NULL); } } if (d->Browse) { d->Browse->OnNotify(d->Entry, n); } if (n.Type == LNotifyReturnKey) { d->OnGuest(); } } break; } case IDC_REPEAT: { if (!Ctrl->Value()) break; auto Dlg = new LRecurDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto ctrlId) { if (!ctrlId) SetCtrlValue(IDC_REPEAT, false); delete dlg; }); break; } case IDC_TIMEZONE: { auto Tz = Item->GetObject()->GetStr(FIELD_CAL_TIMEZONE); auto Dlg = new LInput(this, Tz, "Time zone:", "Calendar Event Timezone"); Dlg->DoModal([this, Dlg](auto dlg, auto Result) { if (Result) { Item->GetObject()->SetStr(FIELD_CAL_TIMEZONE, Dlg->GetStr()); Item->SetDirty(); } delete dlg; }); break; } case IDC_REMINDER_ADD: { const char *v = GetCtrlName(IDC_REMINDER_VALUE); const char *param = GetCtrlName(IDC_REMINDER_PARAM); d->Reminders->Insert ( new ReminderItem ( (CalendarReminderType) d->ReminderType->Value(), v ? (float)atof(v) : 1.0f, (CalendarReminderUnits) d->ReminderUnit->Value(), param ) ); d->Reminders->ResizeColumnsToContent(); break; } case IDC_ADD_GUEST: { d->OnGuest(); break; } case IDC_ADD_FILE: { if (!Item) { LAssert(!"No item"); break; } auto sel = new LFileSelect(this); sel->MultiSelect(true); sel->Open([this, sel](auto dlg, auto ok) { if (ok) { LList *attachLst = NULL; if (!GetViewById(IDC_ATTACHMENTS, attachLst)) { LAssert(!"No list"); } else { for (size_t i=0; iLength(); i++) { auto fn = (*sel)[i]; if (!fn) { LAssert(!"No filename"); continue; } auto data = Item->ImportAttachment(fn); if (!data) { LAssert(!"ImportAttachment failed"); break; } attachLst->Insert(new CalendarAttachmentItem(this, data)); } attachLst->ResizeColumnsToContent(); } } delete dlg; }); break; } case IDC_SAVE: { LViewI *v; if (GetViewById(IDC_GUEST_ENTRY, v) && v->Focus()) { d->OnGuest(); break; } else { OnSave(); // Fall through } } case IDCANCEL: { if (Item) { // Is the user canceling an object that hasn't been saved yet? // If so delete the object. auto obj = Item->GetObject(); if (obj && obj->IsOrphan()) { Item->DecRef(); Item = NULL; } } Quit(); break; } } return 0; } void CalendarUi::OnLoad() { THREAD_UNSAFE(); // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Copy object values into UI int64 AllDay = false; auto CalSubject = o->GetStr(FIELD_CAL_SUBJECT); SetCtrlName(IDC_SUBJECT, CalSubject); SetCtrlName(IDC_LOCATION, o->GetStr(FIELD_CAL_LOCATION)); SetCtrlName(IDC_DESCRIPTION, o->GetStr(FIELD_CAL_NOTES)); SetCtrlValue(IDC_ALL_DAY, AllDay = o->GetInt(FIELD_CAL_ALL_DAY)); if (d->Guests) { LJson j(o->GetStr(FIELD_ATTENDEE_JSON)); for (auto g: j.GetArray(LString())) { LAutoPtr la(new ListAddr(App)); if (la) { la->sAddr = g.Get("email"); la->sName = g.Get("name"); d->Guests->Insert(la.Release()); } } d->Guests->ResizeColumnsToContent(); } if (d->Reminders) { d->Reminders->Empty(); LString Rem = o->GetStr(FIELD_CAL_REMINDERS); auto a = Rem.SplitDelimit("\n"); for (unsigned i=0; iSetString(a[i])) d->Reminders->Insert(ri); else delete ri; } } d->Reminders->ResizeColumnsToContent(); } auto 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; } auto 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; } auto 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; } } auto dt = o->GetDate(FIELD_CAL_START_UTC); if (dt && dt->IsValid()) { 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()); SetCtrlName(IDC_START_DATE, tmp.GetDate()); SetCtrlName(IDC_START_TIME, AllDay ? "" : tmp.GetTime().Get()); SetCtrlEnabled(IDC_START_TIME, !AllDay); } dt = o->GetDate(FIELD_CAL_END_UTC); if (dt && dt->IsValid()) { 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()); SetCtrlName(IDC_END_DATE, tmp.GetDate()); SetCtrlName(IDC_END_TIME, AllDay ? "" : tmp.GetTime().Get()); 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)) Name(LString::Fmt("%s - %s", LLoadString(IDS_CAL_EVENT), CalSubject)); // Load the list of attached files: LList *attachLst; if (GetViewById(IDC_ATTACHMENTS, attachLst)) { auto fileLst = Item->GetObject()->GetList(FIELD_CAL_ATTACHMENTS); if (fileLst) { for (auto i=fileLst->First(); i; i=fileLst->Next()) { auto data = dynamic_cast(i); if (data) attachLst->Insert(new CalendarAttachmentItem(this, data)); else LAssert(!"Wrong object!"); } // No point resizing the columns here, as the table hasn't has it's first layout // And will most likely be too small attachLst->ResizeColumnsToContent(); } } UpdateRelative(); } void CalendarUi::OnSave() { THREAD_UNSAFE(); // Sanity check if (!Item) { LAssert(!"No item."); return; } LDataI *o = Item->GetObject(); if (!o) { LAssert(!"No object."); return; } // Save UI values into object bool AllDay = false; o->SetStr(FIELD_CAL_SUBJECT, GetCtrlName(IDC_SUBJECT)); o->SetStr(FIELD_CAL_LOCATION, GetCtrlName(IDC_LOCATION)); o->SetStr(FIELD_CAL_NOTES, GetCtrlName(IDC_DESCRIPTION)); o->SetInt(FIELD_CAL_ALL_DAY, AllDay = (GetCtrlValue(IDC_ALL_DAY) != 0)); if (d->Guests) { List All; if (d->Guests->GetAll(All)) { LString::Array a; LString Sep(", "); for (auto la: All) { LString e; if (la->Serialize(e, true)) a.Add(e); } LString Guests = Sep.Join(a); o->SetStr(FIELD_TO, Guests); } } if (d->Reminders) { List All; if (d->Reminders->GetAll(All)) { LString::Array a; LString Sep("\n"); for (auto ri: All) { a.Add(ri->GetString()); } LString Reminders = Sep.Join(a); o->SetStr(FIELD_CAL_REMINDERS, Reminders); } else { o->SetStr(FIELD_CAL_REMINDERS, ""); } } int64 Show = GetCtrlValue(IDC_AVAILABLE_TYPE); switch (Show) { case 0: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalFree); break; case 1: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalTentative); break; default: case 2: o->SetInt(FIELD_CAL_SHOW_TIME_AS, CalBusy); break; } int64 Priv = GetCtrlValue(IDC_PRIVACY_TYPE); switch (Priv) { default: case 0: o->SetInt(FIELD_CAL_PRIVACY, CalDefaultPriv); break; case 1: o->SetInt(FIELD_CAL_PRIVACY, CalPublic); break; case 2: o->SetInt(FIELD_CAL_PRIVACY, CalPrivate); break; } auto Col = d->Colour->Value(); o->SetInt(FIELD_COLOUR, Col > 0 ? Col : -1); /* FIXME: change calendar item location if the user selects a different cal for (unsigned i=0; iSources.Length(); i++) { CalendarSource *src = d->Sources[i]; LAutoString s_path(src->GetPath()); LAutoString i_path; if (Item->GetFolder()) i_path = Item->GetFolder()->GetPath(); if (s_path && i_path && _stricmp(s_path, i_path) == 0) { SetCtrlValue(IDC_CALENDAR, i); break; } } */ LDateTime dt; if (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(); if (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,3648 +1,3648 @@ #include "Scribe.h" #include "lgi/common/ListItemCheckBox.h" #include "lgi/common/ListItemRadioBtn.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/DropFiles.h" #include "lgi/common/MonthView.h" #include "lgi/common/YearView.h" #include "lgi/common/Array.h" #include "lgi/common/ScrollBar.h" #include "lgi/common/MonthView.h" #include "lgi/common/SkinEngine.h" #include "lgi/common/Combo.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Printer.h" #include "lgi/common/Notifications.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/PopupNotification.h" #include "CalendarView.h" #include "ScribePageSetup.h" #include "Store3Webdav/WebdavStore.h" #include "resdefs.h" #include "resource.h" auto ScribeCalendarObject = "com.memecode.Calendar"; #define BORDER_YEAR 28 #define THRESHOLD_EDGE 3 // Px #define WEEKEND_TINT_LEVEL 0.97f #define WEEKEND_TINT_COLOUR LColour(0, 0, 0xd0) #define TODAY_TINT_LEVEL 0.95f #define TODAY_TINT_COLOUR LColour(255, 0, 0) ///////////////////////////////////////////////////////////////////////////////////// #define IDM_CAL_DAY 100 #define IDM_CAL_WEEK 101 #define IDM_CAL_MONTH 102 #define IDM_CAL_YEAR 103 #define IDM_PREV 200 #define IDM_BACK 201 #define IDM_TODAY 202 #undef IDM_FORWARD #define IDM_FORWARD 203 #define IDM_NEXT 204 #define IDM_CONFIG 205 #define IDM_TODO 206 #define IDM_15MIN 207 #define IDM_30MIN 208 #define IDM_1HR 209 #define IDM_NEW_EVENT 210 #define IDC_TODO 300 ///////////////////////////////////////////////////////////////////////////////////// void LoadCalendarStringTable() { ShortDayNames[0] = LLoadString(IDS_CAL_SDAY_SUN); ShortDayNames[1] = LLoadString(IDS_CAL_SDAY_MON); ShortDayNames[2] = LLoadString(IDS_CAL_SDAY_TUE); ShortDayNames[3] = LLoadString(IDS_CAL_SDAY_WED); ShortDayNames[4] = LLoadString(IDS_CAL_SDAY_THU); ShortDayNames[5] = LLoadString(IDS_CAL_SDAY_FRI); ShortDayNames[6] = LLoadString(IDS_CAL_SDAY_SAT); FullDayNames[0] = LLoadString(IDS_CAL_LDAY_SUN); FullDayNames[1] = LLoadString(IDS_CAL_LDAY_MON); FullDayNames[2] = LLoadString(IDS_CAL_LDAY_TUE); FullDayNames[3] = LLoadString(IDS_CAL_LDAY_WED); FullDayNames[4] = LLoadString(IDS_CAL_LDAY_THU); FullDayNames[5] = LLoadString(IDS_CAL_LDAY_FRI); FullDayNames[6] = LLoadString(IDS_CAL_LDAY_SAT); ShortMonthNames[0] = LLoadString(IDS_CAL_SMONTH_JAN); ShortMonthNames[1] = LLoadString(IDS_CAL_SMONTH_FEB); ShortMonthNames[2] = LLoadString(IDS_CAL_SMONTH_MAR); ShortMonthNames[3] = LLoadString(IDS_CAL_SMONTH_APR); ShortMonthNames[4] = LLoadString(IDS_CAL_SMONTH_MAY); ShortMonthNames[5] = LLoadString(IDS_CAL_SMONTH_JUN); ShortMonthNames[6] = LLoadString(IDS_CAL_SMONTH_JUL); ShortMonthNames[7] = LLoadString(IDS_CAL_SMONTH_AUG); ShortMonthNames[8] = LLoadString(IDS_CAL_SMONTH_SEP); ShortMonthNames[9] = LLoadString(IDS_CAL_SMONTH_OCT); ShortMonthNames[10] = LLoadString(IDS_CAL_SMONTH_NOV); ShortMonthNames[11] = LLoadString(IDS_CAL_SMONTH_DEC); FullMonthNames[0] = LLoadString(IDS_CAL_LMONTH_JAN); FullMonthNames[1] = LLoadString(IDS_CAL_LMONTH_FEB); FullMonthNames[2] = LLoadString(IDS_CAL_LMONTH_MAR); FullMonthNames[3] = LLoadString(IDS_CAL_LMONTH_APR); FullMonthNames[4] = LLoadString(IDS_CAL_LMONTH_MAY); FullMonthNames[5] = LLoadString(IDS_CAL_LMONTH_JUN); FullMonthNames[6] = LLoadString(IDS_CAL_LMONTH_JUL); FullMonthNames[7] = LLoadString(IDS_CAL_LMONTH_AUG); FullMonthNames[8] = LLoadString(IDS_CAL_LMONTH_SEP); FullMonthNames[9] = LLoadString(IDS_CAL_LMONTH_OCT); FullMonthNames[10] = LLoadString(IDS_CAL_LMONTH_NOV); FullMonthNames[11] = LLoadString(IDS_CAL_LMONTH_DEC); } ///////////////////////////////////////////////////////////////////////////////////// class LColourItem : public LListItemColumn { LListItem *Item; LView *Colour; public: LColourItem(LListItem *i, int c, COLOUR col = -1) : LListItemColumn(i, c) { Item = i; Colour = LViewFactory::Create("LColourSelect"); LAssert(Colour != NULL); LArray colours; for (int n=0; nSetColourList(&colours); Colour->Value(col); } } ~LColourItem() { DeleteObj(Colour); } void OnPaintColumn(ItemPaintCtx &Ctx, int i, LItemColumn *c) { if (!Colour->IsAttached()) { Colour->Attach(Item->GetList()); } Colour->SetPos(Ctx); Colour->Visible(true); } void OnMouseClick(LMouse &m) { Colour->OnMouseClick(m); } int64 Value() { return Colour->Value(); } void Value(int64 i) { Colour->Value(i); } void Disconnect() { Colour->Detach(); } }; ///////////////////////////////////////////////////////////////////////////////////// CalendarTodoItem::CalendarTodoItem(ScribeWnd *app, Calendar *todo) { App = app; Todo = 0; Done = 0; SetTodo(todo); EditingLabel = false; } CalendarTodoItem::~CalendarTodoItem() { if (Todo) { LAssert(Todo->TodoView == this); Todo->TodoView = 0; } } int TodoCompare(LListItem *la, LListItem *lb, NativeInt d) { int Status = 0; CalendarTodoItem *a = dynamic_cast(la); CalendarTodoItem *b = dynamic_cast(lb); if (a && b) { int Col = abs((int)d) - 1; int Mul = d >= 0 ? 1 : -1; bool Atodo = a->Todo != 0; bool Btodo = b->Todo != 0; if (Atodo ^ Btodo) { Status = Btodo - Atodo; } else if (a->Todo && b->Todo) { switch (Col) { case 0: { // Completed int Acomplete = 0; int Bcomplete = 0; a->Todo->GetField(FIELD_CAL_COMPLETED, Acomplete); b->Todo->GetField(FIELD_CAL_COMPLETED, Bcomplete); if (Acomplete != Bcomplete) { Status = Mul * (Acomplete - Bcomplete); } else { goto ByDueDate; } break; } case 1: { // Subject BySubject: const char *Asub = 0; const char *Bsub = 0; a->Todo->GetField(FIELD_CAL_SUBJECT, Asub); b->Todo->GetField(FIELD_CAL_SUBJECT, Bsub); if (Asub && Bsub) { Status = Mul * _stricmp(Asub, Bsub); } break; } case 2: { // Due date ByDueDate: LDateTime Adate; LDateTime Bdate; a->Todo->GetField(FIELD_CAL_START_UTC, Adate); b->Todo->GetField(FIELD_CAL_START_UTC, Bdate); bool Ad = Adate.Year() != 0; bool Bd = Bdate.Year() != 0; if (Ad ^ Bd) { Status = Mul * (Bd - Ad); } else if (Ad && Bd) { Status = Mul * Adate.Compare(&Bdate); } else { goto BySubject; } break; } } } } return Status; } void CalendarTodoItem::Resort() { if (GetList()) { int Sort = 1; for (int i=0; iGetColumns(); i++) { auto c = GetList()->ColumnAt(i); if (c) { if (c->UpArrow()) { Sort = -(i + 1); break; } else if (c->DownArrow()) { Sort = i + 1; break; } } } GetList()->Sort(TodoCompare, Sort); } } void CalendarTodoItem::SetTodo(Calendar *todo) { if (Todo) { Todo->TodoView = 0; } Todo = todo; if (Todo) { Todo->TodoView = this; int Completed = false; Todo->GetField(FIELD_CAL_COMPLETED, Completed); Done = new LListItemCheckBox(this, 0, Completed != 0); } else { DeleteObj(Done); } SetImage(Todo ? ICON_TODO : -1); } const char *CalendarTodoItem::GetText(int Col) { if (Todo) { switch (Col) { case 1: { // Name const char *s; if (Todo->GetField(FIELD_CAL_SUBJECT, s)) { return s; } break; } case 2: { // Due LDateTime d; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); d.Get(DateCache, sizeof(DateCache)); return DateCache; } break; } } } else if (Col == 1) { if (EditingLabel) { EditingLabel = false; } else { return (char*)LLoadString(IDS_CLICK_TO_CREATE); } } return 0; } bool CalendarTodoItem::SetText(const char *s, int Col) { if (Col == 1) { if (Todo) { // Set the name... Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); Resort(); } else { if (ValidStr(s)) { // Create new todo ScribeFolder *Cal = App->GetFolder(FOLDER_CALENDAR); if (Cal) { Thing *t = App->CreateItem(MAGIC_CALENDAR, Cal, false); if (t && t->IsCalendar()) { SetTodo(t->IsCalendar()); Todo->SetCalType(CalTodo); Todo->SetField(FIELD_CAL_SUBJECT, (char*)s); GetList()->Insert(new CalendarTodoItem(App)); Resort(); } } else { LgiMsg(GetList(), "No calendar folder.", AppName); } } else { return false; } } } return LListItem::SetText(s, Col); } void CalendarTodoItem::OnPaint(ItemPaintCtx &Ctx) { if (!Todo) { Ctx.Fore = LColour(L_LOW); Ctx.Back = LColour(L_WORKSPACE); } else if (Done && Done->Value()) { Ctx.Fore = LColour(L_LOW); } else { LDateTime d, Now; if (Todo->GetField(FIELD_CAL_START_UTC, d)) { d.ToLocal(true); Now.SetNow(); if (Now.Compare(&d) > 0) { Ctx.Fore.Set(255, 0, 0); } } } LListItem::OnPaint(Ctx); } void CalendarTodoItem::OnColumnNotify(int Col, int64 Data) { if (Col == 0) { Todo->SetField(FIELD_CAL_COMPLETED, Data ? 100 : 0); Resort(); } } void CalendarTodoItem::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Down()) { if (m.Left()) { int Col = Todo ? GetList()->ColumnAtX(m.x) : 1; switch (Col) { case 1: { if (!Todo) { EditingLabel = true; } EditLabel(1); break; } default: { if (Todo && m.Double()) { Todo->DoUI(); } break; } } } else if (m.Right()) { if (Todo) { Todo->DoContextMenu(m, GetList()); } } } } ///////////////////////////////////////////////////////////////////////////////////// class LMonthView : public LView, public MonthView { LRect rTitle; LRect rCells; LRect rLeft, rRight; int Cell; CalendarView *CalView; public: LMonthView(int Id, LDateTime *n, CalendarView *calView); void OnCellClick(int Cx, int Cy); void SeekMonth(int Dir); void OnMouseClick(LMouse &m); void OnPaint(LSurface *pDC); }; // This structure encodes the direction to move the cursor // on different key presses. The 'years' isn't implemented in // the OnKey handler. struct CalViewArrow { public: CalendarViewMode Mode; int Key; int Flags; int Hours; int Days; int Months; int Years; }; CalViewArrow Arrows[] = { // Flags, H D M Y // Week view movement {CAL_VIEW_WEEK, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_WEEK, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_WEEK, LK_UP, 0, -1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_DOWN, 0, 1, 0, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, 0, 0, -7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,0, 0, 7, 0, 0}, {CAL_VIEW_WEEK, LK_PAGEUP, LGI_EF_CTRL, 0, 0, -1, 0}, {CAL_VIEW_WEEK, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 1, 0}, // Month view movement {CAL_VIEW_MONTH, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_MONTH, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_MONTH, LK_UP, 0, 0, -7, 0, 0}, {CAL_VIEW_MONTH, LK_DOWN, 0, 0, 7, 0, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, 0, 0, 0, -1, 0}, {CAL_VIEW_MONTH, LK_PAGEDOWN,0, 0, 0, 1, 0}, {CAL_VIEW_MONTH, LK_PAGEUP, LGI_EF_CTRL, 0, 0, 0, -1}, {CAL_VIEW_MONTH, LK_PAGEDOWN,LGI_EF_CTRL, 0, 0, 0, 1}, // Year view movement {CAL_VIEW_YEAR, LK_LEFT, 0, 0, -1, 0, 0}, {CAL_VIEW_YEAR, LK_RIGHT, 0, 0, 1, 0, 0}, {CAL_VIEW_YEAR, LK_UP, 0, 0, 0, -1, 0}, {CAL_VIEW_YEAR, LK_DOWN, 0, 0, 0, 1, 0}, {CAL_VIEW_YEAR, LK_PAGEUP, 0, 0, 0, 0, -1}, {CAL_VIEW_YEAR, LK_PAGEDOWN,0, 0, 0, 0, 1} }; CalViewArrow *GetModeArrow(CalendarViewMode m, LKey &k) { for (int i=0; i CalendarView::CalendarViews; CalendarView::CalendarView(ScribeFolder *folder, int id, LRect *pos, const char *name) { CalendarViews.Add(this); if (Calendar::DayStart < 0) InitCalendarView(); DragMode = DragNone; DragEvent = NULL; LastTsOffset = 0; DayStart = Calendar::DayStart; DayEnd = Calendar::DayEnd; if (!App) App = folder->App; Mode = CAL_VIEW_MONTH; LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarViewMode, v)) Mode = (CalendarViewMode) v.CastInt32(); if (id > 0) SetId(id); if (pos) SetPos(*pos); if (name) Name(name); SetPourLargest(true); Sunken(true); MonthX = 7; MonthY = 5; LFontType Type; Type.GetSystemFont("Small"); Font.Reset(Type.Create()); Cursor.SetNow(); LoadUsers(); OnOptionsChange(); } CalendarView::~CalendarView() { LVariant v; App->GetOptions()->SetValue(OPT_CalendarViewMode, v = (int)Mode); CalendarViews.Delete(this); } void CalendarView::OnOptionsChange() { LVariant v; if (App && App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); for (auto c: CalendarViews) c->OnContentsChanged(); } CalendarView *Calendar::GetView() { if (CalendarView::CalendarViews.Length() == 0) return NULL; return CalendarView::CalendarViews[0]; } void CalendarView::LoadUsers() { LArray Sources; App->GetCalendarSources(Sources); LVariant v; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); LDateTime dt = Cursor; Cursor.Set("1/1/1900"); SetCursor(dt); // Do we really need this? // OnContentsChanged(); } void CalendarView::DeleteSource(CalendarSource *cs) { // Delete events out of 'Current' for (unsigned i=0; iGetSource() == cs) { Current.DeleteAt(i--); } } // Delete the reference in the options file... cs->Delete(); // Delete the C++ object and list ref... DeleteObj(cs); // Refresh the screen. Invalidate(); } bool CalendarView::GetEventsBetween(LArray &Events, LDateTime Start, LDateTime End) { bool Status = false; LDateTime EndMinute = End; EndMinute.AddMinutes(-1); for (auto &t: Current) { if (t.Overlap(Start, End)) { Events.Add(t); Status = true; } } return Status; } void CalendarView::OnCreate() { SetWindow(this); SetupScroll(); SetPulse(3000); } void CalendarView::OnPulse() { CalendarViewWnd *w = dynamic_cast(GetWindow()); LArray all; if (w->CalLst->GetAll(all)) { for (auto a: all) a->OnPulse(); } } LMessage::Result CalendarView::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_GET_EVENTS_DONE: { if (SourceEventsDirty) { SourceEventsDirty = false; Invalidate(); } break; } } return LLayout::OnEvent(Msg); } bool CalendarView::OnLayout(LViewLayoutInfo &Inf) { if (Inf.Width.Max == 0) { Inf.Width.Min = -1; Inf.Width.Max = -1; } else { Inf.Height.Min = -1; Inf.Height.Max = -1; } return true; } int CalendarView::OnNotify(LViewI *v, LNotification n) { switch (v->GetId()) { case IDC_VSCROLL: { if (n.Type == LNotifyScrollBarCreate) { SetupScroll(); } Invalidate(); break; } } return 0; } void CalendarView::SelectDropTarget(LDateTime *start, LDateTime *end) { /* if (start && DropStart) { if (*start == *DropStart) { // the same return; } } DeleteObj(DropStart); DeleteObj(DropEnd); if (start) { DropStart = new LDateTime; if (DropStart) { *DropStart = *start; } if (end) { DropEnd = new LDateTime; if (DropEnd) { *DropEnd = *end; } } } Invalidate(); */ } void CalendarView::OnSelect(Calendar *c, bool Ctrl, bool Shift) { if (!Ctrl) { Selection.Length(0); } else { Selection.Delete(c); } if (c) { Selection.Add(c); Invalidate(&c->ViewPos); } } CalendarViewMode CalendarView::GetViewMode() { return Mode; } void CalendarView::SetupScroll() { SetScrollBars(false, Mode == CAL_VIEW_WEEK); if (VScroll) { int Page = (int)(Calendar::DayEnd - Calendar::DayStart); VScroll->SetRange(24); VScroll->SetPage(Page); VScroll->Value(Calendar::DayStart); } } void CalendarView::CalDelete(Calendar *c) { Selection.Delete(c); for (size_t i=0; i 0) Diff -= 7; Start.AddDays(Diff); First = Start; break; } case CAL_VIEW_MONTH: { First = Cursor; First.Day(1); Start = First; int DayOfWeek = Start.DayOfWeek(); int Diff = FirstDayOfWeek - DayOfWeek; if (Diff > 0) Diff -= 7; Start.AddDays(Diff); break; } case CAL_VIEW_YEAR: { YearView v(&Cursor); v.SetCursor(0, 0); First = v.Get(); Start = Cursor; Start.Day(1); Start.Month(1); break; } } bool YearCh = Old.Year() != Cursor.Year(); bool MthCh = Old.Month() != Cursor.Month() || YearCh; bool DayCh = Old.Day() != Cursor.Day() || MthCh; OnCursorChange(DayCh, MthCh, YearCh); } void CalendarView::OnSourceDelete(CalendarSource *s) { for (size_t i=0; i, bool> InCur; for (auto &c: Current) InCur.Add(c.c, true); for (unsigned i=0; iSource = NULL; } */ if (!GetEvents) { new CalendarSourceGetEvents( App, &GetEvents, s, e, CalendarSource::GetSources(), [this](auto Events) { Current = Events; Invalidate(); PostEvent(M_GET_EVENTS_DONE); }); } else { SourceEventsDirty = true; } - LDateTime::GetDaylightSavingsInfo(Dst, s, &e); + LTimeZone::GetDaylightSavingsInfo(Dst, s, &e); LWindow *Wnd = GetWindow(); if (Wnd) { char s[256]; sprintf_s(s, sizeof(s), "%s [%s %i]", LLoadString(IDS_CAL_VIEW), FullMonthNames[Cursor.Month()-1], Cursor.Year()); Wnd->Name(s); } } Invalidate(); } bool CalendarView::OnPrintPage(LPrintDC *pDC, int PageIndex) { LVariant Bx1, By1, Bx2, By2; LOptionsFile *Options = App->GetOptions(); LFontType FontType("Courier New", 8); FontType.GetSystemFont("small"); Bx1 = By1 = Bx2 = By2 = 1.0; // cm if (Options) { // read any options out.. #define GetMargin(opt, var) \ { LVariant v; if (Options->GetValue(opt, v)) var = v.CastDouble(); } GetMargin(OPT_MarginX1, Bx1); GetMargin(OPT_MarginY1, By1); GetMargin(OPT_MarginX2, Bx2); GetMargin(OPT_MarginY2, By2); } LAutoPtr ScreenFont = Font; if (pDC) { // setup device context double CmToInch = 0.393700787; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); double ScaleX = (double)ScreenDpi.x / DcDpi.x; double ScaleY = (double)ScreenDpi.y / DcDpi.y; // LRect c = GetClient(); PrintMargin.x1 = (int) (( (Bx1.CastDouble() * CmToInch) * DcDpi.x ) * ScaleX); PrintMargin.y1 = (int) (( (By1.CastDouble() * CmToInch) * DcDpi.y ) * ScaleY); PrintMargin.x2 = (int) (( pDC->X() - ((Bx2.CastDouble() * CmToInch) * DcDpi.x) ) * ScaleX); PrintMargin.y2 = (int) (( pDC->Y() - ((By2.CastDouble() * CmToInch) * DcDpi.y) ) * ScaleY); // setup font Font.Reset(FontType.Create(pDC)); if (Font) { Font->Colour(L_BLACK, L_WHITE); Font->Create(0, 0, pDC); LDisplayString ds(Font, " "); Font->TabSize(ds.X() * 8); OnPaint(pDC); } } Font = ScreenFont; return false; } int EventSorter(TimePeriod *a, TimePeriod *b) { return a->s.Compare(&b->s); } bool CalendarView::Overlap(LDateTime &Start, LDateTime &End, Calendar *a, Calendar *b) { if (a && b) { if (a->Overlap(b)) { LArray Ap, Bp; a->GetTimes(Start, End, Ap); b->GetTimes(Start, End, Bp); for (unsigned i=0; iFont->Colour(L_TEXT, L_MED); i->Font->Transparent(true); LDisplayString ds(i->Font, (char*)i->Txt); ds.Draw(pDC, i->x, i->y, &r); } void CalendarView::DrawSelectionBox(LSurface *pDC, LRect &r) { int Edge = 4; // int Width = 2; #ifdef WINDOWS int Op = pDC->Op(GDC_XOR); pDC->Colour(Rgba32(0xff, 0xff, 0xff, 0), 32); #else // Other platforms don't have a working XOR operator... so black is // a good default against their default White background. pDC->Colour(Rgb24(0, 0, 0), 24); #endif LRect p; // Top p.Set(r.x1, r.y1, r.x2, r.y1 + Edge - 1); PatternBox(pDC, p); // Bottom p.Set(r.x1, r.y2 - Edge + 1, r.x2, r.y2); PatternBox(pDC, p); // Left p.Set(r.x1, r.y1 + Edge, r.x1 + Edge - 1, r.y2 - Edge); PatternBox(pDC, p); // Right p.Set(r.x2 - Edge + 1, r.y1 + Edge, r.x2, r.y2 - Edge); PatternBox(pDC, p); // WriteDC("c:\\temp\\cal.bmp", pDC); #ifdef WINDOWS pDC->Op(Op); #endif } void CalendarView::OnPaint(LSurface *pDC) { #ifndef MAC // Mac is double buffered anyway LDoubleBuffer Buf(pDC); #endif LColour InMonth(L_WORKSPACE); LColour OutMonth = GdcMixColour(LColour(L_HIGH), LColour(L_WORKSPACE), 0.5); LColour CellEdge(0xc0, 0xc0, 0xc0); LSkinEngine *SkinEngine = LAppInst->SkinEngine; pDC->Colour(Rgb32(255, 255, 255), 32); pDC->Rectangle(); LRect c = GetClient(); c.Offset(-c.x1, -c.y1); float _Sx = 1.0; float _Sy = 1.0; if (pDC->IsPrint()) { c = PrintMargin; auto ScreenDpi = LScreenDpi(); auto DcDpi = pDC->GetDpi(); _Sx = (float)DcDpi.x / ScreenDpi.x; _Sy = (float)DcDpi.y / ScreenDpi.y; } float Scale = _Sx < _Sy ? _Sx : _Sy; SRect(c); Layout = c; if (Mode != CAL_VIEW_YEAR) { Title = c; Title.y2 = Title.y1 + Font->GetHeight() + (int)SY(6); Layout.y1 = Title.y2 + 1; } else { Title.ZOff(-1, -1); } for (auto &c: Current) { c.c->ViewPos.Empty(); } switch (Mode) { /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { // Recalc day start/end int DayVisible = DayEnd - DayStart; DayStart = VScroll ? (int)VScroll->Value() : 6; DayEnd = DayStart + DayVisible; // Setup... LDateTime Dt = Start, Now; Dt.SetTime("0:0:0.0"); Now.SetNow(); LDisplayString ds(Font, "22:00p"); int TimeX = (int)((float)ds.X() + SX(10)); Layout.Set(TimeX, Title.y2 + 1, c.x2, c.y2); // d=0 is the hours column, d=1 is the first day (either sun or mon), etc... d=7 is last day for (int d=0; d<8; d++) { LDateTime Tomorrow = Dt; Tomorrow.AddDays(1); LTimeStamp TodayTs, TomorrowTs; Dt.Get(TodayTs); Tomorrow.Get(TomorrowTs); // Heading int x1 = d ? TimeX + ((d-1) * (c.X()-TimeX) / 7) : 0; int x2 = d ? TimeX + ((d * (c.X()-TimeX) / 7) - 1) : TimeX - 1; LRect p( x1, Title.y1, x2, Title.y2); if (d) { int NameIdx = (FirstDayOfWeek+d-1) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } else { pDC->Colour(L_LOW); pDC->Rectangle(&p); } // Content area p.Set( x1, Title.y2 + 1, x2, c.y2); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); int Divisions = DayEnd - DayStart; int DayOfWeek = Dt.DayOfWeek(); #define HourToY(hour) (p.y1 + (((hour)-(double)DayStart) * p.Y() / Divisions)) for (int h=DayStart; h<=DayEnd; h++) { LRect Hour( x1, (int)HourToY(h), x2, (int)HourToY(h+1)-1); LColour Back = DayOfWeek == 0 || DayOfWeek == 6 ? GdcMixColour(LColour(L_WORKSPACE), WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL) : OutMonth; if (DayOfWeek >= Calendar::WorkWeekStart && DayOfWeek <= Calendar::WorkWeekEnd && h >= Calendar::WorkDayStart && h < Calendar::WorkDayEnd) Back = InMonth; /* bool Select = Focus() && Cur->Day() == Dt.Day() && Cur->Hours() == h; if (Select) { Back = LC_FOCUS_SEL_BACK; } */ bool Today = Now.Day() == Dt.Day() && Now.Month() == Dt.Month() && Now.Year() == Dt.Year(); if (Today) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } if (d) { // Draw hourly blocks // Background pDC->Colour(Back); pDC->Rectangle(Hour.x1, Hour.y1, Hour.x2-1, Hour.y2-1); pDC->Colour(CellEdge); pDC->Line(Hour.x1, Hour.y2, Hour.x2, Hour.y2); // Date at the top.. if (h == DayStart) { char s[32]; Dt.GetDate(s, sizeof(s)); Font->Colour(/*Select ? LC_FOCUS_SEL_FORE :*/ L_LOW, L_MED); Font->Transparent(true); LDisplayString ds(Font, s); ds.Draw(pDC, Hour.x1 + 2, Hour.y1 + 2); } } else { // Draw times in the first column char s[32]; if (Now.GetFormat() & GDTF_24HOUR) sprintf_s(s, sizeof(s), "%i:00", h); else sprintf_s(s, sizeof(s), "%i:00%c", h == 0 ? 12 : h > 12 ? h - 12 : h, h >= 12 ? 'p' : 'a'); LRect Temp = Hour; LWideBorder(pDC, Temp, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, s); ds.Draw(pDC, Temp.x1 + (int)SX(2), Temp.y1, &Temp); } } if (d) { // Draw events LArray All; for (auto &t: Current) { if (t.Overlap(Dt, Tomorrow)) All.Add(t); } All.Sort(EventSorter); // Sort the entries into overlapping groups typedef LArray EventArray; LArray Groups; for (uint32_t e=0; e 0) { // Check if it overlaps anything in the previous group EventArray *a = Groups[Groups.Length()-1]; for (uint32_t i=0; iLength(); i++) { TimePeriod *t = (*a)[i]; if (t->Overlap(*Ev)) { // It does... so add it. a->Add(Ev); Ev = 0; break; } } } if (Ev) { // Create new group EventArray *g = new EventArray; if (g) { g->Add(Ev); Groups.Add(g); } } } // Lay the groups of entries out and then paint them for (uint32_t g=0; gGetObject(); auto allDay = obj ? obj->GetInt(FIELD_CAL_ALL_DAY) : false; double StartH = (double) t.s.Hours() + ((double)t.s.Minutes() / 60), EndH; if (t.e.IsSameDay(t.s) && t.e.Hours()) EndH = ((double)t.e.Hours()) + ((double)t.e.Minutes() / 60); else EndH = 24; int x1 = p.x1 + (int)SX(4); int x2 = p.x2 - (int)SX(5); double dx = x2 - x1 + SX(3); int StartX = x1 + (int)((double)i * dx / (double)Group.Length()); int EndX = StartX + (int)((dx / (double)Group.Length()) - SX(5)); LRect Vp( StartX, (int)HourToY(StartH) - 1, EndX, (int)HourToY(EndH) - 3); // LgiTrace("paint: %s %s\n", Vp.GetStr(), t.c->ToString().Get()); t.c->OnPaintView(pDC, Font, &Vp, &t); } } // Clean up the memory Groups.DeleteObjects(); for (unsigned i=0; i TomorrowTs) EndSec = (int)((TomorrowTs - TodayTs) / LDateTime::Second64Bit); else EndSec = (int)((rng.EndTs - TodayTs) / LDateTime::Second64Bit); int StartY = (int) HourToY((double)StartSec / LDateTime::HourLength); int EndY = (int) HourToY((double)EndSec / LDateTime::HourLength); /* printf("%f,%f - %i,%i - %i,%i\n", (double)StartSec / LDateTime::HourLength, (double)EndSec / LDateTime::HourLength, StartY, EndY, DayStart, Divisions); */ LRect r(p.x1 + 4, StartY, p.x2 - 4, EndY); DrawSelectionBox(pDC, r); } } // Increment date of day we're painting Dt = Tomorrow; } } break; } case CAL_VIEW_MONTH: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; int ObjY = Font->GetHeight() + (int)SY(4); LDateTime i = Start, Now, Tomorrow; Now.SetNow(); i.SetTime("0:0:0.0"); Tomorrow = i; Tomorrow.AddDays(1); char Str[256]; for (int h=0; h<7; h++) { LRect p( Title.x1 + (h * Title.X() / MonthX), Title.y1, Title.x1 + (((h+1) * Title.X() / MonthX) - 1), Title.y2); int NameIdx = (h + FirstDayOfWeek) % 7; if (SkinEngine) { ColumnPaintInfo i = { Font, FullDayNames[NameIdx], (int)SX(2), (int)SY(3) }; LSkinState State; State.pScreen = pDC; State.Rect = p; State.View = this; SkinEngine->OnPaint_ListColumn(CalendarColumnPaint, &i, &State); } else { LWideBorder(pDC, p, DefaultRaisedEdge); Font->Colour(L_TEXT, L_MED); Font->Transparent(false); LDisplayString ds(Font, (char*)FullDayNames[NameIdx]); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SY(2), &p); } } for (int y=0; yMonth(); bool Today = Now.Day() == i.Day() && Now.Month() == i.Month() && Now.Year() == i.Year(); if (i.Day() == Cur->Day() && i.Month() == Cur->Month() && i.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Font->Fore(L_FOCUS_SEL_FORE); } else { // normal day Back = (IsInMonth) ? InMonth : OutMonth; Font->Fore(L_TEXT); } if (DayOfWeek == 0 || DayOfWeek == 6) Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); if (Today) Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); int Edge = (int)SX(1); if (!pDC->IsPrint() || Back != LColour(L_WORKSPACE)) { pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-Edge, p.y2-Edge); } pDC->Colour(CellEdge); pDC->Rectangle(p.x2-Edge+1, p.y1, p.x2, p.y2); pDC->Rectangle(p.x1, p.y2-Edge+1, p.x2-Edge, p.y2); if (pDC->IsPrint() && x == 0) { pDC->Rectangle(p.x1, p.y1, p.x1+Edge, p.y2-Edge); } Font->Transparent(true); Font->Back(Back); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1 + (int)SX(2), p.y1 + (int)SX(2)); LRect Clip = p; Clip.Inset(Edge, Edge); pDC->ClipRgn(&Clip); int CalY = ObjY + (int)SY(2); LArray All; uint32_t n; for (n=0; nGetTimes(i, Tomorrow, All); } All.Sort(EventSorter); for (n=0; nOnPaintView(pDC, Font, &Vp, &t); CalY += ObjY + (int)SY(2); } for (auto rng : Ranges) { if (rng.Overlap(i.Ts(), Tomorrow.Ts())) { LRect Vp(p.x1 + (int)SX(3), p.y1 + CalY, p.x2 - (int)SX(4), p.y1 + CalY + ObjY); DrawSelectionBox(pDC, Vp); CalY += ObjY + (int)SY(2); break; } } pDC->ClipRgn(0); i = Tomorrow; Tomorrow.AddDays(1); } } break; } case CAL_VIEW_YEAR: { LDateTime *Cur = (DragStart.IsValid()) ? &DragStart : &Cursor; // int ObjY = Font->GetHeight() + (int)SY(2); LDateTime i = Start, Now, Tomorrow; YearView v(Cur); Now.SetNow(); i.Hours(0); i.Minutes(0); i.Seconds(0); i.Thousands(0); Tomorrow = i; Tomorrow.AddDays(1); Layout.x1 += (int)SX(BORDER_YEAR); int Fy = Font->GetHeight(); char Str[256]; for (int y=0; yTransparent(false); Font->Colour(L_BLACK, L_MED); LDisplayString ds(Font, (char*)ShortMonthNames[y]); ds.Draw(pDC, T.x1 + (int)SX(2), T.y1, &T); for (int x=0; xDay() && t.Month() == Cur->Month() && t.Year() == Cur->Year()) { // Is cursor day Back = Focus() ? LColour(L_FOCUS_SEL_BACK) : GdcMixColour(LColour(L_FOCUS_SEL_BACK), LColour(L_WORKSPACE)); Fore = LColour(L_FOCUS_SEL_FORE); } else { // Other day.. Fore = LColour(L_LOW); Back = v.IsMonth() ? InMonth : OutMonth; } // Weekend tint int Day = t.DayOfWeek(); if (Day == 0 || Day == 6) { Back = GdcMixColour(Back, WEEKEND_TINT_COLOUR, WEEKEND_TINT_LEVEL); } // Today tint.. if (v.IsMonth() && Now.Day() == t.Day() && Now.Month() == t.Month() && Now.Year() == t.Year()) { Back = GdcMixColour(Back, TODAY_TINT_COLOUR, TODAY_TINT_LEVEL); } // Fill cell background pDC->Colour(Back); pDC->Rectangle(p.x1, p.y1, p.x2-1, p.y2-1); pDC->Colour(CellEdge); pDC->Line(p.x2, p.y1, p.x2, p.y2); pDC->Line(p.x1, p.y2, p.x2, p.y2); // Draw day number if (v.IsMonth()) { Font->Transparent(true); Font->Colour(Fore, Back); sprintf_s(Str, sizeof(Str), "%i", t.Day()); LDisplayString ds(Font, Str); ds.Draw(pDC, p.x1+1, p.y1-1); } // Paint events LArray e; LDateTime Tomorrow(t); Tomorrow.AddDays(1); int Cy = p.y1 + Fy; if (v.IsMonth() && GetEventsBetween(e, t, Tomorrow)) { LRect Safe = p; Safe.y2--; for (uint32_t i=0; i Safe.y2) { c->ViewPos.ZOff(-1, -1); } else { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); Vp.Bound(&Safe); c->OnPaintView(pDC, Font, &Vp, &e[i]); Cy += Fy + 1; } } } for (auto rng : Ranges) { if (rng.Overlap(t.Ts(), Tomorrow.Ts())) { LRect Vp(p.x1 + (int)SX(1), Cy, p.x2 - (int)SX(2), Cy + Fy); DrawSelectionBox(pDC, Vp); Cy += Fy + 1; break; } } } } break; } default: { pDC->Colour(L_WHITE); pDC->Rectangle(); break; } } } bool CalendarView::OnKey(LKey &k) { switch (k.vkey) { case LK_ESCAPE: { if (IsCapturing() && k.Down()) { DragStart.Empty(); DragEnd.Empty(); Ranges.Length(0); DragMode = DragNone; Invalidate(); } return true; } default: { switch (k.c16) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down() && GetWindow()) { GetWindow()->Quit(); return true; } break; } } } } /* if (k.c16 != 17 && k.Down()) { // Arrow behaviour CalViewArrow *Arrow = GetModeArrow(Mode, k); if (Arrow) { LDateTime c = Cursor; c.AddHours(Arrow->Hours); c.AddDays(Arrow->Days); c.AddMonths(Arrow->Months); c.AddMonths(Arrow->Years * 12); SetCursor(c); return true; } // Other commands switch (k.c16) { case LK_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { OnDelete(); } return true; break; } } } */ return false; } void CalendarView::OnDelete() { Calendar *c; while ((c=Selection.First())) { c->OnDelete(); Selection.Delete(c); } } Calendar *CalendarView::CalendarAt(int x, int y) { switch (Mode) { default: LAssert(0); break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: case CAL_VIEW_MONTH: case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { for (uint32_t i=0; iViewPos.Overlap(x, y)) { return Current[i].c; } } } break; } } return 0; } LDateTime *CalendarView::TimeAt(int x, int y, int SnapMinutes, LPoint *Cell) { LPoint cell; if (!Cell) Cell = &cell; Cell->x = -1; Cell->y = -1; switch (Mode) { default: break; /* case CAL_VIEW_DAY: { break; } case CAL_VIEW_WEEKDAY: { break; } */ case CAL_VIEW_WEEK: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { int64 Page = (int)(Calendar::DayEnd - Calendar::DayStart); int FirstHr = 0; if (VScroll) { FirstHr = (int) VScroll->Value(); Page = VScroll->Page(); } Cell->x = ((x - Layout.x1) * 7) / Layout.X(); // day in week int MinuteOffset = ((y - Layout.y1) * (int)Page * 60) / Layout.Y(); Cell->y = ((y - Layout.y1) * (int)Page) / Layout.Y(); // hour of day LAssert(MinuteOffset / 60 == Cell->y); int Minutes = (FirstHr * 60) + MinuteOffset; if (SnapMinutes) { int Snap = Minutes % SnapMinutes; if (Snap) { Minutes -= Snap; } } *Day = Start; Day->AddDays(Cell->x); Day->Hours(Minutes / 60); Day->Minutes(Minutes % 60); Day->Seconds(0); Day->Thousands(0); return Day; } } break; } case CAL_VIEW_MONTH: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { Cell->x = ((x - Layout.x1) * MonthX) / Layout.X(); Cell->y = ((y - Layout.y1) * MonthY) / Layout.Y(); *Day = Start; Day->AddDays( (Cell->y * MonthX) + Cell->x ); Day->SetTime("0:0:0"); return Day; } } break; } case CAL_VIEW_YEAR: { if (Layout.Overlap(x, y)) { LDateTime *Day = new LDateTime; if (Day) { YearView v(&Cursor); Cell->x = (x - Layout.x1) * v.X() / Layout.X(); Cell->y = (y - Layout.y1) * v.Y() / Layout.Y(); v.SetCursor(Cell->x, Cell->y); *Day = v.Get(); Day->SetTime("0:0:0"); return Day; } } break; } } return 0; } -LDateTime::LDstInfo *CalendarView::GetDstForDate(LDateTime t) +LDstInfo *CalendarView::GetDstForDate(LDateTime t) { auto ts = t.Ts(); for (uint32_t i=0; i= Prev.Ts() && ts < Next.Ts()) { return &Dst[i]; } } else if (ts > Dst[i].Utc) { return &Dst[i]; } } return 0; } bool CalendarView::HitTest(int x, int y, EventDragMode &mode, Calendar *&event) { if (!Layout.Overlap(x, y)) return false; for (uint32_t i=0; iViewPos.First(); r; r = c->ViewPos.Next()) { if (x >= r->x1 && x <= r->x2) { if (abs(y-r->y1) < THRESHOLD_EDGE) { event = c; mode = DragMoveStart; return true; } if (abs(y-r->y2) < THRESHOLD_EDGE) { event = c; mode = DragMoveEnd; return true; } } } } if (c->ViewPos.Overlap(x, y)) { event = c; mode = DragMoveSelection; return true; } } return false; } Calendar *CalendarView::NewEvent(LDateTime &dtStart, LDateTime &dtEnd) { auto Src = FolderCalendarSource::GetCreateIn(); if (!Src) return NULL; LError createErr; auto c = Src->NewEvent(&createErr); if (!c) { LPopupNotification::Message ( GetWindow(), LString::Fmt("Can't create event in %s: %s", Src->GetClass(), createErr.ToString().Get()) ); return NULL; } LDateTime Start, End; if (dtStart < dtEnd) { Start = dtStart; End = dtEnd; } else { Start = dtEnd; End = dtStart; } LDateTime n = Start; auto CurDst = GetDstForDate(Start); if (CurDst) n.SetTimeZone(CurDst->Offset, false); char s[64]; sprintf_s(s, sizeof(s), "%+.1f", (double)n.GetTimeZone() / 60.0); c->SetField(FIELD_CAL_TIMEZONE, s); Start.ToUtc(true); c->SetField(FIELD_CAL_START_UTC, Start); End.ToUtc(true); c->SetField(FIELD_CAL_END_UTC, End); c->OnCreate(); return c; } void CalendarView::OnMouseClick(LMouse &m) { EventDragMode HitMode = DragNone; Calendar *c = NULL; HitTest(m.x, m.y, HitMode, c); bool AlreadySelected = (c) ? Selection.HasItem(c) : false; LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); /* if (Hit) printf("Hit=%s\n", Hit->Get().Get()); */ if (m.IsContextMenu()) { Focus(true); // Select the item if not already selected when asking for a context menu. if (c && !Selection.HasItem(c)) { Selection.Add(c); Invalidate(&c->ViewPos); } if (!c) { char sHit[64] = ""; if (Hit) Hit->GetTime(sHit, sizeof(sHit)); LString NewMsg; NewMsg.Printf("New event at %s", sHit); LSubMenu s; s.AppendItem(NewMsg, IDM_NEW_EVENT); LSubMenu *snap = s.AppendSub("Snap"); if (snap) { LMenuItem *it = snap->AppendItem("15 minutes", IDM_15MIN); if (it && SnapMinutes == 15) it->Checked(true); it = snap->AppendItem("30 minutes", IDM_30MIN); if (it && SnapMinutes == 30) it->Checked(true); it = snap->AppendItem("1 hour", IDM_1HR); if (it && SnapMinutes == 60) it->Checked(true); } m.ToScreen(); int Cmd = s.Float(this, m); switch (Cmd) { case IDM_NEW_EVENT: { LDateTime End = *Hit; End.AddHours(1); Calendar *ev = NewEvent(*Hit, End); if (ev) ev->DoUI(); break; } case IDM_15MIN: { SnapMinutes = 15; break; } case IDM_30MIN: { SnapMinutes = 30; break; } case IDM_1HR: { SnapMinutes = 60; break; } } } } else { Capture(m.Down()); if (m.Down()) { Focus(true); DragEvent = NULL; if (m.Left()) { Ranges.Length(0); if (!AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } ClickPt.x = m.x; ClickPt.y = m.y; if (Hit) { DragStart = *Hit; if (HitMode == DragMoveStart) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); r.StartTs = DragStart; r.EndTs = DragEnd; } else if (HitMode == DragMoveEnd) { DragMode = HitMode; DragStart = *c->GetObject()->GetDate(FIELD_CAL_START_UTC); DragEnd = *c->GetObject()->GetDate(FIELD_CAL_END_UTC); DragStart.ToLocal(true); DragEnd.ToLocal(true); DragEvent = c; TsRange &r = Ranges.New(); r.StartTs = DragStart; r.EndTs = DragEnd; } else if (Selection.Length()) { DragMode = DragMoveSelection; DragEnd = *Hit; for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); r.StartTs = dt.Ts().Get(); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else { r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); } // Does this new range overlap any existing range? for (unsigned n=0; n %s\n", DragStart.Get().Get(), DragEnd.Get().Get()); TsRange &r = Ranges.New(); DragStart.Get(r.StartTs); DragEnd.Get(r.EndTs); } } else { DragStart.Empty(); DragEnd.Empty(); } Invalidate(); } } else // up { if (DragStart.IsValid() && DragEnd.IsValid()) { switch (DragMode) { case DragNewEvent: { // New event... // printf("DragStart=%s, DragEnd=%s\n", DragStart.Get().Get(), DragEnd.Get().Get()); c = NewEvent(DragStart, DragEnd); if (c) c->DoUI(); OnContentsChanged(NULL); break; } case DragMoveSelection: { // Adjust the times of the selection by the offset. int64 TsOffset = DragEnd.Ts() - DragStart.Ts(); if (TsOffset != 0) { // TsOffset = 0; for (unsigned i=0; iGetObject(); LDateTime start_dt = *o->GetDate(FIELD_CAL_START_UTC); start_dt.Set(start_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_START_UTC, &start_dt); LDateTime end_dt = *o->GetDate(FIELD_CAL_END_UTC); end_dt.Set(end_dt.Ts() + TsOffset); o->SetDate(FIELD_CAL_END_UTC, &end_dt); o->SetInt(FIELD_STATUS, Store3Delayed); s->SetDirty(); } OnContentsChanged(NULL); } break; } case DragMoveStart: { if (DragEvent) { if (DragEnd > DragStart) { LDateTime dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } case DragMoveEnd: { if (DragEvent) { if (DragEnd < DragStart) { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_START_UTC, &dt); dt = DragStart; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } else { LDateTime dt = DragEnd; dt.ToUtc(true); DragEvent->GetObject()->SetDate(FIELD_CAL_END_UTC, &dt); } DragEvent->SetDirty(); OnContentsChanged(NULL); } break; } default: break; } } if (AlreadySelected) { OnSelect(c, m.Ctrl(), m.Shift()); } if (DragStart.IsValid() || Ranges.Length()) { Ranges.Length(0); DragStart.Empty(); DragEnd.Empty(); Invalidate(); } DragEvent = NULL; } } if (c) { c->OnMouseClick(m); } } void CalendarView::OnMouseMove(LMouse &m) { if (IsCapturing()) { LAutoPtr Hit(TimeAt(m.x, m.y, SnapMinutes)); if ( Hit && ( abs(m.x - ClickPt.x) > 4 || abs(m.y - ClickPt.y) > 4 ) ) { #if 1 switch (DragMode) { default: break; case DragMoveStart: case DragMoveEnd: case DragNewEvent: { LDateTime End = *Hit; if (End >= DragStart) End.AddMinutes(SnapMinutes); if (End != DragEnd) { DragEnd = End; if (DragStart < DragEnd) { DragStart.Get(Ranges[0].StartTs); DragEnd.Get(Ranges[0].EndTs); } else { DragStart.Get(Ranges[0].EndTs); DragEnd.Get(Ranges[0].StartTs); } Invalidate(); } break; } case DragMoveSelection: { DragEnd = *Hit; auto TsOffset = DragEnd.Ts() - DragStart.Ts(); if (LastTsOffset != TsOffset) { LastTsOffset = TsOffset; Ranges.Length(0); for (unsigned i=0; iGetObject()->GetDate(FIELD_CAL_START_UTC); dt.ToLocal(true); dt.Get(r.StartTs); dt = *s->GetObject()->GetDate(FIELD_CAL_END_UTC); if (dt.IsValid()) { dt.ToLocal(true); dt.Get(r.EndTs); } else r.EndTs = r.StartTs + ((uint64)60 * 60 * LDateTime::Second64Bit); r.StartTs += TsOffset; r.EndTs += TsOffset; // Does this new range overlap any existing range? for (unsigned n=0; nValue(); v += (int) ceil(Lines / 3.0); VScroll->Value(v); return true; } LCursor CalendarView::GetCursor(int x, int y) { EventDragMode mode = DragNone; Calendar *event = NULL; if (HitTest(x, y, mode, event)) { if (mode == DragMoveStart || mode == DragMoveEnd) return LCUR_SizeVer; } return LView::GetCursor(x, y); } void CalendarView::OnFocus(bool f) { Invalidate(); } ////////////////////////////////////////////////////////////////////////////// int CalendarView::WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { int Status = DROPEFFECT_NONE; LDateTime *Hit = TimeAt(Pt.x, Pt.y, SnapMinutes); SelectDropTarget(Hit); if (Hit) { DeleteObj(Hit); if (Formats.HasFormat(ScribeCalendarObject)) Formats.Supports(ScribeCalendarObject); else Formats.SupportsFileDrops(); if (Formats.GetSupported().Length()) Status = DROPEFFECT_MOVE; } return Status; } void CalendarView::OnDragExit() { SelectDropTarget(); } int CalendarView::OnDrop(LArray &Data, LPoint Pt, int KeyState) { for (auto &dd: Data) { if (dd.IsFileDrop()) { LDropFiles Files(dd); CalendarSource *First = CalendarSource::GetCreateIn(); if (First) { for (auto f: Files) { LAutoPtr in(new LFile); auto type = LGetFileMimeType(f); LString errMsg; LError createErr; if (!in->Open(f, O_READ)) { errMsg.Printf("Failed to open '%s' for reading.", f); } else if (auto c = First->NewEvent(&createErr)) { if (c->Import(c->AutoCast(in), type)) { c->Save(); Invalidate(); } else { errMsg.Printf("Calendar import failed: %s", c->ErrMsg.Get()); c->DecRef(); } } else { errMsg.Printf("Failed to create calendar event in %s: %s", First->GetClass(), createErr.ToString().Get()); } if (errMsg) { LPopupNotification::Message(GetWindow(), errMsg); LgiTrace("%s:%i - %s\n", _FL, errMsg.Get()); } } } else LgiTrace("%s:%i - Do data sources?\n", _FL); } else if (dd.IsFormat(ScribeCalendarObject)) { if (dd.Data.Length() == 0) continue; LVariant *v = &dd.Data[0]; if (v->Type == GV_BINARY && v->Value.Binary.Length >= sizeof(NativeInt)*2 && DragStart.IsValid()) { NativeInt *d = (NativeInt*) v->Value.Binary.Data; if (d[0] == MAGIC_CALENDAR && d[1] > 0) { char Str[32]; if (Mode == CAL_VIEW_WEEK || Mode == CAL_VIEW_DAY) { DragStart.Get(Str, sizeof(Str)); } else { DragStart.GetDate(Str, sizeof(Str)); } Calendar **c = (Calendar**) (d + 2); for (int i=0; iGetField(FIELD_CAL_START_UTC, s)) { bool HasEnd = c[i]->GetField(FIELD_CAL_END_UTC, e); LDateTime Diff; if (HasEnd) Diff = e - s; else Diff.Hours(1); s.Set(Str); c[i]->SetField(FIELD_CAL_START_UTC, s); if (HasEnd) { e = s + Diff; c[i]->SetField(FIELD_CAL_END_UTC, e); } c[i]->Save(); } } SetCursor(DragStart); } } } } SelectDropTarget(); return 0; } void CalendarView::OnDragInit(bool Success) { } char *CalendarView::TypeOf() { return 0; } bool CalendarView::GetData(LArray &Data) { if (Selection.Length() <= 0) return false; bool Status = false; for (unsigned di=0; di(s); if (t) { Status |= t->GetDropFiles(Files); } } if (Status) { LMouse m; GetMouse(m, true); Status |= CreateFileDrop(&dd, m, Files); } } else if (_stricmp(dd.Format, ScribeCalendarObject) == 0) { ssize_t Size = (2 + Selection.Length()) * sizeof(NativeInt); LArray d; if (d.Length(Size)) { d[0] = MAGIC_CALENDAR; d[1] = Selection.Length(); int n=0; Calendar **l = (Calendar**) (&d[2]); for (unsigned i=0; i 0; } class CalendarViewPrint : public LPrintEvents { CalendarView *cv; public: CalendarViewPrint(CalendarView *v) { cv = v; } bool OnPrintPage(LPrintDC *pDC, int PageIndex) { return cv->OnPrintPage(pDC, PageIndex); } }; ////////////////////////////////////////////////////////////////////////////// LMonthView::LMonthView(int Id, LDateTime *n, CalendarView *calView) : MonthView(n) { FirstDayOfWeek = CalendarView::FirstDayOfWeek; SetId(Id); rTitle.ZOff(-1, -1); rCells.ZOff(-1, -1); Cell = 1; CalView = calView; Set(n); } void LMonthView::OnCellClick(int Cx, int Cy) { SetCursor(Cx, Cy); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::SeekMonth(int Dir) { LDateTime t = Cursor; t.AddMonths(Dir); Set(&t); Invalidate(); SendNotify(LNotifyCursorChanged); } void LMonthView::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { } else if (m.Left() && m.Down()) { if (rCells.Overlap(m.x, m.y)) { int x = (m.x - rCells.x1) / Cell; int y = (m.y - rCells.y1) / Cell; OnCellClick(x, y); } else if (rLeft.Overlap(m.x, m.y)) { SeekMonth(-1); } else if (rRight.Overlap(m.x, m.y)) { SeekMonth(1); } } } void LMonthView::OnPaint(LSurface *pDC) { pDC->Colour(L_WORKSPACE); pDC->Rectangle(); Cell = LView::X() / MonthView::X(); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); LRect Client = GetClient(); LDisplayString n(LSysBold, Title()); LSysBold->Colour(L_TEXT, L_WORKSPACE); LSysBold->Transparent(true); n.Draw(pDC, 6, 0); rTitle.ZOff(Client.X()-1, n.Y()*3/2-1); rRight = rTitle; rRight.x1 = rRight.x2 - LSysFont->GetHeight() + 1; rLeft = rRight; rLeft.Offset(-rLeft.Y(), 0); rCells.ZOff(Cell * MonthView::X(), Cell * MonthView::Y()); rCells.Offset(0, rTitle.Y()); LDisplayString DsLeft(LSysFont, "<"); DsLeft.Draw(pDC, rLeft.x1, rLeft.y1); LDisplayString DsRight(LSysFont, ">"); DsRight.Draw(pDC, rRight.x1, rRight.y1); LDateTime t = Start; auto Mode = CalView->GetViewMode(); auto CursorPos = MonthView::GetCursor(); for (int y=0; yColour(L_FOCUS_SEL_FORE, L_FOCUS_SEL_BACK); else { LColour Fore, Back; if (InMonth) { Fore = LColour(L_TEXT); Back.Rgb(0xf7, 0xf7, 0xf7); } else { Fore.Rgb(192, 192, 192); Back = LColour(L_WORKSPACE); } if (Mode == CAL_VIEW_WEEK && y == CursorPos.y) { Back = Back.Mix(LColour(L_FOCUS_SEL_BACK), 0.1f); } LSysFont->Colour(Fore, Back); } LRect r; r.ZOff(Cell-2, Cell-2); r.Offset(x*Cell, y*Cell+rCells.y1); int Cx = Cell - Ds.X(); int Cy = Cell - Ds.Y(); Ds.Draw(pDC, r.x1+(Cx>>1), r.y1+(Cy>>1), &r); if (!t.AddDays(1)) { LAssert(!"Add days failed."); break; } } } } ////////////////////////////////////////////////////////////////////////////// LArray CalendarViewWindows; CalendarViewWnd::CalendarViewWnd(ScribeFolder *folder) { CalendarViewWindows.Add(this); App = folder ? folder->App : 0; Cv = 0; Split = 0; Todo = 0; HorBox = NULL; VerBox = NULL; CalLst = NULL; MonthV = NULL; Name("Calendar View"); if (!SerializeState(App->GetOptions(), OPT_CalendarViewPos, true)) { LRect p(0, 0, 600, 500); SetPos(p); MoveToCenter(); } LDateTime Now; Now.SetNow(); OnOptionsChange(); #if !defined(WINDOWS) SetIcon("_cal.png"); #endif auto ToolBar = folder->App->LoadToolbar(this, folder->App->GetResourceFile(ResToolbarFile), folder->App->GetToolbarImgList()); if (ToolBar) { AddView(ToolBar); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_WEEK)), IDM_CAL_WEEK, TBT_RADIO, true, IMG_CAL_WEEK); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_MONTH)), IDM_CAL_MONTH, TBT_RADIO, true, IMG_CAL_MONTH); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_YEAR)), IDM_CAL_YEAR, TBT_RADIO, true, IMG_CAL_YEAR); ToolBar->AppendSeparator(); ToolBar->AppendButton(0, IDM_PREV, TBT_PUSH, true, IMG_CAL_PREV); ToolBar->AppendButton(0, IDM_BACK, TBT_PUSH, true, IMG_CAL_BACK); ToolBar->AppendButton(LLoadString(IDS_TODAY), IDM_TODAY, TBT_PUSH, true, IMG_CAL_TODAY); ToolBar->AppendButton(0, IDM_FORWARD, TBT_PUSH, true, IMG_CAL_FORWARD); ToolBar->AppendButton(0, IDM_NEXT, TBT_PUSH, true, IMG_CAL_NEXT); ToolBar->AppendSeparator(); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_TODO)), IDM_TODO, TBT_TOGGLE, true, IMG_CAL_TODO); // ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_CONFIGURE)), IDM_CONFIG, TBT_PUSH, true, IMG_CAL_CONFIG); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); ToolBar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); } // Month control #if defined(WINDOWS) int ColPixels = 160; #else int ColPixels = 180; #endif LCss::Len ColPx(LCss::LenPx, (float)ColPixels); LCss::Len Auto("auto"); LCss::Len Pad("10px"); Cv = new CalendarView(folder, IDC_CALENDAR, NULL, "Calendar View"); AddView(HorBox = new LBox); HorBox->AddView(VerBox = new LBox); HorBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->SetVertical(true); VerBox->GetCss(true)->Width(ColPx); VerBox->GetCss(true)->BackgroundColor(LColour(L_WORKSPACE)); VerBox->AddView(MonthV = new LMonthView(IDC_MONTH_VIEW, &Now, Cv)); MonthV->GetCss(true)->Height(ColPx); LRect r(0, 0, ColPixels-1, ColPixels-1); MonthV->SetPos(r); // c->Padding(Pad); // List of calendar sources... VerBox->AddView(CalLst = new LList(IDC_LIST, 0, 0, 100, 100, "Calendar Sources")); CalLst->AddColumn("x", 20); CalLst->AddColumn("Calendar", 150); // Main layout view HorBox->AddView(Cv); if (Cv) { Cv->OnCursorChange(false, true, false); Cv->Visible(true); switch (Cv->GetViewMode()) { default: break; case CAL_VIEW_WEEK: SetCtrlValue(IDM_CAL_WEEK, 1); break; case CAL_VIEW_MONTH: SetCtrlValue(IDM_CAL_MONTH, 1); break; case CAL_VIEW_YEAR: SetCtrlValue(IDM_CAL_YEAR, 1); break; } } if (Cv && CalLst) { for (unsigned i=0; i(s); CalLst->Insert(Li); bool IsCreateIn = FolderCalendarSource::GetCreateIn() == s; Li->Select(IsCreateIn); } } #if WINNATIVE CreateClassW32("Calendar", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_CALENDER))); #endif if (Attach(0)) { AttachChildren(); Visible(true); LVariant ViewTodo; App->GetOptions()->GetValue(OPT_CalendarViewTodo, ViewTodo); SetCtrlValue(IDM_TODO, ViewTodo.CastInt32()); if (ViewTodo.CastInt32()) { // Layout(); } } } CalendarViewWnd::~CalendarViewWnd() { if (CalLst) CalLst->RemoveAll(); // The CalendarView owns the list items. SerializeState(App->GetOptions(), OPT_CalendarViewPos, false); LVariant s; App->GetOptions()->SetValue(OPT_CalendarViewTodo, s = (int)GetCtrlValue(IDM_TODO)); CalendarViewWindows.Delete(this); } void CalendarViewWnd::OptionsChange() { LVariant v; int FirstDayOfWeek = 0; if (App->GetOptions()->GetValue(OPT_CalendarFirstDayOfWeek, v)) FirstDayOfWeek = v.CastInt32(); if (MonthV) { MonthV->FirstDayOfWeek = CalendarView::FirstDayOfWeek; MonthV->Invalidate(); } } void CalendarViewWnd::OnOptionsChange() { if (!CalendarView::App && CalendarViewWindows.Length() > 0) { CalendarView::App = CalendarViewWindows[0]->App; } CalendarView::OnOptionsChange(); for (auto w: CalendarViewWindows) w->OptionsChange(); } bool CalendarViewWnd::OnKey(LKey &k) { switch (k.vkey) { case 'w': case 'W': { if (k.CtrlCmd() && k.Down()) { Quit(); return true; } break; } } return LWindow::OnKey(k); } int CalendarViewWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { CalViewArrow *Va = 0; switch (Cmd) { case IDM_CAL_DAY: { Cv->SetViewMode(CAL_VIEW_DAY); break; } case IDM_CAL_WEEK: { Cv->SetViewMode(CAL_VIEW_WEEK); break; } case IDM_CAL_MONTH: { Cv->SetViewMode(CAL_VIEW_MONTH); break; } case IDM_CAL_YEAR: { Cv->SetViewMode(CAL_VIEW_YEAR); break; } case IDM_PREV: { LKey k; k.c16 = LK_PAGEUP; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_BACK: { LKey k; k.c16 = LK_PAGEUP; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_TODAY: { LDateTime Dt; Dt.SetNow(); Dt.Minutes(0); Dt.Seconds(0); Dt.Thousands(0); Cv->SetCursor(Dt); break; } case IDM_FORWARD: { LKey k; k.c16 = LK_PAGEDOWN; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_NEXT: { LKey k; k.c16 = LK_PAGEDOWN; k.Flags = LGI_EF_CTRL; Va = GetModeArrow(Cv->GetViewMode(), k); break; } case IDM_PRINT: { auto *Printer = Cv && App ? App->GetPrinter() : NULL; if (Printer) { CalendarViewPrint Cvp(Cv); Printer->Print(&Cvp, NULL, "Scribe Calendar", -1, this); } break; } case IDM_HELP: { App->LaunchHelp("calendar.html"); break; } } if (Va) { LDateTime Cursor = Cv->GetCursor(); Cursor.AddHours(Va->Hours); Cursor.AddDays(Va->Days); Cursor.AddMonths(Va->Months); Cursor.AddMonths(Va->Years * 12); Cv->SetCursor(Cursor); } return 0; } LMessage::Result CalendarViewWnd::OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHANGE: { if (m->A() == IDC_LIST) { // One of the calendar source's has changed... // Check it's still in our list and 'valid' CalendarSource *s = (CalendarSource*)m->B(); LArray All; CalLst->GetAll(All); if (All.IndexOf(s) >= 0) Cv->OnContentsChanged(s); } break; } #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(m); } LString CalendarViewWnd::LoadString(int id) { return LString(LLoadString(id)).Replace("&",""); } LString CalendarViewWnd::UnusedKey() { LString Key; // Find an unused index for the new source... while (true) { Key.Printf("%s.Source-%i", OPT_CalendarSources, LRand(1000)); if (App->GetOptions()->LockTag(Key, _FL)) App->GetOptions()->Unlock(); else break; } return Key; } int CalendarViewWnd::OnNotify(LViewI *c, LNotification n) { static bool Processing = false; if (Processing) return 0; Processing = true; switch (c->GetId()) { case IDC_CALENDAR: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t = Cv->GetCursor(); MonthV->Set(&t); MonthV->Invalidate(); } break; } case IDC_MONTH_VIEW: { if (Cv && MonthV && n.Type == LNotifyCursorChanged) { LDateTime t; t = MonthV->Get(); Cv->SetCursor(t); } break; } case IDC_LIST: { if (!CalLst) break; switch (n.Type) { default: break; case LNotifyValueChanged: { if (Cv) { auto src = dynamic_cast(CalLst->GetSelected()); if (src) Cv->OnContentsChanged(src); else LAssert(0); } break; } case LNotifyItemContextMenu: { LMouse m; GetMouse(m); m.ToScreen(); LListItem *Sel = CalLst->GetSelected(); LSubMenu s; if (Sel) { LSubMenu *ColMenu = s.AppendSub("Colour"); if (ColMenu) { BuildMarkMenu( ColMenu, MS_None, 0); } } s.AppendItem(LLoadString(IDS_ADD_LOCAL_CAL_FOLDER), IDM_ADD_LOCAL_CAL); s.AppendItem(LLoadString(IDS_ADD_CAL_URL), IDM_ADD_CAL_URL); s.AppendSeparator(); s.AppendItem(LoadString(IDS_EDIT), IDM_EDIT, Sel != NULL); s.AppendItem(LoadString(IDS_DELETE), IDM_DELETE, Sel != NULL); int Id = s.Float(this, m); switch (Id) { case IDM_ADD_LOCAL_CAL: { auto Dlg = new FolderDlg(this, App, MAGIC_CALENDAR); Dlg->DoModal([this, Dlg](auto dlg, auto ok) { if (ok) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); // Create the source... FolderCalendarSource *cs = new FolderCalendarSource(App, Parts.Last()); if (cs) { cs->SetPath(Dlg->Get()); cs->SetColour(CalendarSource::FindUnusedColour()); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dlg; }); break; } case IDM_ADD_CAL_URL: { auto dlg = new LInput(this, "", LLoadString(IDC_CAL_URL, "Calendar URL:"), AppName); dlg->DoModal([this, dlg](auto dialog, auto ok) { if (ok) { auto Key = UnusedKey(); auto Parts = Key.SplitDelimit("."); auto Url = dlg->GetStr(); // Create the source... RemoteCalendarSource *cs = new RemoteCalendarSource(App, Parts.Last()); if (cs) { cs->SetColour(CalendarSource::FindUnusedColour()); cs->SetUri(Url); CalLst->Insert(cs); // CalLst doesn't own the ptr cs->Write(); } App->SaveOptions(); } delete dialog; }); break; } case IDM_EDIT: { CalendarSource *Src = dynamic_cast(Sel); if (!Src) break; Src->EditPath(this, Cv); break; } case IDM_DELETE: { CalendarSource *Src = dynamic_cast(Sel); if (Cv && Src) { auto *Lst = Src->LListItem::GetList(); if (Lst) Lst->Remove(Src); Cv->DeleteSource(Src); } break; } default: { int Idx = Id - IDM_MARK_BASE; if (Idx >= 0 && Idx < CountOf(MarkColours32)) { LColour Mc(MarkColours32[Idx], 32); CalendarSource *Src = dynamic_cast(Sel); if (Src) { Src->SetColour(Mc); Src->Write(); } } break; } } break; } } break; } case IDC_TODO: { if (Todo && n.Type == LNotifyItemColumnClicked) { int Col = 0; LMouse m; if (Todo->GetColumnClickInfo(Col, m)) { int Sort = 0; for (int i=0; iGetColumns(); i++) { LItemColumn *c = Todo->ColumnAt(i); if (c) { if (i == Col) { if (c->DownArrow()) { c->UpArrow(true); Sort = -(i + 1); } else { c->DownArrow(true); Sort = i + 1; } } else { c->DownArrow(false); c->UpArrow(false); } } } Todo->Sort(TodoCompare, Sort); } } break; } } Processing = false; return 0; } ////////////////////////////////////////////////////////////////////////////// void OpenCalender(ScribeFolder *folder) { if (!CalendarView::CalendarViews.Length()) { new CalendarViewWnd(folder); } } void CalendarSource::FolderDelete(ScribeFolder *f) { for (auto s: AllSources) s->OnFolderDelete(f); } LColour CalendarSource::FindUnusedColour() { // MarkColours32 LArray Used; Used.Length(IDM_MARK_MAX); for (auto &s: AllSources) { auto c = s->GetColour(); uint32_t c32 = c.c32(); for (int i=0; i sources, CalendarSource::GetEventCb callback) : LView::ViewEventTarget(app, M_CALENDAR_SOURCE_STATE), Owner(owner) { *Owner = this; Sources = sources; Start = start; End = end; Callback = callback; PostEvent(M_CALENDAR_SOURCE_STATE); } CalendarSourceGetEvents::~CalendarSourceGetEvents() { *Owner = NULL; } void CalendarSourceGetEvents::OnState() { if (Sources.Length() == 0) { //LgiTrace("CalendarSourceGetEvents: finished...\n"); if (Callback) Callback(Events); delete this; } else if (auto src = Sources[0]) { Sources.DeleteAt(0); if (!App) App = src->GetApp(); //LgiTrace("CalendarSourceGetEvents: %s\n", src->ToString().Get()); src->GetEvents(Start, End, [this, src](auto events) { #ifdef _DEBUG LAssert(!GotCb.Find(src)); GotCb.Add(src, true); #endif //LgiTrace("CalendarSourceGetEvents: Callback %s %i\n", src->ToString().Get(), (int)events.Length()); Events += events; PostEvent(M_CALENDAR_SOURCE_STATE); }); } } LMessage::Result CalendarSourceGetEvents::OnEvent(LMessage *Msg) { if (Msg->Msg() == M_CALENDAR_SOURCE_STATE) OnState(); return 0; } diff --git a/src/CalendarView.h b/src/CalendarView.h --- a/src/CalendarView.h +++ b/src/CalendarView.h @@ -1,195 +1,195 @@ #ifndef __Calendar_VIEW_H #define __Calendar_VIEW_H #include "Calendar.h" #define SX(x) ((x)*Scale) #define SY(y) ((y)*Scale) #define SRect(r) ((r).x1 = (int)((double)(r).x1*Scale));\ ((r).y1 = (int)((double)(r).y1*Scale));\ ((r).x2 = (int)((double)(r).x2*Scale));\ ((r).y2 = (int)((double)(r).y2*Scale)); class CalendarView : public LLayout, public LDragDropTarget, public LDragDropSource { friend class Calendar; friend class CalendarConfig; public: struct TsRange { LTimeStamp StartTs; LTimeStamp EndTs; bool Overlap(LTimeStamp s, LTimeStamp e) { if (EndTs < s || StartTs > e) return false; return true; } bool Overlap(TsRange &r) { if (EndTs < r.StartTs || StartTs > r.EndTs) return false; return true; } }; enum EventDragMode { DragNone, // No drag operation in effect. DragNewEvent, // Selecting a region for a new event DragMoveSelection, // Moving existing events to a new time DragMoveStart, // Editing the start time of a single event DragMoveEnd, // Editing the end time of a single event DragDropObject, // Dragging an external object over the view }; protected: // Data LArray Current; LArray Selection; CalendarViewMode Mode; CalendarSourceGetEvents *GetEvents = NULL; bool SourceEventsDirty = false; // Date/Times LDateTime Cursor; LDateTime First; // of month LDateTime Start; // of visible LDateTime SingleClick; // time clicked on // Clicking and dragging interaction LPoint ClickPt; LDateTime DragStart, DragEnd; LArray Ranges; EventDragMode DragMode; uint64 LastTsOffset; Calendar *DragEvent; // Week view int DayStart, DayEnd; // DST info - LArray Dst; - LDateTime::LDstInfo *GetDstForDate(LDateTime t); + LArray Dst; + LDstInfo *GetDstForDate(LDateTime t); // Layout data LRect Title; LRect Layout; LRect PrintMargin; int MonthX, MonthY; // Display LAutoPtr Font; // Drag'n'drop Target int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) override; int OnDrop(LArray &Data, LPoint Pt, int KeyState) override; void OnDragExit() override; // Drag'n'drop Source void OnDragInit(bool Success) override; char *TypeOf(); bool GetData(LArray &Data) override; bool GetFormats(LDragFormats &Formats) override; // Internal Methods void OnSelect(Calendar *c, bool Ctrl, bool Shift); bool Overlap(LDateTime &Start, LDateTime &End, Calendar *a, Calendar *b); void SetupScroll(); void CalDelete(Calendar *c); public: static ScribeWnd *App; static int SnapMinutes; static int FirstDayOfWeek; static LArray CalendarViews; static void OnOptionsChange(); static void OnDelete(Calendar *c) { for (auto cv: CalendarViews) cv->CalDelete(c); } CalendarView(ScribeFolder *folder, int Id = -1, LRect *r = NULL, const char *Name = NULL); ~CalendarView(); const char *GetClass() override { return "CalendarView"; } // Methods Calendar *CalendarAt(int x, int y); LDateTime *TimeAt(int x, int y, int SnapMinutes, LPoint *Cell = NULL); void OnDelete(); LArray &GetSelection() { return Selection; } void SelectDropTarget(LDateTime *Start = NULL, LDateTime *End = NULL); bool GetEventsBetween(LArray &list, LDateTime Start, LDateTime End); void LoadUsers(); void DeleteSource(CalendarSource *cs); LCursor GetCursor(int x, int y) override; bool HitTest(int x, int y, EventDragMode &mode, Calendar *&event); Calendar *NewEvent(LDateTime &dtStart, LDateTime &dtEnd); // Properties CalendarViewMode GetViewMode(); void SetViewMode(CalendarViewMode m); LDateTime &GetCursor(); void SetCursor(LDateTime &c); // Overridable virtual void OnContentsChanged(CalendarSource *s = NULL); virtual void OnSourceDelete(CalendarSource *s); virtual void OnCursorChange(bool Day = true, bool Month = true, bool Year = true); // Events void DrawSelectionBox(LSurface *pDC, LRect &r); void OnPaint(LSurface *pDC) override; bool OnPrintPage(LPrintDC *pDC, int PageIndex); void OnMouseClick(LMouse &m) override; void OnMouseMove(LMouse &m) override; bool OnMouseWheel(double Lines) override; void OnFocus(bool f) override; bool OnKey(LKey &k) override; void OnCreate() override; void OnPulse() override; int OnNotify(LViewI *v, LNotification n) override; bool OnLayout(LViewLayoutInfo &Inf) override; LMessage::Result OnEvent(LMessage *Msg) override; }; class CalendarViewWnd : public LWindow { ScribeWnd *App; CalendarView *Cv; LSplitter *Split; LList *Todo; class LMonthView *MonthV; LBox *HorBox, *VerBox; void OptionsChange(); public: LList *CalLst; CalendarViewWnd(ScribeFolder *folder); ~CalendarViewWnd(); LString LoadString(int id); LString UnusedKey(); bool OnKey(LKey &k); int OnCommand(int Cmd, int Event, OsView WndHandle); LMessage::Result OnEvent(LMessage *m); int OnNotify(LViewI *c, LNotification n); static void OnOptionsChange(); }; #endif diff --git a/src/ScribeContact.cpp b/src/ScribeContact.cpp --- a/src/ScribeContact.cpp +++ b/src/ScribeContact.cpp @@ -1,2382 +1,2380 @@ /* ** FILE: ScribeContact.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe contact object and UI ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include "Scribe.h" #include "ScribePageSetup.h" #include "lgi/common/Map.h" #include "lgi/common/vCard-vCal.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/TabView.h" #include "lgi/common/TableLayout.h" #include "lgi/common/DisplayString.h" #include "lgi/common/Button.h" #include "lgi/common/GdcTools.h" #include "lgi/common/DropFiles.h" #include "lgi/common/Http.h" #include "lgi/common/LgiRes.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/List.h" #include "lgi/common/Json.h" #include "lgi/common/CssTools.h" #include "lgi/common/FileSelect.h" #include "PrintContext.h" #include "resdefs.h" #include "resource.h" bool ConvertImageToContactSize(LAutoPtr &Img, int Px, LSurface *Raw) { if (!Raw) { LAssert(!"No raw image?"); return false; } if (!Img.Reset(new LMemDC(Px, Px, Raw->GetColourSpace()))) { LAssert(!"Failed to create image?"); return false; } LRect Src; if (Raw->X() > Raw->Y()) { // Wider than high Src.ZOff(Raw->Y()-1, Raw->Y()-1); Src.Offset((Raw->X()-Raw->Y())>>1, 0); } else if (Raw->X() < Raw->Y()) { Src.ZOff(Raw->X()-1, Raw->X()-1); Src.Offset(0, (Raw->Y()-Raw->X())>>1); } else { Src = Raw->Bounds(); } ResampleDC(Img, Raw, &Src); return true; } class ContactPriv { public: Contact *Item; char *Scratch; List Plugins; LAutoPtr Image; LString ImagePath; LString DateCache; LAutoPtr CustomCache; ContactPriv(Contact *c) : Item(c) { Scratch = 0; } ~ContactPriv() { DeleteArray(Scratch); Plugins.DeleteArrays(); } LJson *GetJson() { auto Obj = Item->GetObject(); if (!Obj) return NULL; if (!CustomCache) { auto json = Obj->GetStr(FIELD_CONTACT_JSON); CustomCache.Reset(new LJson(json)); } return CustomCache; } }; ItemFieldDef ContactFieldDefs[] = { // Standard fields {"Title", SdTitle, GV_STRING, FIELD_TITLE, IDC_TITLE, OPT_Title}, {"First", SdFirst, GV_STRING, FIELD_FIRST_NAME, IDC_FIRST, OPT_First}, {"Last", SdSurname, GV_STRING, FIELD_LAST_NAME, IDC_LAST, OPT_Last}, {"Email", SdEmail, GV_STRING, FIELD_EMAIL, IDC_EMAIL, OPT_Email}, {"Nickname", SdNickname, GV_STRING, FIELD_NICK, IDC_NICK, OPT_Nick}, {"Spouse", SdSpouse, GV_STRING, FIELD_SPOUSE, IDC_SPOUSE, OPT_Spouse}, {"Notes", SdNotes, GV_STRING, FIELD_NOTE, IDC_NOTE, OPT_Note}, {"Uid", SdUid, GV_INT32, FIELD_UID, -1, OPT_Uid}, {"TimeZone", SdTimeZone, GV_STRING, FIELD_TIMEZONE, IDC_TIMEZONE, OPT_TimeZone}, // Home fields {"Home Street", SdHomeStreet, GV_STRING, FIELD_HOME_STREET, IDC_HOME_STREET, OPT_HomeStreet}, {"Home Suburb", SdHomeSuburb, GV_STRING, FIELD_HOME_SUBURB, IDC_HOME_SUBURB, OPT_HomeSuburb}, {"Home Postcode", SdHomePostcode, GV_STRING, FIELD_HOME_POSTCODE, IDC_HOME_POSTCODE, OPT_HomePostcode}, {"Home State", SdHomeState, GV_STRING, FIELD_HOME_STATE, IDC_HOME_STATE, OPT_HomeState}, {"Home Country", SdHomeCountry, GV_STRING, FIELD_HOME_COUNTRY, IDC_HOME_COUNTRY, OPT_HomeCountry}, {"Home Phone", SdHomePhone, GV_STRING, FIELD_HOME_PHONE, IDC_HOME_PHONE, OPT_HomePhone}, {"Home Mobile", SdHomeMobile, GV_STRING, FIELD_HOME_MOBILE, IDC_HOME_MOBILE, OPT_HomeMobile}, {"Home IM#", SdHomeIM, GV_STRING, FIELD_HOME_IM, IDC_HOME_IM, OPT_HomeIM}, {"Home Fax", SdHomeFax, GV_STRING, FIELD_HOME_FAX, IDC_HOME_FAX, OPT_HomeFax}, {"Home Webpage", SdHomeWebpage, GV_STRING, FIELD_HOME_WEBPAGE, IDC_HOME_WEBPAGE, OPT_HomeWebPage}, // Work fields {"Work Street", SdWorkStreet, GV_STRING, FIELD_WORK_STREET, IDC_WORK_STREET, OPT_WorkStreet}, {"Work Suburb", SdWorkSuburb, GV_STRING, FIELD_WORK_SUBURB, IDC_WORK_SUBURB, OPT_WorkSuburb}, {"Work Postcode", SdWorkPostcode, GV_STRING, FIELD_WORK_POSTCODE, IDC_WORK_POSTCODE, OPT_WorkPostcode}, {"Work State", SdWorkState, GV_STRING, FIELD_WORK_STATE, IDC_WORK_STATE, OPT_WorkState}, {"Work Country", SdWorkCountry, GV_STRING, FIELD_WORK_COUNTRY, IDC_WORK_COUNTRY, OPT_WorkCountry}, {"Work Phone", SdWorkPhone, GV_STRING, FIELD_WORK_PHONE, IDC_WORK_PHONE, OPT_WorkPhone}, {"Work Mobile", SdWorkMobile, GV_STRING, FIELD_WORK_MOBILE, IDC_WORK_MOBILE, OPT_WorkMobile}, {"Work IM#", SdWorkIM, GV_STRING, FIELD_WORK_IM, IDC_WORK_IM, OPT_WorkIM}, {"Work Fax", SdWorkFax, GV_STRING, FIELD_WORK_FAX, IDC_WORK_FAX, OPT_WorkFax}, {"Company", SdCompany, GV_STRING, FIELD_COMPANY, IDC_COMPANY, OPT_Company}, {"Work Webpage", SdWorkWebpage, GV_STRING, FIELD_WORK_WEBPAGE, IDC_WORK_WEBPAGE, OPT_WorkWebPage}, {"CustomFields", SdCustomFields, GV_STRING, FIELD_CONTACT_JSON, IDC_CUSTOM_FIELDS, OPT_CustomFields}, {"DateModified", SdDateModified, GV_DATETIME, FIELD_DATE_MODIFIED, -1, NULL}, {0} }; #define ForAllContactFields(var) ItemFieldDef *var = 0; for (var = ContactFieldDefs; var->Option && var->FieldId; var++) #define MacroAllFields(Macro) \ Macro(FIELD_TITLE, OPT_Title); \ Macro(FIELD_FIRST_NAME, OPT_First); \ Macro(FIELD_LAST_NAME, OPT_Last); \ Macro(FIELD_EMAIL, OPT_Email); \ Macro(FIELD_NICK, OPT_Nick); \ Macro(FIELD_SPOUSE, OPT_Spouse); \ Macro(FIELD_NOTE, OPT_Note); \ Macro(FIELD_TIMEZONE, OPT_TimeZone); \ Macro(FIELD_HOME_STREET, OPT_HomeStreet); \ Macro(FIELD_HOME_SUBURB, OPT_HomeSuburb); \ Macro(FIELD_HOME_POSTCODE, OPT_HomePostcode); \ Macro(FIELD_HOME_STATE, OPT_HomeState); \ Macro(FIELD_HOME_COUNTRY, OPT_HomeCountry); \ Macro(FIELD_HOME_PHONE, OPT_HomePhone); \ Macro(FIELD_HOME_MOBILE, OPT_HomeMobile); \ Macro(FIELD_HOME_IM, OPT_HomeIM); \ Macro(FIELD_HOME_FAX, OPT_HomeFax); \ Macro(FIELD_HOME_WEBPAGE, OPT_HomeWebPage); \ Macro(FIELD_WORK_STREET, OPT_WorkStreet); \ Macro(FIELD_WORK_SUBURB, OPT_WorkSuburb); \ Macro(FIELD_WORK_POSTCODE, OPT_WorkPostcode); \ Macro(FIELD_WORK_STATE, OPT_WorkState); \ Macro(FIELD_WORK_COUNTRY, OPT_WorkCountry); \ Macro(FIELD_WORK_PHONE, OPT_WorkPhone); \ Macro(FIELD_WORK_MOBILE, OPT_WorkMobile); \ Macro(FIELD_WORK_IM, OPT_WorkIM); \ Macro(FIELD_WORK_FAX, OPT_WorkFax); \ Macro(FIELD_WORK_WEBPAGE, OPT_WorkWebPage); \ Macro(FIELD_COMPANY, OPT_Company); \ Macro(FIELD_CONTACT_JSON, OPT_CustomFields); ////////////////////////////////////////////////////////////////////////////// const char *WinFmtUrl = "UniformResourceLocatorW"; const char *FmtUrlList = "text/uri-list"; #include "lgi/common/SkinEngine.h" class LContactBtn : public LView { bool Down; bool Over; public: LContactBtn(int id) { SetId(id); Over = false; Down = false; LRect r(0, 0, 23, 23); SetPos(r); } ~LContactBtn() { } void OnMouseClick(LMouse &m) { Capture(Down = m.Down()); Invalidate(); if (m.Down()) Over = true; else if (Over) SendNotify(); } void OnMouseMove(LMouse &m) { if (IsCapturing()) { bool o = GetClient().Overlap(m.x, m.y); if (o ^ Over) { Down = Over = o; Invalidate(); } } } void OnPaint(LSurface *pDC) { LRect c = GetClient(); if (LApp::SkinEngine && c.X() && c.Y()) { LCssTools Tools(this); LColour Base = Tools.GetBack(); LMemDC Mem(c.X(), c.Y(), System32BitColourSpace); Mem.Colour(0, 32); Mem.Rectangle(); LRect r(0, 0, X()-1, Y()-1); LApp::SkinEngine->DrawBtn(&Mem, r, Base, Down, true); int Dot = 2; int Gap = 3; int Sz = (Dot * 3) + (Gap * 2); int x = ((X()-Sz) >> 1) + Down; int y = (Y() / 2) + Down; Mem.Colour(L_TEXT); for (int i=0; i<3; i++) { Mem.Rectangle(x, y, x + 1, y + 1); x += Dot + Gap; } pDC->Op(GDC_ALPHA); pDC->Blt(0, 0, &Mem); } } }; class LContactImage : public LView, public ResObject, public LDragDropTarget { friend class ContactUi; constexpr static int M_IMAGE_LOADED = M_USER + 100; ScribeWnd *App = NULL; Contact *c = NULL; LContactBtn *Btn = NULL; bool IsNoFace = false; // This is the uncompressed bitmap which is not saved. LAutoPtr Img; // This is a compressed version that is saved. LVariant CompressedImg; class UriLoader : public LThread, public LCancel { LContactImage *Ci; LString Uri; public: bool Status; LMemStream Data; LString Error; UriLoader(LContactImage *ci, LString uri) : LThread("LContactImage"), Data(1024) { Ci = ci; Uri = uri; Status = false; Run(); } ~UriLoader() { while (!IsExited()) LSleep(1); } int Main() { Status = LgiGetUri(this, &Data, &Error, Uri, NULL, NULL); Ci->PostEvent(M_IMAGE_LOADED); return 0; } }; LAutoPtr Worker; public: LContactImage() : ResObject(Res_Custom) { SetTabStop(true); } LVariant &GetImage() { if (!IsNoFace && CompressedImg.Type != GV_BINARY) { LMemStream m(4 << 10); if (GdcD->Save(&m, Img, "icon.jpg")) { CompressedImg.Type = GV_BINARY; auto &Bin = CompressedImg.Value.Binary; Bin.Length = (ssize_t) m.GetSize(); Bin.Data = new char[Bin.Length]; m.SetPos(0); m.Read(Bin.Data, Bin.Length); } } return CompressedImg; } bool OnKey(LKey &k) { switch (k.vkey) { case 'v': case 'V': { if (k.Ctrl()) { // Paste shortcut handler: if (k.Down()) { CompressedImg.Empty(); LClipBoard c(this); IsNoFace = false; c.Bitmap([this](auto bmp, auto str) { SetImage(bmp); }); } return true; } break; } case LK_DELETE: { // Delete the image... CompressedImg.Empty(); SetDefaultNoFace(); break; } } return false; } void OnMouseClick(LMouse &m) { if (m.Down()) Focus(true); } void SetImage(LAutoPtr Raw) { if (Raw && (Raw->X() != 160 || Raw->Y() != 160)) ConvertImageToContactSize(Img, 160, Raw); else Img = Raw; Invalidate(); } void OnCreate() { SetWindow(this); } bool OnLayout(LViewLayoutInfo &Inf) { if (!Inf.Width.Min) Inf.Width.Min = Inf.Width.Max = 160; else Inf.Height.Min = Inf.Height.Max = 160; return true; } void OnPaint(LSurface *pDC) { LRegion rgn(GetClient()); if (Img) { pDC->Blt(0, 0, Img); LRect Bnds = Img->Bounds(); rgn.Subtract(&Bnds); } for (int i=0; iColour(L_LOW); pDC->Rectangle(rgn[i]); } if (Btn) { LRect c = GetClient(); c.Inset(6, 6); LRect p = Btn->GetPos(); p.Offset(c.x2 - p.X() - p.x1 + 1, c.y1 - p.y1); Btn->SetPos(p); } if (Focus()) { auto c = GetClient(); PatternBox(pDC, LRect(c.x1, c.y1, c.x1+1, c.y2)); PatternBox(pDC, LRect(c.x2-1, c.y1, c.x2, c.y2)); PatternBox(pDC, LRect(c.x1+2, c.y1, c.x2-2, c.y1+1)); PatternBox(pDC, LRect(c.x1+2, c.y2-1, c.x2-2, c.y2)); } } void SetDefaultNoFace() { if (App) { LVariant NoFace; if (App->GetValue("NoContact.Image[160]", NoFace)) { if (NoFace.Str()) { IsNoFace = Img.Reset(GdcD->Load(NoFace.Str())); Invalidate(); } } } } void SetContact(Contact *contact) { c = contact; if (c) { App = c->App; SetDefaultNoFace(); if (!Btn) { Btn = new LContactBtn(100); } if (Btn) { #if 0 Btn->GetCss(true)->NoPaintColor(LCss::ColorDef(LCss::ColorRgb, Rgb32(0xcb,0xcb,0xcb))); #else Btn->GetCss(true)->NoPaintColor(LCss::ColorDef(LCss::ColorTransparent)); #endif AddView(Btn); } } } bool SetImage(const LVariant &v, const char *FileHint = NULL) { if (v.Type != GV_BINARY) return false; CompressedImg = v; LMemStream mem( CompressedImg.Value.Binary.Data, CompressedImg.Value.Binary.Length, false); LAutoPtr i(GdcD->Load(&mem, FileHint)); if (!i) { CompressedImg.Empty(); return false; } SetImage(i); IsNoFace = false; return true; } bool Load(const char *File) { LFile f; if (!f.Open(File, O_READ)) { LgiMsg(this, "Couldn't open image file.", AppName, MB_OK); return false; } int Sz = (int)f.GetSize(); LAutoPtr p(new uint8_t[Sz]); if (!p) { LgiMsg(this, "Couldn't alloc mem for image.", AppName, MB_OK); return false; } ssize_t rd = f.Read(p, Sz); if (rd <= 0) { LgiMsg(this, "Couldn't read from image file.", AppName, MB_OK); return false; } LVariant v; v.SetBinary(rd, p.Release(), true); if (!SetImage(v, File)) { LgiMsg(this, "Couldn't decompress image.", AppName, MB_OK); return false; } IsNoFace = false; return true; } LMessage::Param OnEvent(LMessage *Msg) { if (Msg->Msg() == M_IMAGE_LOADED) { if (Worker && Worker->Status) { LAutoPtr pDC(GdcD->Load(&Worker->Data, NULL)); if (pDC) { IsNoFace = false; SetImage(pDC); } } Worker.Reset(); return 0; } return LView::OnEvent(Msg); } int OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == 100) { auto s = new LFileSelect(this); s->Open([this](auto dlg, auto status) { if (status) Load(dlg->Name()); delete dlg; }); } return 0; } int WillAccept(LDragFormats &Formats, LPoint Pt, int KeyState) { if (Formats.HasFormat(LGI_FileDropFormat)) Formats.SupportsFileDrops(); else { #if WINDOWS Formats.Supports(WinFmtUrl); #endif Formats.Supports(FmtUrlList); } return Formats.GetSupported().Length() ? DROPEFFECT_COPY : DROPEFFECT_NONE; } int OnDrop(LArray &Data, LPoint Pt, int KeyState) { for (unsigned i=0; i 0) { Load(files[0]); return DROPEFFECT_COPY; } } else if (dd.IsFormat(FmtUrlList) || dd.IsFormat(WinFmtUrl)) { auto &v = dd.Data[0]; if (v.Type == GV_BINARY) { LString uri = (char16*)v.Value.Binary.Data; if (uri) Worker.Reset(new UriLoader(this, uri)); } else if (v.Type == GV_STRING) { Worker.Reset(new UriLoader(this, v.Str())); } else LAssert(!"Unexpected type."); } } return DROPEFFECT_NONE; } }; class LContactImageFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (Class && !_stricmp(Class, "LContactImage")) return new LContactImage; return NULL; } } ContactImageFactory; ////////////////////////////////////////////////////////////////////////////// class LFieldItem : public LListItem { public: LString Field, Value; const char *GetText(int i) { switch (i) { case 0: return Field; case 1: return Value; } return NULL; } bool SetText(const char *s, int i=0) { switch (i) { case 0: Field = s; break; case 1: Value = s; break; default: return false; } LListItem::SetText(s, i); auto Lst = GetList(); Lst->ResizeColumnsToContent(); Lst->OnNotify(Lst, LNotifyItemChange); return true; } void OnMouseClick(LMouse &m) { if (m.Down() && m.Double()) { int Col = GetList()->ColumnAtX(m.x); if (Col >= 0 && Col <= 1) EditLabel(Col); } } }; class LFieldEditor : public LList { LString u; LAutoWString w; public: LFieldEditor() : LList(IDC_STATIC) { SetObjectName(Res_Custom); DrawGridLines(true); AllowEditLabels(true); SetNotify(this); AddColumn("Field", 110); AddColumn("Value", 110); OnChange(); } void OnChange() { LArray a; GetAll(a); for (auto i: a) if (!i->Field && !i->Value) return; Insert(new LFieldItem); } int OnNotify(LViewI *Ctrl, LNotification n) { if (Ctrl->GetId() == GetId()) { switch (n.Type) { case LNotifyItemChange: { OnChange(); break; } case LNotifyDeleteKey: { LArray a; GetSelection(a); a.DeleteObjects(); OnChange(); break; } } } return LList::OnNotify(Ctrl, n); } bool NameW(const char16 *n) { LAutoString a(WideToUtf8(n)); return Name(a); } const char16 *NameW() { w.Reset(Utf8ToWide(Name())); return w; } bool Name(const char *n) { Empty(); LJson j(n); LArray keys = j.GetKeys(); for (auto k: keys) { LFieldItem *i = new LFieldItem; i->Field = k; i->Value = j.Get(k); Insert(i); } OnChange(); // LgiTrace("SetText: %s\n", n); return true; } const char *Name() { LJson j; LArray items; GetAll(items); for (auto i: items) { if (i->Field) j.Set(i->Field, i->Value); } u = j.GetJson(); // LgiTrace("GetText: %s\n", u.Get()); return u; } }; class LFieldEditorFactory : public LViewFactory { LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (Class && !_stricmp(Class, "LFieldEditor")) return new LFieldEditor; return NULL; } } FieldEditorFactory; ////////////////////////////////////////////////////////////////////////////// List Contact::Everyone; LHashTbl, int> Contact::PropMap; Contact::Contact(ScribeWnd *app, LDataI *object) : Thing(app, object) { if (!PropMap.Length()) { PropMap.Add("Type", FIELD_TYPE); ForAllContactFields(Fld) { LAssert(Fld->FieldId > 0 && Fld->FieldId < FIELD_MAX); // LgiTrace("AddProp %i, %s\n", Fld->FieldId, Fld->Option); PropMap.Add(Fld->Option, Fld->FieldId); } } DefaultObject(object); d = new ContactPriv(this); Everyone.Insert(this); } Contact::~Contact() { Everyone.Delete(this); DeleteObj(Ui); DeleteObj(d); } LString Contact::GetLocalTime(const char *TimeZone) { LString Status; if (!ValidStr(TimeZone)) { Get(OPT_TimeZone, TimeZone); } if (ValidStr(TimeZone)) { double TheirTz = atof(TimeZone); LDateTime d; d.SetNow(); d.SetTimeZone((int)(TheirTz * 60), true); Status = d.Get(); } return Status; } Contact *Contact::LookupEmail(const char *Email) { if (!Email) return NULL; for (auto c : Everyone) if (c->HasEmail(Email)) return c; return NULL; } bool Contact::Get(const char *Opt, int &Value) { if (GetObject()) { int Id = PropMap.Find(Opt); if (Id) { int i = (int)GetObject()->GetInt(Id); if (i >= 0) { Value = i; return true; } } } return false; } bool Contact::Get(const char *Opt, const char *&Value) { if (GetObject()) { int Id = PropMap.Find(Opt); if (Id) { Value = GetObject()->GetStr(Id); return Value != 0; } } return false; } bool Contact::Set(const char *Opt, int Value) { if (GetObject()) { int Id = PropMap.Find(Opt); if (Id) { return GetObject()->SetInt(Id, Value) > Store3Error; } } return false; } bool Contact::Set(const char *Opt, const char *Value) { auto o = GetObject(); if (!o) return false; auto Id = PropMap.Find(Opt); if (Id <= 0 || Id >= FIELD_MAX) { LAssert(!"Invalid ID."); return false; } return o->SetStr(Id, Value) > Store3Error; } int Contact::GetAddrCount() { if (!GetObject()) return 0; auto DefEmail = GetObject()->GetStr(FIELD_EMAIL); auto AltEmail = GetObject()->GetStr(FIELD_ALT_EMAIL); int c = ValidStr(DefEmail) ? 1 : 0; if (AltEmail) { c++; for (auto s = AltEmail; s && *s; s++) { if (*s == ',') c++; } } return c; } LString::Array Contact::GetEmails() { LString::Array e; const char *s; if (Get(OPT_Email, s) && s) e.New() = s; if (GetObject()) { auto alt = LString(GetObject()->GetStr(FIELD_ALT_EMAIL)).Split(","); if (alt.Length()) e += alt; } return e; } bool Contact::HasEmail(LString email) { auto Emails = GetEmails(); for (auto e: Emails) if (e.Equals(email)) return true; return false; } LString Contact::GetAddrAt(int i) { if (i == 0) { const char *e; if (Get(OPT_Email, e)) return e; } else if (i > 0) { auto t = LString(GetObject()->GetStr(FIELD_ALT_EMAIL)).SplitDelimit(","); if (i <= (int)t.Length()) return t[i-1]; } return LString(); } bool Contact::SetVariant(const char *Name, LVariant &Value, const char *Array) { auto Obj = GetObject(); if (!Name || !Obj) return false; // Check normal fields.. ScribeDomType Fld = StrToDom(Name); if (Fld == SdDateModified) return SetDateField(FIELD_DATE_MODIFIED, Value); int Id = PropMap.Find(Name); if (Id) { // Pre-defined field auto r = Obj->SetStr(Id, Value.CastString()); if (r > Store3Error) { SetDirty(); return true; } } else { // Custom field... auto j = d->GetJson(); if (!j) return false; if (!j->Set(Name, Value.CastString())) return false; if (!Obj->SetStr(FIELD_CONTACT_JSON, j->GetJson())) return false; SetDirty(); return true; } return false; } bool Contact::GetVariant(const char *Name, LVariant &Value, const char *Array) { // Check scribe level fields... ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; return true; } case SdFolder: // Type: ScribeFolder { ScribeFolder *f = GetFolder(); if (!f) return false; Value = (LDom*)f; return true; } default: break; } auto Obj = GetObject(); if (!Obj) return false; switch (Fld) { case SdEmail: // Type: String[] { if (!Value.SetList()) return false; int c = GetAddrCount(); for (int i=0; iInsert(new LVariant(e)); } return true; } case SdType: // Type: Int32 { Value = Obj->Type(); return true; } case SdGroups: // Type: String[] { LHashTbl,bool> Emails; for (int i=0; i Grps; auto Srcs = App->GetThingSources(MAGIC_GROUP); for (auto g: Srcs) { for (auto t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (Grp) { List Addr; if (Grp->GetAddresses(Addr)) { for (auto a: Addr) { if (Emails.Find(a)) { auto Name = Grp->GetFieldText(FIELD_GROUP_NAME); if (Name) { Grps.Insert(new LVariant(Name)); } } } Addr.DeleteArrays(); } } } } Value.SetList(&Grps); return true; } case SdImage: // Type: Image { LAssert(!"Impl."); return false; } case SdImageHtml: // Type: String { if (!d->ImagePath) { int Px = 80; if (Array) { int i = atoi(Array); if (i > 0) Px = i; } // Check to see if the contact has an image... const LVariant *Bin = Obj->GetVar(FIELD_CONTACT_IMAGE); if (Bin && Bin->Type != GV_NULL) { if (Bin->Type == GV_BINARY) { auto Png = LFilterFactory::New("name.png", O_WRITE, NULL); if (Png) { if (!d->Image) { LMemStream Mem(Bin->Value.Binary.Data, Bin->Value.Binary.Length, false); LAutoPtr Raw(GdcD->Load(&Mem)); if (Raw) { ConvertImageToContactSize(d->Image, Px, Raw); } } if (d->Image) { LFile::Path p(ScribeTempPath()); const char *First = Obj->GetStr(FIELD_FIRST_NAME); const char *Last = Obj->GetStr(FIELD_LAST_NAME); LString leaf; leaf.Printf("%s%sContact.png", First, Last); p += leaf; LFile f; if (f.Open(p, O_WRITE)) { if (Png->WriteImage(&f, d->Image)) { #if 0 // We could save the resized version here? int64 NewSize = f.GetSize(); if (Bin->Length() > (50 << 10)) { int asd=0; } #endif d->ImagePath = p.GetFull(); } } } } } else LAssert(0); } } if (d->ImagePath) { LString html; html.Printf("\n", d->ImagePath.Get()); Value = html; return true; } return App->GetValue("NoContact.ImageHtml", Value); } case SdTitle: // Type: String Value = Obj->GetStr(FIELD_TITLE); return true; case SdFirst: // Type: String Value = Obj->GetStr(FIELD_FIRST_NAME); return true; case SdSurname: // Type: String Value = Obj->GetStr(FIELD_LAST_NAME); return true; case SdNickname: // Type: String Value = Obj->GetStr(FIELD_NICK); return true; case SdSpouse: // Type: String Value = Obj->GetStr(FIELD_SPOUSE); return true; case SdNotes: // Type: String Value = Obj->GetStr(FIELD_NOTE); return true; case SdUid: // Type: Int64 Value = Obj->GetInt(FIELD_UID); return true; case SdTimeZone: // Type: String Value = Obj->GetStr(FIELD_TIMEZONE); return true; case SdHomeStreet: // Type: String Value = Obj->GetStr(FIELD_HOME_STREET); return true; case SdHomeSuburb: // Type: String Value = Obj->GetStr(FIELD_HOME_SUBURB); return true; case SdHomePostcode: // Type: String Value = Obj->GetStr(FIELD_HOME_POSTCODE); return true; case SdHomeState: // Type: String Value = Obj->GetStr(FIELD_HOME_STATE); return true; case SdHomeCountry: // Type: String Value = Obj->GetStr(FIELD_HOME_COUNTRY); return true; case SdHomePhone: // Type: String Value = Obj->GetStr(FIELD_HOME_PHONE); return true; case SdHomeMobile: // Type: String Value = Obj->GetStr(FIELD_HOME_MOBILE); return true; case SdHomeIM: // Type: String Value = Obj->GetStr(FIELD_HOME_IM); return true; case SdHomeFax: // Type: String Value = Obj->GetStr(FIELD_HOME_FAX); return true; case SdHomeWebpage: // Type: String Value = Obj->GetStr(FIELD_HOME_WEBPAGE); return true; case SdWorkStreet: // Type: String Value = Obj->GetStr(FIELD_WORK_STREET); return true; case SdWorkSuburb: // Type: String Value = Obj->GetStr(FIELD_WORK_SUBURB); return true; case SdWorkPostcode: // Type: String Value = Obj->GetStr(FIELD_WORK_POSTCODE); return true; case SdWorkState: // Type: String Value = Obj->GetStr(FIELD_WORK_STATE); return true; case SdWorkCountry: // Type: String Value = Obj->GetStr(FIELD_WORK_COUNTRY); return true; case SdWorkPhone: // Type: String Value = Obj->GetStr(FIELD_WORK_POSTCODE); return true; case SdWorkMobile: // Type: String Value = Obj->GetStr(FIELD_WORK_MOBILE); return true; case SdWorkIM: // Type: String Value = Obj->GetStr(FIELD_WORK_IM); return true; case SdWorkFax: // Type: String Value = Obj->GetStr(FIELD_WORK_FAX); return true; case SdCompany: // Type: String Value = Obj->GetStr(FIELD_COMPANY); return true; case SdWorkWebpage: // Type: String Value = Obj->GetStr(FIELD_WORK_WEBPAGE); return true; case SdDateModified: // Type: DateTime return GetDateField(FIELD_DATE_MODIFIED, Value); default: { // Check custom fields.. if (!d->CustomCache) { auto json = Obj->GetStr(FIELD_CONTACT_JSON); d->CustomCache.Reset(new LJson(json)); } if (!d->CustomCache) return false; Value = d->CustomCache->Get(Name); return true; } } return false; } bool Contact::CallMethod(const char *MethodName, LScriptArguments &Args) { return Thing::CallMethod(MethodName, Args); } Thing &Contact::operator =(Thing &c) { Contact *Arg = c.IsContact(); if (GetObject() && Arg && Arg->GetObject()) { GetObject()->CopyProps(*Arg->GetObject()); } return *this; } bool Contact::IsAssociatedWith(char *PluginName) { if (PluginName) { for (auto p: d->Plugins) { if (_stricmp(p, PluginName) == 0) { return true; } } } return false; } int Contact::Compare(LListItem *Arg, ssize_t FieldId) { Contact *c1 = this; Contact *c2 = dynamic_cast(Arg); if (c1 && c2) { static int Fields[] = {FIELD_FIRST_NAME, FIELD_LAST_NAME, FIELD_EMAIL}; if (FieldId < 0) { int i = -(int)FieldId - 1; if (i >= 0 && i < CountOf(Fields)) { FieldId = Fields[i]; } } auto s1 = c1->GetObject()->GetStr((int)FieldId); auto s2 = c2->GetObject()->GetStr((int)FieldId); const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } return 0; } ThingUi *Contact::DoUI(MailContainer *c) { if (!Ui) Ui = new ContactUi(this); return Ui; } int Contact::DefaultContactFields[] = { FIELD_FIRST_NAME, FIELD_LAST_NAME, FIELD_EMAIL, 0 }; int *Contact::GetDefaultFields() { return DefaultContactFields; } const char *Contact::GetFieldText(int Field) { const char *Status = NULL; #define GetFldText(Id, Opt) \ case Id: Get(Opt, Status); break; switch (Field) { MacroAllFields(GetFldText); case FIELD_DATE_MODIFIED: { auto Dt = GetObject()->GetDate(Field); if (Dt) d->DateCache = Dt->Local().Get(); else d->DateCache = LLoadString(IDS_NONE); return d->DateCache; break; } case FIELD_PLUGIN_ASSOC: { static char Str[256]; Str[0] = 0; for (auto p: d->Plugins) { if (Str[0]) strcat(Str, ", "); strcat(Str, p); } return Str; break; } } return Status; } const char *Contact::GetText(int i) { if (FieldArray.Length()) return GetFieldText(FieldArray[i]); return GetFieldText(DefaultContactFields[i]); } void Contact::OnMouseClick(LMouse &m) { if (m.IsContextMenu()) { LScriptUi s(new LSubMenu); if (s.Sub) { bool Sep = false; const char *Email; if (Get(OPT_Email, Email) && ValidStr(Email)) { char Str[256]; const char *First = 0, *Last = 0; Get(OPT_First, First); Get(OPT_Last, Last); char *LiteralEmail = NewStr(AddAmp(LLoadString(IDS_EMAIL), 'e')); if (First || Last) { sprintf_s(Str, sizeof(Str), "%s %s %s", LiteralEmail, (First) ? First : "", (Last) ? Last : ""); } else { sprintf_s(Str, sizeof(Str), "%s %s", LiteralEmail, Email); } DeleteArray(LiteralEmail); s.Sub->AppendItem(Str, IDM_NEW_EMAIL, true); Sep = true; } const char *Web = 0; if (Get(OPT_HomeWebPage, Web) && ValidStr(Web)) { s.Sub->AppendItem(Web, IDM_LOAD, true); Sep = true; } if (Sep) { s.Sub->AppendSeparator(); } s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) { App->ExecuteScriptCallback(*c, Args); } Args.DeleteObjects(); } if (GetList()->GetMouse(m, true)) { int Result; switch (Result = s.Sub->Float(GetList(), m.x, m.y)) { case IDM_NEW_EMAIL: { App->CreateMail(this); break; } case IDM_LOAD: { if (Web) LExecute(Web, 0, 0); break; } case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Del)) { for (auto It = Del.rbegin(); It != Del.end(); It--) { auto c = dynamic_cast(*It); if (c) c->OnDelete(); } ParentList->Invalidate(); } } break; } case IDM_EXPORT: { ExportAll(GetList(), sMimeVCard, NULL); break; } default: { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } break; } } } DeleteObj(s.Sub); } } else if (m.Left()) { if (m.Double()) { DoUI(); } } } bool Contact::Save(ScribeFolder *Folder) { bool Status = false; if (!Folder) { Folder = GetFolder(); } if (!Folder && App) { Folder = App->GetFolder(FOLDER_CONTACTS); } if (Folder) { LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); Status = Folder->WriteThing(this) != Store3Error; if (Status) SetDirty(false); } return Status; } bool Contact::GetFormats(bool Export, LString::Array &MimeTypes) { if (!Export) MimeTypes.Add(sMimeVCard); return MimeTypes.Length() > 0; } Thing::IoProgress Contact::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCard)) IoProgressNotImpl(); VCard vCard; if (!vCard.Import(GetObject(), stream)) IoProgressError("vCard import failed."); IoProgressSuccess(); } Thing::IoProgress Contact::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeVCard)) IoProgressNotImpl(); VCard vCard; if (!vCard.Export(GetObject(), stream)) IoProgressError("vCard export failed."); IoProgressSuccess(); } char *Contact::GetDropFileName() { if (!DropFileName) { char File[64] = "Contact"; const char *First = 0, *Last = 0; Get(OPT_First, First); Get(OPT_Last, Last); if (First && Last) sprintf_s(File, sizeof(File), "%s-%s", First, Last); else if (First) strcpy_s(File, sizeof(File), First); else if (Last) strcpy_s(File, sizeof(File), Last); DropFileName.Reset(MakeFileName(File, "vcf")); } return DropFileName; } bool Contact::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), sMimeVCard); } } if (LFileExists(DropFileName)) { Files.Add(DropFileName.Get()); Status = true; } } return Status; } void Contact::OnPrintHeaders(struct ScribePrintContext &Context) { LDisplayString *ds = Context.Text(LLoadString(IDS_CONTACT)); 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 Contact::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Print document ForAllContactFields(Fld) { const char *Value = 0; if (Get(Fld->Option, Value)) { LString v = Value; LString::Array Lines = v.SplitDelimit("\n"); int x = 0; for (unsigned i=0; iFieldId); f.Printf("%s: ", Name ? Name : Fld->DisplayText); LDisplayString *ds = Context.Text(f); if (ds) { x = ds->X(); Context.CurrentY -= ds->Y(); Context.Text(Lines[i], x); } } } } } } const char *ToField(int f) { ForAllContactFields(Fld) { if (Fld->FieldId == f) { return Fld->Option; } } if (f == FIELD_PERMISSIONS) { return "Perms"; } LAssert(0); return 0; } //////////////////////////////////////////////////////////////////////////////////////////// #define M_REMOVE_EMAIL_ADDR (M_USER+0x400) class EmailAddr : public LListItem { Contact *c; LAutoString Email; bool EditOneShot; bool Default; const char *ClickToAdd; public: EmailAddr(Contact *contact, const char *email = 0, bool def = false) { c = contact; EditOneShot = false; Default = def; Email.Reset(NewStr(email)); ClickToAdd = LLoadString(IDS_CLICK_TO_ADD); } bool GetDefault() { return Default; } char *GetEmail() { return Email; } LFont *GetFont() { return Default ? c->App->GetBoldFont() : 0; } const char *GetText(int Col=0) { if (EditOneShot) { EditOneShot = false; return (char*)""; } return Email ? Email : (char*)ClickToAdd; } void Delete() { if (Parent->Length() > 1) { Parent->GetWindow()->PostEvent(M_REMOVE_EMAIL_ADDR, (LMessage::Param)this); if (Default) { // Set one of the other's to the default List All; Parent->GetAll(All); for (auto a: All) { if (a != this && a->Email) { a->Default = true; a->Update(); break; } } } } else { Email.Reset(); } } bool SetText(const char *s, int Col=0) { if (s && Col == 0) { if (ValidStr(s)) { if (!Email) { Parent->Insert(new EmailAddr(c)); } Email.Reset(NewStr(s)); if (GetCss()) GetCss()->DeleteProp(LCss::PropColor); Update(); } else { Delete(); } } return false; } /* void OnPaint(LItem::ItemPaintCtx &Ctx) { if (!Email) { LColour Back; if (Select()) Back.Set(LC_FOCUS_SEL_BACK, 24); else Back.Set(LC_WORKSPACE, 24); COLOUR Fore24 = LC_LOW; COLOUR BackGrey = GdcGreyScale(Back.c24(), 24); COLOUR ForeGrey = GdcGreyScale(Fore24, 24); int Diff = abs((int)(BackGrey - ForeGrey)); if (Diff < 94) Fore24 = LC_FOCUS_SEL_FORE; // LgiTrace("Fore=%i Back=%i Diff=%i Fore=%x\n", BackGrey, ForeGrey, Diff, Fore24); SetForegroundFill(new GViewFill(Fore24, 24)); } else SetForegroundFill(0); LListItem::OnPaint(Ctx); } */ bool OnKey(LKey &k) { if (k.Down()) { switch (k.c16) { default: { if ( k.Down() && ( IsAlpha(k.c16) || k.c16 == ' ' || k.c16 == LK_F2 ) ) { EditOneShot = !Email; LViewI *v = EditLabel(0); if (v && k.IsChar && IsAlpha(k.c16)) { v->IterateViews().DeleteObjects(); if (v) { LEdit *e = dynamic_cast(v); if (e && !ValidStr(v->Name())) { LAutoString u(WideToUtf8(&k.c16, 1)); if (u) { e->Name(u); e->SetCaret(1); } } v->Focus(true); } else { LgiTrace("%s:%i - no edit.\n", __FILE__, __LINE__); } } return true; } break; } case LK_DELETE: { Parent->Delete(this); return true; break; } } } return false; } void OnMouseClick(LMouse &m) { if (m.Down()) { if (m.IsContextMenu()) { auto RClick = new LSubMenu; if (RClick) { #define IDM_SET_DEFAULT 200 RClick->AppendItem(LLoadString(IDS_SET_DEFAULT), IDM_SET_DEFAULT, !Default); RClick->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); // RClick->AppendSeparator(); if (LListItem::Parent->GetMouse(m, true)) { switch (RClick->Float(LListItem::Parent, m.x, m.y)) { case IDM_SET_DEFAULT: { // Clear the old default List All; Parent->GetAll(All); for (auto a: All) { if (a->Default) { a->Default = false; a->Update(); break; } } // Set the new default Default = true; Update(); break; } case IDM_DELETE: { Delete(); break; } } } DeleteObj(RClick); } } else if (m.Left()) { int c = Parent->ColumnAtX(m.x); if (c >= 0) { EditOneShot = !Email; EditLabel(c); } } } } }; ContactUi::ContactUi(Contact *item) : ThingUi(item, "Contact") { Lst = 0; ImgView = NULL; Item = item; if (!(Item && Item->App)) { return; } #if WINNATIVE SetStyle(GetStyle() & ~WS_VISIBLE); CreateClassW32("Scribe::ContactUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_CONTACT))); #endif if (Attach(0)) { LRect p; LString s = "Contact"; if (LoadFromResource(IDD_CONTACT, this, &p, &s)) { // size/position Name(s); SetPos(p); MoveSameScreen(App); // MoveToCenter(); // list setup if (GetViewById(IDC_EMAIL, Lst)) { Lst->ColumnHeaders(false); Lst->AddColumn("Email", Lst->X()); Lst->Insert(new EmailAddr(Item)); } // Tz setup LCombo *c; if (GetViewById(IDC_PICK_TZ, c)) { c->Sort(true); c->Sub(GV_DOUBLE); - LTimeZone *Tz = LTimeZones; + auto Tz = LTimeZone::GetTimeZones(); while (Tz->Text) { - char s[256]; - sprintf_s(s, sizeof(s), "%.1f %s", Tz->Offset, Tz->Text); - c->Insert(s); + c->Insert(LString::Fmt("%.1f %s", Tz->Offset, Tz->Text)); Tz++; } } // Show buttons to toggle mode... LButton *Show; if (GetViewById(IDC_SHOW_ADDR, Show)) { Show->SetIsToggle(true); Show->Value(1); } if (GetViewById(IDC_SHOW_EXTRA, Show)) Show->SetIsToggle(true); // controls AttachChildren(); // Image setup if (GetViewById(IDC_IMAGE, ImgView)) { ImgView->SetContact(Item); } OnLoad(); // show the window Visible(true); // set default button _Default = FindControl(IDOK); LViewI *f = FindControl(IDC_FIRST); if (f) f->Focus(true); } } SetPulse(1000); } ContactUi::~ContactUi() { Item->Ui = 0; } void ContactUi::OnDestroy() { THREAD_UNSAFE(); if (Item) { Item->Ui = NULL; } } bool ContactUi::InitField(int Id, const char *Name) { THREAD_UNSAFE(false); if (Item) { const char *s; if (Item->Get(Name, s)) { SetCtrlName(Id, s); return true; } int i; if (Item->Get(Name, i)) { SetCtrlValue(Id, i); return true; } } return false; } bool ContactUi::SaveField(int Id, const char *Name) { THREAD_UNSAFE(false); if (Item) { if (Id == FIELD_UID) { Item->Set(Name, (int)GetCtrlValue(Id)); return true; } else { Item->Set(Name, GetCtrlName(Id)); return true; } } return false; } void ContactUi::OnLoad() { THREAD_UNSAFE(); int Insert = 0; ForAllContactFields(f) { if (f->FieldId == FIELD_EMAIL) { const char *e; if (Item->Get(f->Option, e)) { Lst->Insert(new EmailAddr(Item, e, true), Insert++); } } else if (f->CtrlId > 0) { InitField(f->CtrlId, f->Option); } } auto AltEmail = LString(Item->GetObject()->GetStr(FIELD_ALT_EMAIL)).SplitDelimit(","); for (auto e: AltEmail) Lst->Insert(new EmailAddr(Item, e), Insert++); if (ImgView) { const LVariant *v = Item->GetObject()->GetVar(FIELD_CONTACT_IMAGE); if (v && v->Type == GV_BINARY) { ImgView->IsNoFace = false; ImgView->SetImage(*v); } } auto FirstName = Item->GetObject()->GetStr(FIELD_FIRST_NAME); auto LastName = Item->GetObject()->GetStr(FIELD_LAST_NAME); if (ValidStr(FirstName)||ValidStr(LastName)) { LString s; s.Printf("%s - %s%s%s", LLoadString(IDS_CONTACT), FirstName?FirstName:"", FirstName?" ":"", LastName?LastName:""); Name(s); } LViewI *c; if (GetViewById(IDC_TIMEZONE, c)) { LNotification note(LNotifyValueChanged); OnNotify(c, note); } } void ContactUi::OnSave() { THREAD_UNSAFE(); auto Obj = Item->GetObject(); EmailAddr *Def = NULL; List All; Lst->GetAll(All); ForAllContactFields(f) { if (f->FieldId == FIELD_EMAIL) { for (auto a: All) { if (a->GetDefault()) { Def = a; break; } } if (!Def) Def = All[0]; if (Def) Item->Set(f->Option, Def->GetEmail()); } else if (f->CtrlId > 0) { SaveField(f->CtrlId, f->Option); } } LString::Array AltEmails; for (auto a: All) { if (a != Def && a->GetEmail()) AltEmails.New() = a->GetEmail(); } LString AltEmail = LString(",").Join(AltEmails); Obj->SetStr(FIELD_ALT_EMAIL, AltEmail); if (ImgView) { // Reset the cache... Item->d->Image.Reset(); Item->d->ImagePath.Empty(); // Set the image in the back end store... LVariant *Img = &ImgView->GetImage(); Obj->SetVar(FIELD_CONTACT_IMAGE, Img); } if (Item && Item->App) { Item->Save(); LArray c; c.Add(Obj); Item->App->SetContext(_FL); Item->App->OnChange(c, 0); } } LMessage::Result ContactUi::OnEvent(LMessage *Msg) { THREAD_UNSAFE(0); switch (Msg->Msg()) { case M_REMOVE_EMAIL_ADDR: { EmailAddr *Addr = (EmailAddr*)Msg->A(); if (Addr) { Lst->Delete(Addr); } break; } } return ThingUi::OnEvent(Msg); } int ContactUi::OnNotify(LViewI *Ctrl, LNotification n) { THREAD_UNSAFE(0); switch (Ctrl->GetId()) { case IDOK: { OnSave(); // fall thru } case IDCANCEL: { PostEvent(M_CLOSE); break; } case IDC_TIMEZONE: { LString v = Ctrl->Name(); if (!v) break; auto dv = v.Float(); LCombo *c; if (GetViewById(IDC_PICK_TZ, c)) { for (size_t i=0; iLength(); i++) { auto s = (*c)[i]; if (s) { auto sval = atof(s); if (ABS(sval-dv) < 0.0001) { c->Value(i); return true; } } } } break; } case IDC_PICK_TZ: { if (Ctrl->Value() >= 0 && Ctrl->Name()) { double Tz = atof(Ctrl->Name()); char s[32]; sprintf_s(s, sizeof(s), "%.1f", Tz); SetCtrlName(IDC_TIMEZONE, s); } break; } case IDC_SHOW_ADDR: { LTableLayout *Tbl; if (GetViewById(IDC_TABLE, Tbl)) { int64 Shown = Ctrl->Value(); Ctrl->Name(Shown ? "-" : "+"); int y = 2; auto *c = Tbl->GetCell(0, y); if (c) c->Display(Shown ? LCss::DispBlock : LCss::DispNone); c = Tbl->GetCell(1, y); if (c) c->Display(Shown ? LCss::DispBlock : LCss::DispNone); Tbl->InvalidateLayout(); } break; } case IDC_SHOW_EXTRA: { LTableLayout *Tbl; if (GetViewById(IDC_TABLE, Tbl)) { int64 Shown = Ctrl->Value(); Ctrl->Name(Shown ? "-" : "+"); int y = 4; auto *c = Tbl->GetCell(0, y); if (c) c->Display(Shown ? LCss::DispBlock : LCss::DispNone); c = Tbl->GetCell(1, y); if (c) c->Display(Shown ? LCss::DispBlock : LCss::DispNone); Tbl->InvalidateLayout(); } break; } } return 0; } void ContactUi::OnPulse() { THREAD_UNSAFE(); SetCtrlName(IDC_LOCALTIME, Item->GetLocalTime(GetCtrlName(IDC_TIMEZONE))); } diff --git a/src/Store3Common.cpp b/src/Store3Common.cpp --- a/src/Store3Common.cpp +++ b/src/Store3Common.cpp @@ -1,621 +1,644 @@ #include #include "lgi/common/Lgi.h" #include "lgi/common/Mail.h" #include "lgi/common/TextConvert.h" #include "Store3Common.h" #include "ScribeInc.h" #include "DomType.h" LHashTbl,LDataStoreI*> LDataStoreI::Map; ////////////////////////////////////////////////////////////////////////////////////// const char *Store3ItemTypeToMime(Store3ItemTypes type) { switch (type) { case MAGIC_MAIL: return "message/rfc822"; case MAGIC_CONTACT: return "text/vcard"; case MAGIC_ATTACHMENT: return "application/octet-stream"; case MAGIC_CALENDAR: return "text/vcalendar"; case MAGIC_FILTER: return "text/x-email-filter"; default: LAssert(!"Unknown type"); break; } return NULL; } +CalendarType ParseCalendarType(const char *type) +{ + if (type && ToLower(*type) == 'v') + type++; + #define _(name) if (!Stricmp(type, #name)) return Cal##name; + _(Event) _(Todo) _(Journal) _(Request) _(Reply) + #undef _ + return CalTypeMax; +} + +const char *ToString(CalendarType type) +{ + switch (type) + { + case CalEvent: return "Event"; + case CalTodo: return "Todo"; + case CalJournal: return "Journal"; + case CalRequest: return "Request"; + case CalReply: return "Reply"; + default: return NULL; + } +} + ////////////////////////////////////////////////////////////////////////////////////// LDataUserI::LDataUserI() { Object = NULL; } LDataUserI::~LDataUserI() { if (Object) Object->UserData = NULL; } LDataI *LDataUserI::GetObject() { return Object; } bool LDataUserI::SetObject(LDataI *o, bool InDestuctor, const char *File, int Line) { if (o == Object) return true; if (Object) { Object->UserData = NULL; if (!InDestuctor && Object->IsOrphan()) delete Object; Object = NULL; } Object = o; if (File) SetterRef.Printf("%s:%i", File, Line); else SetterRef.Empty(); if (Object) Object->UserData = this; return true; } ////////////////////////////////////////////////////////////////////////////// bool LDataI::ParseHeaders() { // Reload from headers... LString InetHdrs = GetStr(FIELD_INTERNET_HEADER); auto Subject = LDecodeRfc2047(LGetHeaderField(InetHdrs, "subject")); if (LIsUtf8(Subject)) SetStr(FIELD_SUBJECT, Subject); // From auto s = LDecodeRfc2047(LGetHeaderField(InetHdrs, "from")); if (LIsUtf8(s)) { auto from = GetObj(FIELD_FROM); DecodeAddrName(s, [&](auto name, auto email) { from->SetStr(FIELD_NAME, name); from->SetStr(FIELD_EMAIL, email); }, NULL); } s = LDecodeRfc2047(LGetHeaderField(InetHdrs, "reply-to")); if (LIsUtf8(s)) { auto replyTo = GetObj(FIELD_REPLY); DecodeAddrName(s, [&](auto name, auto email) { replyTo->SetStr(FIELD_NAME, name); replyTo->SetStr(FIELD_EMAIL, email); }, NULL); } // Parse To and CC headers. s = LDecodeRfc2047(LGetHeaderField(InetHdrs, "to")); if (s.IsUtf8()) ParseAddresses(s, MAIL_ADDR_TO); s = LDecodeRfc2047(LGetHeaderField(InetHdrs, "cc")); if (s.IsUtf8()) ParseAddresses(s, MAIL_ADDR_CC); // Data if ((s = LGetHeaderField(InetHdrs, "date"))) { LDateTime dt; if (dt.Decode(s)) { dt.ToUtc(); SetDate(FIELD_DATE_SENT, &dt); } } return true; } bool LDataI::ParseAddresses(const char *Str, int CC) { LString::Array Addr; TokeniseStrList(Str, Addr, ","); auto store = GetStore(); auto to = GetList(FIELD_TO); if (!to || !store) return false; for (auto &RawAddr: Addr) { LAutoPtr a(to->Create(store)); if (!a) return false; auto sa = dynamic_cast(a.Get()); LAssert(sa != NULL); if (!sa) return false; DecodeAddrName(RawAddr, sa->Name, sa->Addr, 0); sa->CC = CC; to->Insert(a.Release()); } return true; } ////////////////////////////////////////////////////////////////////////////// Store3Addr::Store3Addr(LDataStoreI *store, LDataPropI *i) { LAssert(store != NULL); Store = store; CC = 0; if (i) CopyProps(*i); } Store3Addr::~Store3Addr() { } size_t Store3Addr::Sizeof() { return sizeof(*this) + Addr.Length() + Name.Length(); } void Store3Addr::SetStore(LDataStoreI *s) { Store = s; LAssert(Store != NULL); } void Store3Addr::Empty() { Name.Empty(); Addr.Empty(); CC = 0; } Store3CopyImpl(Store3Addr) { Empty(); Name = p.GetStr(FIELD_NAME); Addr = p.GetStr(FIELD_EMAIL); CC = (int)p.GetInt(FIELD_CC); return true; } const char *Store3Addr::GetStr(int id) { switch (id) { case FIELD_NAME: return Name; case FIELD_EMAIL: return Addr; } return 0; } Store3Status Store3Addr::SetStr(int id, const char *str) { switch (id) { case FIELD_NAME: { Name = str; break; } case FIELD_EMAIL: { Addr = str; break; } default: return Store3Error; } return Store3Success; } bool Store3Addr::GetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Value = Name; break; } case SdEmail: // Type: String { Value = Addr; break; } case SdDomain: // Type: String { char *At = Addr ? strchr(Addr, '@') : NULL; if (At) Value = At + 1; else Value.Empty(); break; } case SdText: // Type: String { char s[512]; LString EscName; if (Name) EscName = LString::Escape(Name, -1, "\'"); if (Name && Addr) sprintf_s(s, sizeof(s), "\"%s\" <%s>", EscName.Get(), (char*)Addr); else if (Name) sprintf_s(s, sizeof(s), "\"%s\"", EscName.Get()); else if (Addr) sprintf_s(s, sizeof(s), "<%s>", (char*)Addr); else return false; s[sizeof(s)-1] = 0; Value = s; break; } case SdContact: // Type: Contact { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (!e->Match(Store, this, MAGIC_CONTACT, Matches)) return false; Value = Matches[0]; break; } case SdGroups: // Type: String[] { LDataEventsI *e = Store->GetEvents(); if (!e) return false; LArray Matches; if (e->Match(Store, this, MAGIC_GROUP, Matches)) { Value.SetList(); for (unsigned i=0; i v(new LVariant); if (Matches[i]->GetValue("Name", *v)) Value.Value.Lst->Insert(v.Release()); } } else Value.Empty(); break; } default: { LAssert(!"Not a supported field."); return false; } } return true; } bool Store3Addr::SetVariant(const char *n, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(n); switch (Fld) { case SdName: // Type: String { Name = Value.Str(); break; } case SdEmail: // Type: String { Addr = Value.Str(); break; } case SdText: // Type: String { DecodeAddrName(Value.Str(), [this](LString name, LString addr){ Name = LString::UnEscape(name); Addr = addr; }, NULL); break; } default: { LAssert(!"Not a valid field"); return false; } } return true; } int64 Store3Addr::GetInt(int id) { switch (id) { case FIELD_CC: return CC; } return -1; } Store3Status Store3Addr::SetInt(int id, int64 i) { switch (id) { case FIELD_CC: CC = (int)i; break; default: return Store3Error; } return Store3Success; } /////////////////////////////////////////////////////////////////////////////////// Store3Field::Store3Field(LDataStoreI *Store, int id, int width) { Id = id; Width = width; } const char *Store3Field::GetStr(int id) { return 0; } int64 Store3Field::GetInt(int id) { switch (id) { case FIELD_ID: return Id; case FIELD_WIDTH: return Width; } return -1; } Store3Status Store3Field::SetInt(int id, int64 i) { switch (id) { case FIELD_ID: Id = (int)i; break; case FIELD_WIDTH: Width = (int)i; break; default: return Store3Error; } return Store3Success; } //////////////////////////////////////////////////////////////////////////////// // Mime conversion bool Store3ToLMime(LMime *Out, LDataPropI *InInterface) { auto In = dynamic_cast(InInterface); if (!Out || !In) { LAssert(0); return false; } auto Type = In->Type(); if (Type == MAGIC_MAIL) { auto Sub = In->GetList(FIELD_MIME_SEG); auto Child = Sub->First(); if (Child) { if (!Store3ToLMime(Out, Child)) return false; } else LAssert(0); } else if (Type == MAGIC_ATTACHMENT) { auto Hdrs = In->GetStr(FIELD_INTERNET_HEADER); if (Hdrs) { if (!Out->SetHeaders(Hdrs)) { LAssert(0); return false; } } else { // No headers??? } auto Charset = In->GetStr(FIELD_CHARSET); if (Charset) Out->SetCharset(Charset); auto Data = In->GetStream(_FL); if (Data) { if (!Out->SetData(true, Data.Release())) { LAssert(0); return false; } } LDataIt Sub = In->GetList(FIELD_MIME_SEG); for (LDataPropI *Child = Sub->First(); Child; Child = Sub->Next()) { LMime *NewSeg = Out->NewChild(); if (NewSeg) { if (Store3ToLMime(NewSeg, Child)) Out->Insert(NewSeg); else return false; } else { LAssert(0); return false; } } } else { LAssert(!"Incorrect object type."); return false; } return true; } bool LMimeToStore3(LDataPropI *Out, LMime *In, bool InMemOnly) { LDataI *DataOut = dynamic_cast(Out); if (!DataOut || !In) { LAssert(0); return false; } if (!Out->SetStr(FIELD_INTERNET_HEADER, In->GetHeaders())) { LAssert(0); return false; } if (In->GetLength() > 0) { LAutoStreamI Data(new LMemStream(In->GetData(), -1, -1)); if (!Data) { LAssert(0); return false; } if (!DataOut) { LAssert(0); return false; } DataOut->SetStream(Data); } for (int i=0; iLength(); i++) { LDataI *cOut = DataOut->GetStore()->Create(MAGIC_ATTACHMENT); if (!cOut) return false; LMime *cIn = (*In)[i]; if (!LMimeToStore3(cOut, cIn)) return false; Store3Status s = cOut->Save(DataOut); if (s == Store3Error) { LAssert(0); return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////// LString HeadersFromStream(LStreamI *Msg) { LString s; int Block = 1024; Msg->SetPos(0); for (ssize_t Pos = 0; Pos < (512 << 10); ) { if (!s.Length(Pos + Block)) break; ssize_t Rd = Msg->Read(s.Get() + Pos, s.Length() - Pos); if (Rd <= 0) { s.Empty(); break; } Pos += Rd; s.Length(Pos); ptrdiff_t EndOfHeader = s.Find("\r\n\r\n"); if (EndOfHeader > 0) { s.Length(EndOfHeader); break; } } return s; } //////////////////////////////////////////////////////////////////////////////////////// LString CreateMboxHeader(LDataI *Object) { LString s; LDataPropI *From; if (!Object || !(From = Object->GetObj(FIELD_FROM))) { LAssert(0); return s; } // generate from header s.Printf("From %s ", From->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *Object->GetDate(FIELD_DATE_RECEIVED); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); char Temp[64]; strftime(Temp, sizeof(Temp), "%a %b %d %H:%M:%S %Y", &Ft); s += Temp; s += "\r\n"; return s; } diff --git a/win/Scribe_vs2019.vcxproj b/win/Scribe_vs2019.vcxproj --- a/win/Scribe_vs2019.vcxproj +++ b/win/Scribe_vs2019.vcxproj @@ -1,738 +1,737 @@  Debug Win32 Debug x64 ReleaseNoOptimize Win32 ReleaseNoOptimize x64 Release Win32 Release x64 Scribe {540DDE64-0927-4E67-B927-2E3BE7ECB029} Scribe_vc8 Win32Proj 10.0 Application v142 Unicode true Application v142 Unicode true Application v142 Unicode Application v142 Unicode true Application v142 Unicode true Application v142 Unicode <_ProjectFileVersion>12.0.30501.0 $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false true C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;$(IncludePath) $(ProjectName) $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;C:\Office 2010 Developer Resources\include;$(IncludePath) $(ProjectName) $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;$(IncludePath) $(ProjectName) $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;$(IncludePath) $(ProjectName) $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;$(IncludePath) $(ProjectName) $(Platform)$(Configuration)19\ $(Platform)$(Configuration)19\ false C:\Program Files\OpenSSL-Win64\include;P:\CodeLib\libpng-1.6.26;P:\CodeLib\zlib-1.2.8;P:\CodeLib\libjpeg-9a;$(IncludePath) $(ProjectName) Disabled ..\src;..\Resources;..\src\Sqlite;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\include;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\private\common;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\libpng\build32;..\..\..\..\CodeLib\libpng\build32\zlib_dir;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;..\..\libs\mapi;..\..\libs\libchardet\src;..\..\libs\libchardet\include;c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include\mapi;c:\Program Files (x86)\OpenSSL\include;%(AdditionalIncludeDirectories);Utils\Tables SCRIBE_APP;WIN32;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL Level3 ProgramDatabase false Update version cd Code\Py "C:\Program Files\Python311\python.exe" win32_pre_build_step.py cd .. call "c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\vcvars32.bat" rc Resource.rc cd .. copy Code\Resource.res $(OutDir) imm32.lib;Ws2_32.lib;libchardet\Release\chardet14x32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x32;..\..\..\Lgi\trunk\lib;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;%(AdditionalLibraryDirectories) true Windows false MachineX86 ..\Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) X64 Disabled ..\src;..\src\Sqlite;..\Resources;..\Utils\Tables;..\..\libs\mapi;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\build-x64\libiconv-1.17\include;..\..\libs\build-x64\libpng\zlib_dir;..\..\libs\build-x64\libpng;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\private\common;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\include;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;c:\Program Files\OpenSSL\include;%(AdditionalIncludeDirectories) SCRIBE_APP;WIN64;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL NotUsing Level3 ProgramDatabase true imm32.lib;Ws2_32.lib;libchardet\Debug\chardet19x64d.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x64;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;..\..\..\Lgi\trunk\lib;..\..\..\CodeLib\protobuf-3.9.1\cmake\build\solution\$(Configuration);%(AdditionalLibraryDirectories) true Windows false MachineX64 UseLinkTimeCodeGeneration ..\Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) true ..\src;..\Resources;..\src\Sqlite;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\include;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\private\common;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\libpng\build32;..\..\..\..\CodeLib\libpng\build32\zlib_dir;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;..\..\libs\mapi;c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include\mapi;c:\Program Files (x86)\OpenSSL\include;%(AdditionalIncludeDirectories);Utils\Tables;..\crashpad-release-x86-64-stable\include;..\crashpad-release-x86-64-stable\include\mini_chromium SCRIBE_APP;WIN32;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL Level3 ProgramDatabase cd Code\Py "C:\Program Files\Python311\python.exe" win32_pre_build_step.py cd .. call "c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\vcvars32.bat" rc Resource.rc cd .. copy Code\Resource.res $(OutDir) imm32.lib;Ws2_32.lib;libchardet\Release\chardet14x32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x32;..\..\..\Lgi\trunk\lib;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;%(AdditionalLibraryDirectories) true Windows true true false MachineX86 Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) X64 ..\src;..\src\Sqlite;..\Resources;..\Utils\Tables;..\..\libs\mapi;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\build-x64\libiconv-1.17\include;..\..\libs\build-x64\libpng\zlib_dir;..\..\libs\build-x64\libpng;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\private\common;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\include;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;c:\Program Files\OpenSSL\include;%(AdditionalIncludeDirectories) SCRIBE_APP;WIN64;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL NotUsing Level1 ProgramDatabase MinSpace true cd ..\src\Py "C:\Program Files\Python311\python.exe" win32_pre_build_step.py cd .. call "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" rc Resource.rc copy Resource.res ..\win\$(OutDir) imm32.lib;Ws2_32.lib;libchardet\Release\chardet19x64.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x64;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;..\..\..\Lgi\trunk\lib;..\..\..\CodeLib\protobuf-3.9.1\cmake\build\solution\$(Configuration);%(AdditionalLibraryDirectories) true Windows true true false MachineX64 UseLinkTimeCodeGeneration ..\Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) Disabled ..\src;..\Resources;..\src\Sqlite;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\include;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\private\common;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\libpng\build32;..\..\..\..\CodeLib\libpng\build32\zlib_dir;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;..\..\libs\mapi;c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include\mapi;c:\Program Files (x86)\OpenSSL\include;%(AdditionalIncludeDirectories);Utils\Tables;..\crashpad-release-x86-64-stable\include;..\crashpad-release-x86-64-stable\include\mini_chromium SCRIBE_APP;WIN32;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL Level3 ProgramDatabase Update Version cd Code\Py "C:\Program Files\Python311\python.exe" win32_pre_build_step.py cd .. call "c:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\vcvars32.bat" rc Resource.rc cd .. copy Code\Resource.res $(OutDir) imm32.lib;Ws2_32.lib;libchardet\Release\chardet14x32.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x32;..\..\..\Lgi\trunk\lib;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;%(AdditionalLibraryDirectories) true Windows true true false MachineX86 Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) X64 ..\src;..\src\Sqlite;..\Resources;..\Utils\Tables;..\..\libs\mapi;..\..\libs\libchardet\include;..\..\libs\libchardet\src;..\..\libs\build-x64\libiconv-1.17\include;..\..\libs\build-x64\libpng\zlib_dir;..\..\libs\build-x64\libpng;..\..\libs\aspell-0.60.6.1\interfaces\cc;..\..\..\Lgi\trunk\private\common;..\..\..\Lgi\trunk\include\lgi\win;..\..\..\Lgi\trunk\include;..\..\..\..\CodeLib\libpng;..\..\..\..\CodeLib\zlib;..\..\..\..\CodeLib\libjpeg-9a;c:\Program Files\OpenSSL\include;%(AdditionalIncludeDirectories) SCRIBE_APP;WIN64;HAVE_STRING_H;WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL NotUsing Level1 ProgramDatabase MinSpace true cd ..\src\Py "C:\Program Files\Python311\python.exe" win32_pre_build_step.py cd .. call "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" rc Resource.rc copy Resource.res ..\win\$(OutDir) imm32.lib;Ws2_32.lib;libchardet\Release\chardet19x64.lib;%(AdditionalDependencies) $(OutDir)$(TargetFileName) ..\..\libs\build-x64;..\bzip2-1.0.6\windows\$(Platform)$(Configuration)14;..\aspell-0.60.6.1\win32\dist\$(Platform)$(Configuration)14;..\..\..\Lgi\trunk\lib;..\..\..\CodeLib\protobuf-3.9.1\cmake\build\solution\$(Configuration);%(AdditionalLibraryDirectories) true Windows true true false MachineX64 UseLinkTimeCodeGeneration ..\Resources\ScribeWindows.manifest;%(AdditionalManifestFiles) - cd $(IntDir) "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\ml64.exe" %(FullPath) /c $(IntDir)%(Filename).obj;%(Outputs) cd $(IntDir) "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\ml64.exe" %(FullPath) /c $(IntDir)%(Filename).obj;%(Outputs) cd $(IntDir) "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\ml64.exe" %(FullPath) /c $(IntDir)%(Filename).obj;%(Outputs) {95df9ca4-6d37-4a85-a648-80c2712e0da1} {f97aaaca-bf41-46b8-b534-a1639589a1a3} {11e9238b-921e-44e5-a3d2-61c5bfa7cc6c} {1509fe6d-0b3a-422a-99d1-e26a26ce8da5} {e5575141-c1a2-4997-badb-7584cf546e1f} {148574ce-4da6-4b8a-9ec6-ff68adc2a046} \ No newline at end of file diff --git a/win/Scribe_vs2019.vcxproj.filters b/win/Scribe_vs2019.vcxproj.filters --- a/win/Scribe_vs2019.vcxproj.filters +++ b/win/Scribe_vs2019.vcxproj.filters @@ -1,1101 +1,1098 @@  BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree Emoji Emoji Emoji RichTextEdit Controls/UI Controls/UI Controls/UI Lgi Filtering Lgi Lgi Lgi Lgi BTree RichTextEdit Lgi Lgi BTree RichTextEdit Lgi Network Network Network Network Graphics RichTextEdit RichTextEdit Store3 Graphics Graphics BTree Lgi Lgi Mail Mail Mail Mail Mail Scripting Scripting Scripting Controls/UI Lgi Install SpellCheck RichTextEdit Mail Graphics Lgi Controls/UI Controls/UI Controls/UI Store3\WebDav Import/Export - - Calendar - Calendar Calendar Controls/UI\Html Controls/UI\Html Controls/UI\Html Controls/UI\Html Network Network SpellCheck Controls/UI Filtering Filtering Calendar Calendar Install Import/Export Calendar Filtering Encryption Import/Export Import/Export Import/Export Import/Export Import/Export Import/Export Import/Export Import/Export Store3\Mail3 Store3\Mail3 Store3\Mail3 Store3\Mail3 Store3\Mail3 Store3\Mail3 Store3\Mail3 Store3\Mail3 User Interface Mail User Interface User Interface User Interface User Interface Printing Printing Import/Export Application Application Calendar User Interface User Interface User Interface User Interface Store3\Imap Store3\Imap Store3\Imap Store3\Imap Store3\Imap User Interface Application User Interface Application Application Application User Interface Application User Interface Application User Interface Store3\Mapi Store3\Mapi Store3\Mapi Store3\Mapi Store3\Mapi Store3\Mapi Store3\Mapi User Interface Application User Interface Store3\Mail3 Store3 User Interface Scripting Application Store3\WebDav Store3\WebDav Store3\WebDav Store3\WebDav Store3\WebDav User Interface User Interface User Interface Printing Application Mail Mail Application Application Application Testing Application BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree BTree Emoji Emoji Controls/UI Controls/UI Controls/UI Lgi Filtering Lgi Lgi Lgi BTree Lgi Lgi BTree Network Network Network RichTextEdit RichTextEdit Scripting Store3 Store3 Store3 Store3 Store3\WebDav BTree Mail Mail Scripting Scripting Scripting Controls/UI Lgi Install SpellCheck Mail Lgi Store3\WebDav Controls/UI Controls/UI Application Lgi Controls/UI\Html Controls/UI\Html Controls/UI\Html Controls/UI\Html Network Network Filtering Calendar Calendar Install Application Application Application Encryption Import/Export Store3\Mail3 User Interface User Interface User Interface Printing Printing Import/Export Application Application User Interface User Interface Store3\Imap Application Application User Interface Application Store3\Mapi User Interface Store3\Mail3 Store3\Mail3 User Interface Store3\WebDav Store3\WebDav Printing Application Application Application Application Application Resources Resources Resources Resources Resources Resources Resources Resources Resources Help Help Help Help Help Help Help Help Help Help Help Help Scripting Resources Help Help Help Help Help Help Install Install Install Install Scripting\Scripts Scripting\Scripts Scripting\Scripts Scripting\Scripts Scripting\Scripts Scripting\Scripts Install Controls/UI\PreviewPanel Controls/UI\PreviewPanel Controls/UI\PreviewPanel Install Application Application Testing\Email Testing\Email Testing\Email Testing\Email Testing\Email Testing\Email Testing\Email Testing\Email Scripting\Scripts Scripting {28943112-0c58-468a-b315-7696092c8778} {f16baf81-af69-4eb1-9faf-6f448a36a13b} {ffd46a02-1aed-411d-a961-754cfaf46299} {cac512da-cfa7-4bbe-b5be-14a1d31848fb} {15bf5500-86f7-4217-ad50-dfc441b2fb89} {111097d8-3c3a-4fb4-a418-548f08670ade} {3347deb8-7d16-4010-9dff-676bafee8413} {84e29f91-6e12-481b-8d4c-18e499c9a501} {1d79521d-1efc-4656-bec1-13973241c432} {90145701-794d-4956-af76-5cc116600a6e} {963cf563-311f-4292-8aef-947945b63c15} {762f00ae-b360-4e54-b181-857af25a6490} {98c796a3-d418-4adb-b8f8-1a51e8349d96} {9f3b229f-b2f8-4bbb-897c-f048d35571d4} {2c79f6d5-53c2-454d-b897-0a6779024c02} {8139c7b4-9593-48c3-98af-a0db78c410aa} {d7ace5ac-018a-4ae6-882e-652cf33d3f50} {2431021b-92a9-444c-9796-2b1e7d81bd5a} {b63a6ed6-bb9e-4538-95f8-2f7782e199eb} {7b5dbea7-b971-4a65-a5f5-e12726480450} {432fd674-ec47-4cfc-afdf-d60b633feb48} {dbfbd2a8-e411-4b43-a188-bde8cd1ac292} {cc8997f1-f092-4b82-a5e5-a3bcc92846ce} {6fbe15c0-133a-4ae9-a441-03a9dcafe050} {4fd600f1-77c6-4b58-b9a8-85d436233d52} {22d7ec0d-a127-4b05-b6f0-937c3255224a} {8fc43a66-4c8f-4df4-a79b-195ac2b933f6} {0bbd47dc-9bff-40f0-9d61-182a4dc33ff7} {c8868eeb-fe8d-42a2-a647-9df3bff160c4} {a1a4d9a6-dba2-4699-8d31-fc77816706cd} Scripting Resources Resources \ No newline at end of file