9 extern int __ecereVMethodID_class_OnFree;
10 extern int __ecereVMethodID_class_OnGetString;
11 extern int __ecereVMethodID_class_OnGetDataFromString;
16 // #define _DEBUG_LINE
19 #define FULL_STRING_SEARCH
21 #define UTF8_IS_FIRST(x) (__extension__({ byte b = x; (!(b) || !((b) & 0x80) || ((b) & 0x40)); }))
22 #define UTF8_NUM_BYTES(x) (__extension__({ byte b = x; (b & 0x80 && b & 0x40) ? ((b & 0x20) ? ((b & 0x10) ? 4 : 3) : 2) : 1; }))
24 define newEntryStringDebug = $"New|id=";
25 define newEntryString = $"New";
27 public char ToASCII(unichar ch)
32 if(ch == 'À' || ch == 'Á' || ch == 'Â' || ch == 'Ã' || ch == 'Ä' || ch == 'Å')
36 else if(ch == 'È' || ch == 'É' || ch == 'Ê' || ch == 'Ë')
38 else if(ch == 'Ì' || ch == 'Í' || ch == 'Î' || ch == 'Ï')
42 else if(ch == 'Ò' || ch == 'Ó' || ch == 'Ô' || ch == 'Õ' || ch == 'Ö')
44 else if(ch == 'Ù' || ch == 'Ú' || ch == 'Û' || ch == 'Ü')
46 else if(ch == 'à' || ch == 'á' || ch == 'â' || ch == 'ã' || ch == 'ä' || ch == 'å')
50 else if(ch == 'è' || ch == 'é' || ch == 'ê' || ch == 'ë')
52 else if(ch == 'ì' || ch == 'í' || ch == 'î' || ch == 'ï')
56 else if(ch == 'ò' || ch == 'ó' || ch == 'ô' || ch == 'õ' || ch == 'ö')
58 else if(ch == 'ù' || ch == 'ú' || ch == 'û' || ch == 'ü')
66 public char * ConvertToASCII(char * string, char * newString, int * len, bool lowerCase)
73 for(c = 0; (unich = UTF8GetChar(string + c, &nb)); c += nb)
75 char ch = ToASCII(unich);
76 if(ch < 128 && (ch == '-' || isalpha(ch) || ch == ','))
77 newString[d++] = lowerCase ? (char)tolower(ch) : (char)ch;
85 public class NoCaseAccent : SQLCustomFunction
87 void Process(char * text)
89 int len = text ? strlen(text) : 0;
91 ConvertToASCII(text ? text : "", array.array, &len, true);
92 array.count = len + 1;
96 public class MemberStringSample
101 default extern int __ecereVMethodID_class_OnUnserialize;
103 public class GetMemberString<class NT:void * = MemberStringSample, name = NT::name> : SQLCustomFunction
105 SerialBuffer buffer { };
106 void Process(char * text)
109 buffer.buffer = text;
110 buffer.count = MAXINT;
112 class(NT)._vTbl[__ecereVMethodID_class_OnUnserialize](class(NT), &pn, buffer);
113 NoCaseAccent::Process((NoCaseAccent)this, (pn && pn.name) ? pn.name : null);
115 buffer.buffer = null;
117 // TOFIX: If we name GetName's type 'T', the following name confuses with Array's 'T'
118 //ConvertToASCII(s ? s : "", array.array, &len, true);
122 // Rename TableEditor to TableControl and move to eda/src/gui/controls
123 public class TableEditor : public Window
131 DebugLn("TableEditor::table|set");
141 DebugLn("TableEditor::index|set");
143 filterRow.tbl = index;
150 DebugLn("TableEditor::OnPostCreate");
158 InitFieldsBoxes(); // IMPORTANT: table must be set *AFTER* all related FieldEditors have been initialized
160 Field fldId = idField, fldName = stringField, fldActive = null;
161 FieldIndex indexedFields[1];
162 if(!idField) fldId = table.FindField(defaultIdField);
163 if(!fldName) fldName = table.FindField(defaultNameField);
164 if(!fldActive) fldActive = table.FindField(defaultActiveField);
165 indexedFields[0] = { fldId };
166 table.Index(1, indexedFields);
174 if(!listEnumerationTimer.hasCompleted)
176 if(list && !list.currentRow)
177 list.SelectRow(list.firstRow); // should the tableeditor select method be used here?
182 bool OnClose(bool parentClosing)
184 bool result = NotifyClosing();
193 property ListBox list
197 DebugLn("TableEditor::list|set");
204 Field indexFilterField;
207 property Array<ListField> listFields
211 DebugLn("TableEditor::listFields|set");
216 Array<ListField> listFields;
218 DataField listSortField;
219 bool disabledFullListing;
221 property Array<StringSearchField> searchFields
225 StringSearchTable searchTable { table, idField, value };
226 DebugLn("TableEditor::searchFields|set");
227 // The searchTables property will incref...
228 property::searchTables = { [ searchTable ] };
232 property Array<StringSearchTable> searchTables
236 // This API is not very clear on ownership of search tables array/search table/field...
237 // Right now both the array and tables themselves are incref'ed here
239 for(t : value) { incref t; }
240 DebugLn("TableEditor::searchTables|set");
241 if(searchTables) searchTables.Free();
243 searchTables = value;
246 Array<StringSearchTable> searchTables;
248 property Array<SQLiteSearchTable> sqliteSearchTables
253 for(t : value) { incref t; }
254 DebugLn("TableEditor::searchTables|set");
255 if(sqliteSearchTables) sqliteSearchTables.Free();
256 delete sqliteSearchTables;
257 sqliteSearchTables = value;
260 Array<SQLiteSearchTable> sqliteSearchTables;
262 property String searchString
266 bool modified = modifiedDocument;
267 DebugLn("TableEditor::searchString|set");
268 switch(modified ? OnLeavingModifiedDocument() : no)
278 if(value && value[0])
279 searchString = CopyString(value);
287 Map<Table, Lookup> lookups;
289 Array<LookupEditor> dynamicLookupEditors;
290 property Array<LookupEditor> dynamicLookupEditors
294 DebugLn("TableEditor::dynamicLookupEditors|set");
295 dynamicLookupEditors = value;
300 property Id selectedId { get { return selectedId; } }
302 Array<FieldBox> fieldsBoxes { };
303 Array<TableEditor> tableEditors { };
304 Array<TableEditor> dynamicLookupTableEditors { };
308 public virtual void OnInitialize();
309 public virtual void OnLoad();
310 public virtual void OnStateChanged();
311 bool internalModifications;
312 public void NotifyModifiedDocument()
314 DebugLn("TableEditor::NotifyModifiedDocument");
315 if(!internalModifications)
319 //public virtual bool Window::NotifyNew(AltListSection listSection, Row r);
320 //virtual void Window::NotifyInitFields(AltEditSection editSection);
322 public virtual DialogResult OnLeavingModifiedDocument()
324 DebugLn("TableEditor::OnLeavingModifiedDocument");
325 return MessageBox { master = this, type = yesNoCancel, text = text && text[0] ? text : $"Table Editor",
326 contents = $"You have modified this entry. Would you like to save it before proceeding?"
330 public virtual bool OnRemovalRequest()
332 DebugLn("TableEditor::OnRemovalRequest");
333 return MessageBox { master = this, type = yesNo, text = text && text[0] ? text : $"Table Editor",
334 contents = $"You are about to permanently remove an entry.\n"
335 "Do you wish to continue?"
339 //public virtual void Window::NotifyDeleting(ListSection listSection);
340 //public virtual void Window::NotifyDeleted(ListSection listSection);
342 public bool NotifyClosing()
345 DebugLn("TableEditor::NotifyClosing");
348 switch(OnLeavingModifiedDocument())
362 StopWordListPrepTimer();
363 StopListEnumerationTimer();
368 //public void List() // this gets called out of nowhere by some constructor thing...
369 public void Enumerate()
371 DebugLn("TableEditor::Enumerate");
374 StopListEnumerationTimer();
378 if(list || OnList != TableEditor::OnList)
381 Array<Id> matches = null;
382 listEnumerationTimer.sqliteSearch = false;
383 if(searchTables && searchTables.count)
384 matches = SearchWordList();
385 //else if(sqliteSearchTables && sqliteSearchTables.count)
386 //matches = SearchSQLite();
387 else if(searchString && searchString[0] &&
388 sqliteSearchTables && sqliteSearchTables.count &&
389 sqliteSearchTables[0].table && sqliteSearchTables[0].idField &&
390 sqliteSearchTables[0].searchFields && sqliteSearchTables[0].searchFields.count &&
391 sqliteSearchTables[0].searchFields[0].field)
392 listEnumerationTimer.sqliteSearch = true;
393 if(matches && matches.count)
394 PrintLn("results count: ", matches.count);
399 modifiedDocument = false; // setting this here is not really logical, enumeration and modified have nothing to do with eachother
402 virtual void OnList(Row r, Array<Id> matches)
404 DebugLn("TableEditor::OnList");
405 if(!listEnumerationTimer.started)
407 listEnumerationTimer.hasCompleted = false;
408 listEnumerationTimer.matchesIndex = 0;
409 listEnumerationTimer.tablesIndex = 0;
410 if(!listEnumerationTimer.sqliteSearch)
411 listEnumerationTimer.row = Row { r.tbl };
414 listEnumerationTimer.matches = { };
415 // TOFIX: This (void *) cast here should NOT be required... Fix this Container warning:
416 // warning: incompatible expression matches (ecere::com::Array<eda::Id>); expected ecere::com::Container<T>
417 listEnumerationTimer.matches.Copy((void *)matches);
420 listEnumerationTimer.matches = null;
421 listEnumerationTimer.Start();
424 DebugLn("TableEditor::OnList -- timer state error");
427 virtual void OnCreateDynamicLookupEditors()
429 DebugLn("TableEditor::OnCreateLookupEditors");
430 if(dynamicLookupEditors && dynamicLookupEditors.count)
432 for(f : dynamicLookupEditors)
434 if(f.editorClass && f.parentWindow && f.lookupFindField)
436 Row row { f.lookupFindIndex ? f.lookupFindIndex : f.lookupFindField.table };
437 // todo: make this work for all types
439 editRow.GetData(f.lookupValueField, id);
440 // TODO: add alternative class instance for creation when no rows are found via lookup
441 for(row.Find(f.lookupFindField, middle, nil, id); !row.nil; row.Next())
443 // todo: make this work for all types, although this is meant to be an id field
445 TableEditor editor = eInstance_New(f.editorClass);
447 editor.parent = f.parentWindow;
448 editor.master = this;
449 dynamicLookupTableEditors.Add(editor);
451 row.GetData(f.lookupIdField, id);
460 TableEditor masterEditor;
462 public property TableEditor masterEditor
466 if(value != masterEditor)
469 masterEditor.RemoveTableEditor(this);
470 masterEditor = value;
472 value.AddTableEditor(this);
479 if(eClass_IsDerived(parent._class, class(TableEditor)))
480 property::masterEditor = (TableEditor)parent;
485 if(eClass_IsDerived(master._class, class(TableEditor)))
486 property::masterEditor = (TableEditor)master;
491 DebugLn("TableEditor::CreateRow");
492 //list.NotifySelect(this, list, null, 0);
493 if(!modifiedDocument)
495 uint id; // = table.rowsCount + 1; // this is bad with deleted rows, won't work, how to have unique id?
496 // I think the 3 following comment lines apply to the old sqlite driver before many fix we done for wsms
497 Row r = editRow;// { table }; // the multipurpose row is buggy with sqlite driver, you can't use the same row to do Row::Last(), Row::Next(), Row::Find(), etc...
498 //Row r { editRow.tbl }; // for example, Row::Last() here is not using the proper sqlite statement and fails to
499 // return false when no rows are present in a table
503 /*uint count = editRow.tbl.GetRowsCount();
506 // r.Last() is returning true even if there are not rows in this table (SQLite)
507 if(count && !(r.Last() || r.Last()))
508 DebugLn("PROBLEM");*/
509 if(r.Last()) // this will reuse ids in cases where the item(s) with the last id have been deleted
511 r.GetData(idField, id);
522 // Patch for SQLite driver which auto-increments IDs
524 if(r.GetData(idField, curId))
527 r.SetData(idField, id);
530 r.SetData(fldActive, active);*/
533 newText = PrintString("[", newEntryStringDebug, id, "]");
535 newText = PrintString("[", newEntryString, "]");
538 //if(NotifyNew(master, this, r))
539 if(listFields && idField)
543 if(lf.dataField && lf.field)
545 if(lf.field.type == class(String))
546 r.SetData(lf.field, newText);
547 else // this whole block is new?
549 if(lf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])
551 Class dataType = lf.field.type;
552 int64 dataHolder = 0;
553 void * data = &dataHolder;
555 if(dataType && dataType.type == structClass)
557 dataHolder = (int64)new0 byte[dataType.structSize];
558 data = (void *)dataHolder;
560 /*else if(dataType && (dataType.type == noHeadClass || dataType.type == normalClass))
562 if(eClass_IsDerived(dataType, class(char*)))
563 dataHolder = (int64)CopyString("");
565 dataHolder = (int64)eInstance_New(dataType);
566 data = (void *)&dataHolder;
574 dataType._vTbl[__ecereVMethodID_class_OnGetDataFromString](dataType, data, newText);
577 /*dataType._vTbl[__ecereVMethodID_class_OnFree](dataType, dataHolder);
578 if(dataType.type == structClass)
580 void * dataPtr = (void *)dataHolder;
592 // have a better technique than Row::Next(); Row::Find(...); to make sure Row::GetData() will work right after a Row::SetData()?
593 // it seems we're missing Row::Update()
598 // next line is a patch for SQLite not returning data from GetData right after a SetData
599 if(idField && r.Find(idField, middle, nil, id))
600 SetListRowFields(r, row, false);
603 else if(idField && stringField)
605 r.SetData(stringField, newText);
608 row = list.AddString(newText);
618 list.Sort(listSortField, listSortOrder);
619 if(row) SelectListRow(row);
627 DebugLn("TableEditor::Remove");
628 if(editRow.sysID) //list && list.currentRow)
630 if(OnRemovalRequest())
634 list.DeleteRow(list.currentRow);
636 //NotifyDeleted(master, this);
638 SelectListRow(list.currentRow);
646 DebugLn("TableEditor::Load");
652 DebugLn("TableEditor::Write");
656 bool ListSelect(DataRow row)
659 DebugLn("TableEditor::ListSelect");
660 if(/*-row && -*/row != lastRow)
666 list.currentRow = lastRow;
668 switch(OnLeavingModifiedDocument())
676 list.currentRow = row;
680 if(list.currentRow == row)
689 DebugLn("TableEditor::Select");
690 // EDA is now set up so that Next()/Prev() will work with sysID = , but not with Find() (As Find() will return a particular set of results)
691 if(idField && (editRow.sysID = id, !editRow.nil))// && editRow.Find(idField, middle, nil, id))
693 //Id test = editRow.sysID;
694 selectedId = editRow.sysID;
706 DebugLn("TableEditor::Filter");
707 if(selectedId && index && indexFilterField)
709 for(filterRow.Find(indexFilterField, middle, nil, id); !filterRow.nil; filterRow.Next())
712 filterRow.GetData(idField, id2);
713 if(id2 == selectedId)
729 bool wasNil = !editRow.sysID;
730 DebugLn("TableEditor::SelectNext");
731 // How about confirmation / saving before changing the entry?
736 if(wasNil && filterRow.sysID == selectedId)
738 editRow.sysID = filterRow.sysID;
747 //Id test = editRow.sysID;
748 selectedId = editRow.sysID;
755 // Wrap around after 2 Next if commented out (1st time could inform user of reaching the end)
756 // The first Next() bring the row to a null row (rowID = 0), a Next() on a rowID = 0 starts at first row
757 //editRow.Previous();
762 bool SelectPrevious()
765 bool wasNil = !editRow.sysID;
766 DebugLn("TableEditor::SelectPrevious");
769 if(filterRow.Previous())
771 if(wasNil && filterRow.sysID == selectedId)
772 filterRow.Previous();
773 editRow.sysID = filterRow.sysID;
782 //Id test = editRow.sysID;
783 selectedId = editRow.sysID;
790 // Wrap around after 2 Prev if commented out (1st time could inform user of reaching the end)
791 // The first Prev() bring the row to a null row (rowID = 0), a Prev() on a rowID = 0 starts at last row
797 void SelectListRow(DataRow row)
799 // Time startTime = GetTime();
800 DebugLn("TableEditor::SelectListRow");
803 selectedId = row.tag;
806 if(list.currentRow != row)
807 list.currentRow = row;
808 if(idField && editRow.Find(idField, middle, nil, selectedId))
810 //Id test = editRow.sysID;
812 //NotifySelectListRow(master, this, selectedId);
816 // Logf("SelectListRow took %f seconds\n", GetTime() - startTime);
826 Array<char> searchCI { };
828 ListEnumerationTimer listEnumerationTimer
830 userData = this, delay = 0.1f;
833 static Time startTime;
836 Row row = listEnumerationTimer.row;
837 Array<Id> matches = listEnumerationTimer.matches;
838 Time delay = listEnumerationTimer.delay;
839 Time lastTime = GetTime();
840 static int slice = 128;
842 static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
845 PrintLn("listing... ");
849 int index = listEnumerationTimer.matchesIndex;
850 if(listFields && idField)
852 for(c=0; c<slice && (next = index++<matches.count); c++)
854 if(row.Find(idField, middle, nil, matches[index]))
857 DataRow dataRow = list.AddRow();
858 row.GetData(idField, id);
860 SetListRowFields(row, dataRow, true);
863 DebugLn($"WordList match cannot be found in database.");
865 listEnumerationTimer.matchesIndex = index;
866 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
868 else if(idField && stringField)
870 for(c=0; c<slice && (next = index++<matches.count); c++)
872 if(row.Find(idField, middle, nil, matches[index]))
876 row.GetData(idField, id);
877 row.GetData(stringField, s);
878 list.AddString(s).tag = id;
882 DebugLn($"WordList match cannot be found in database.");
884 listEnumerationTimer.matchesIndex = index;
885 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
888 else if(listEnumerationTimer.sqliteSearch)
890 static SQLiteSearchTable st = null;
891 Row lookupRow { table };
892 Map<Id, int> uniques = listEnumerationTimer.uniques;
895 if(listEnumerationTimer.tablesIndex < sqliteSearchTables.count)
897 char queryString[4096*4];
899 if(!listEnumerationTimer.uniques)
900 listEnumerationTimer.uniques = uniques = { };
902 st = sqliteSearchTables[listEnumerationTimer.tablesIndex];
903 if(st.table && st.idField && st.searchFields && st.searchFields.count)
905 wordListPrepRowNum = 0;
906 wordListPrepRowCount = st.table.rowsCount;
908 if(st.table && st.idField && st.searchFields && st.searchFields.count &&
909 st.searchFields[0].field)
914 listEnumerationTimer.row = row = { st.table };
917 int len = searchString ? strlen(searchString) : 0;
918 searchCI.size = len + 1;
919 ConvertToASCII(searchString ? searchString : "", searchCI.array, &len, true);
920 searchCI.count = len + 1;
923 sprintf(queryString, "SELECT ROWID, * FROM `%s`", st.table.name);
924 strcat(queryString, " WHERE ");
925 for(sf : st.searchFields)
930 strcat(queryString, " OR ");
931 #define PERSON_SEARCH_LIKE
932 // This code tries to implement the logic of PersonName::OnGetDataFromString because PersonName is inaccessible from here
933 if(strstr(sf.field.type.name, "PersonName"))
935 #ifdef PERSON_SEARCH_LIKE
936 String ln = null, fn = null, mn = null;
937 char * comma = strchr(searchCI.array, ',');
940 int len = comma - searchCI.array;
941 ln = new char[len + 1];
942 memcpy(ln, searchCI.array, len);
945 fn = CopyString(comma+1);
948 for(c = strlen(fn)-2; c > 0; c--)
952 mn = CopyString(fn + c + 1);
959 ln = CopyString(searchCI.array);
961 /* We could simply do this if we had access to PersonName: (don't forget the delete pn; below)
963 pn.OnGetDataFromString(searchCI.array);
967 // Only last name is pecified in search object, it is looked for in all fields
968 strcatf(queryString, "(PNLastName(`%s`) LIKE '%%%s%%' OR PNFirstName(`%s`) LIKE '%%%s%%' OR PNMiddleName(`%s`) LIKE '%%%s%%')",
969 sf.field.name, searchCI.array, sf.field.name, searchCI.array, sf.field.name, ln);
971 else if(ln || fn || mn)
973 // Otherwise search is in respective fields only (all specified must match)
975 strcatf(queryString, "(");
978 if(!first) strcatf(queryString, " AND ");
980 strcatf(queryString, "PNLastName(`%s`) LIKE '%%%s%%'", sf.field.name, ln);
984 if(!first) strcatf(queryString, " AND ");
986 strcatf(queryString, "PNFirstName(`%s`) LIKE '%%%s%%'", sf.field.name, fn);
990 if(!first) strcatf(queryString, " AND ");
992 strcatf(queryString, "PNMiddleName(`%s`) LIKE '%%%s%%'", sf.field.name, mn);
994 strcatf(queryString, ")");
997 delete ln; delete fn; delete mn;
999 // To use CompPersonName::OnCompare:
1000 strcatf(queryString, "`%s` = ?", sf.field.name);
1004 strcatf(queryString, "`%s` LIKE '%%%s%%'", sf.field.name, searchString);
1008 PrintLn(queryString);
1009 startTime = GetTime();
1010 row.query = queryString;
1011 #ifndef PERSON_SEARCH_LIKE
1012 // To use CompPersonName::OnCompare:
1013 for(sf : st.searchFields)
1017 if(strstr(sf.field.type.name, "PersonName"))
1020 sf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString](sf.field.type, &pn, searchCI.array);
1021 row.SetQueryParamObject(++bindId, pn, sf.field.type);
1037 if(listFields && idField)
1039 // should we not start with a Next() ??? :S -- right now, query = does not need a Next(), unless it had parameters (SetQueryParam), See #591
1040 // when going through all the rows in a table you always start with Next() no?
1041 // is this different for query results?
1042 for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
1045 row.GetData(st.idField, id);
1046 //if(uniques[id]++ == 0)
1047 if(uniques[id] == 0)
1049 DataRow dataRow = list.AddRow();
1051 if(st.table == table)
1052 SetListRowFields(row, dataRow, true);
1053 else if(lookupRow.Find(idField, middle, nil, id))
1054 SetListRowFields(lookupRow, dataRow, true);
1058 uniques[id] = uniques[id] + 1;
1060 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1063 delete listEnumerationTimer.row; row = null;
1064 next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1067 else if(idField && stringField)
1069 // should we not start with a Next() ??? :S
1070 // when going through all the rows in a table you always start with Next() no?
1071 // is this different for query results?
1072 for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
1075 row.GetData(st.idField, id);
1076 //if(uniques[id]++ == 0)
1077 if(uniques[id] == 0)
1080 if(st.table == table)
1081 row.GetData(stringField, s);
1082 else if(lookupRow.Find(idField, middle, nil, id))
1083 lookupRow.GetData(stringField, s);
1086 list.AddString(s).tag = id;
1089 uniques[id] = uniques[id] + 1;
1091 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1094 delete listEnumerationTimer.row; row = null;
1095 next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1101 else if(!disabledFullListing)
1103 if(listFields && idField)
1105 for(c = 0; c<slice && (next = row.Next()); c++)
1108 DataRow dataRow = list.AddRow();
1109 row.GetData(idField, id);
1111 SetListRowFields(row, dataRow, true);
1112 //app.UpdateDisplay();
1114 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1116 else if(idField && stringField)
1118 for(c = 0; c<slice && (next = row.Next()); c++)
1122 row.GetData(idField, id);
1123 row.GetData(stringField, s);
1124 list.AddString(s).tag = id;
1127 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1131 list.Sort(listSortField, listSortOrder);
1135 listEnumerationTimer.hasCompleted = true;
1136 StopListEnumerationTimer();
1138 if(startTime) PrintLn("*** Search took ", (uint)((GetTime() - startTime) * 1000), "ms ***");
1143 void StopListEnumerationTimer()
1145 listEnumerationTimer.Stop();
1146 listEnumerationTimer.matchesIndex = 0;
1147 listEnumerationTimer.tablesIndex = 0;
1148 delete listEnumerationTimer.row;
1149 delete listEnumerationTimer.matches;
1150 delete listEnumerationTimer.uniques;
1153 WordListPrepTimer wordListPrepTimer
1155 userData = this, delay = 0.1f;
1159 Row row = wordListPrepTimer.row;
1160 static int slice = 512;
1161 static StringSearchTable st = null;
1163 static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
1167 if(wordListPrepTimer.tablesIndex < searchTables.count)
1169 st = searchTables[wordListPrepTimer.tablesIndex];
1170 if(st.table && st.idField && st.searchFields && st.searchFields.count)
1172 wordListPrepRowNum = 0;
1173 wordListPrepRowCount = st.table.rowsCount;
1175 wordListPrepTimer.row = row = { st.table };
1176 DebugLn("building word list for ", st.table.name, " table ------------------------------------- ");
1183 Time delay = wordListPrepTimer.delay;
1184 Time lastTime = GetTime();
1188 PrintLn("indexing ", wordListPrepRowNum, " of ", wordListPrepRowCount, " --- slice is ", slice);
1190 for(c = 0; c<slice && (next = row.Next()); c++)
1193 row.GetData(st.idField, id);
1195 wordListPrepRowNum++;
1197 for(sf : st.searchFields)
1199 Field field = sf.field;
1200 StringSearchIndexingMethod method = sf.method;
1201 if(field && field.type == class(String))
1203 String string = null;
1204 row.GetData(field, string);
1206 if(string && string[0])
1207 ProcessWordListString(string, method, id);
1210 // todo: need to improve on this...
1211 // else ... call OnGetString of type ... etc...
1212 //PrintLn("todo: support other field types for string search");
1213 else if(field && field.type)
1215 char * n = field.name;
1216 char tempString[MAX_F_STRING];
1218 Class type = field.type;
1219 if(type.type == unitClass && !type.typeSize)
1221 Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1225 if(type.type == structClass)
1226 data = (int64)new0 byte[type.structSize];
1227 ((bool (*)())(void *)row.GetData)(row, field, type, (type.type == structClass) ? (void *)data : &data);
1229 if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1230 field.type._vTbl[__ecereVMethodID_class_OnGetString](field.type, &data, tempString, null, null);
1232 field.type._vTbl[__ecereVMethodID_class_OnGetString](field.type, (void *)data, tempString, null, null);
1235 ProcessWordListString(tempString, method, id);
1237 if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1238 type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1239 if(type.type == structClass)
1244 if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1247 delete wordListPrepTimer.row; row = null;
1248 next = ++wordListPrepTimer.tablesIndex < searchTables.count;
1254 char filePath[MAX_FILENAME];
1257 sprintf(filePath, "%s.search", table.name);
1258 // this doesn't want to work? :S :S :S
1260 f = FileOpenBuffered(filePath, read);
1267 wordListPrepTimer.hasCompleted = true;
1268 StopWordListPrepTimer();
1274 void StopWordListPrepTimer()
1276 wordListPrepTimer.Stop();
1277 wordListPrepTimer.tablesIndex = 0;
1278 delete wordListPrepTimer.row;
1283 DebugLn("TableEditor::~()");
1285 fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the incref in AddFieldBox? -- Free() does just that
1286 delete searchString;
1291 delete dynamicLookupEditors;
1292 delete dynamicLookupTableEditors;
1293 if(searchTables) searchTables.Free();
1294 delete searchTables;
1295 if(sqliteSearchTables) sqliteSearchTables.Free();
1296 delete sqliteSearchTables;
1299 void ResetListFields()
1301 DebugLn("TableEditor::ResetListFields");
1302 if(list && listFields && listFields.count)
1304 bool c = list.created;
1306 for(lf : listFields)
1308 list.AddField(lf.dataField);
1309 incref lf.dataField;
1314 void AddTableEditor(TableEditor tableEditor)
1316 DebugLn("TableEditor::AddTableEditor");
1317 if(!tableEditors.Find(tableEditor))
1319 tableEditors.Add(tableEditor);
1323 DebugLn(" TableEditor instance already added");
1326 void RemoveTableEditor(TableEditor tableEditor)
1328 Iterator<TableEditor> it { tableEditors };
1329 DebugLn("TableEditor::RemoveTableEditor");
1330 if(it.Find(tableEditor))
1332 it.Remove(); // tableEditors.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1333 delete tableEditor; // AddTableEditor increfs...
1336 DebugLn(" TableEditor instance not found, no need to remove");
1339 void AddFieldBox(FieldBox fieldBox)
1341 // I was putting this here to force autosize on the lists (e.g. the Radiologists fields):
1343 if(!fieldBox.autoSize)
1344 fieldBox.autoSize = true;
1346 DebugLn("TableEditor::AddFieldBox");
1347 if(!fieldsBoxes.Find(fieldBox))
1349 fieldsBoxes.Add(fieldBox);
1355 DebugLn(" FieldBox instance already added");
1358 void RemoveFieldBox(FieldBox fieldBox)
1360 Iterator<FieldBox> it { fieldsBoxes };
1361 DebugLn("TableEditor::RemoveFieldBox");
1362 if(it.Find(fieldBox))
1364 it.Remove(); // fieldsBoxes.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1367 DebugLn(" FieldBox instance not found, no need to remove");
1370 void InitFieldsBoxes()
1372 DebugLn("TableEditor::InitFieldsBoxes");
1375 for(fb : fieldsBoxes)
1383 for(fb : fieldsBoxes)
1386 //NotifyInitFields(master, this);
1391 DebugLn("TableEditor::EditNew");
1393 modifiedDocument = false;
1398 DebugLn("TableEditor::EditSave");
1399 internalModifications = true;
1400 for(fb : fieldsBoxes)
1403 if(idField && list && listFields && listFields.count)
1405 DataRow listRow = list.currentRow;
1406 // ADDED THIS HERE FOR SQLITE TO REFRESH
1407 editRow.Find(idField, middle, nil, listRow.tag);
1408 SetListRowFields(editRow, listRow, false);
1409 list.Sort(listSortField, listSortOrder);
1411 internalModifications = false;
1413 for(te : tableEditors)
1416 modifiedDocument = false;
1422 DebugLn("TableEditor::EditLoad");
1425 internalModifications = true;
1431 lu.row = { lu.findIndex ? lu.findIndex : lu.findField.table };
1432 if(lu.valueField && eClass_IsDerived(lu.valueField.type, class(Id)) &&
1433 lu.findField && eClass_IsDerived(lu.findField.type, class(Id)))
1436 editRow.GetData(lu.valueField, id);
1437 lu.row.Find(lu.findField, middle, nil, id);
1441 for(fb : fieldsBoxes)
1443 OnCreateDynamicLookupEditors();
1444 internalModifications = false;
1445 size = size; // This seems to be required to fix autoSize on entering order screen
1447 DebugLn(" TODO: implement virtual method TableEditor::OnSubEditorsLoad");
1449 modifiedDocument = false;
1455 DebugLn("TableEditor::EditClear");
1456 internalModifications = true;
1457 for(fb : fieldsBoxes)
1459 for(e : dynamicLookupTableEditors)
1461 for(e : tableEditors)
1463 tableEditors.Free();
1464 dynamicLookupTableEditors.Free();
1465 //dynamicLookupTableEditors.size = 0;
1466 internalModifications = false;
1468 DebugLn(" TODO: remove all sub table editors");
1470 modifiedDocument = false;
1474 void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection)
1476 // DebugLn("TableEditor::SetListRowFields");
1477 for(lf : listFields)
1479 if(lf.dataField && lf.field)
1481 if(eClass_IsDerived(lf.field.type, class(char*)))
1484 dbRow.GetData(lf.field, s);
1485 listRow.SetData(lf.dataField, s);
1488 else if(eClass_IsDerived(lf.field.type, class(Id)))
1494 dbRow.GetData(lf.field, id);
1495 s = lf.CustomLookup(id);
1496 listRow.SetData(lf.dataField, s);
1499 else if(lf.lookupFindField && (lf.lookupFindIndex || lf.lookupFindField.table) && lf.lookupValueField &&
1500 eClass_IsDerived(lf.lookupFindField.type, class(Id)) &&
1501 eClass_IsDerived(lf.lookupValueField.type, class(char*)))
1505 Row lookupRow { lf.lookupFindIndex ? lf.lookupFindIndex : lf.lookupFindField.table };
1506 dbRow.GetData(lf.field, id);
1507 if(lookupRow.Find(lf.lookupFindField, middle, nil, id))
1508 lookupRow.GetData(lf.lookupValueField, s);
1509 listRow.SetData(lf.dataField, s);
1514 else if(lf.CustomLookup && lf.field.type)
1516 char * n = lf.field.name;
1519 Class type = lf.field.type;
1520 if(type.type == unitClass && !type.typeSize)
1522 Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1526 if(type.type == structClass)
1527 data = (int64)new0 byte[type.structSize];
1528 ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1529 s = lf.CustomLookup((int)data);
1530 listRow.SetData(lf.dataField, s);
1531 if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1532 type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1533 if(type.type == structClass)
1537 else if(lf.field.type && eClass_IsDerived(lf.dataField.dataType, class(char*)))
1539 char * n = lf.field.name;
1540 char tempString[MAX_F_STRING];
1542 Class type = lf.field.type;
1543 if(type.type == unitClass && !type.typeSize)
1545 Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1549 if(type.type == structClass)
1550 data = (int64)new0 byte[type.structSize];
1551 ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1552 if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1553 lf.field.type._vTbl[__ecereVMethodID_class_OnGetString](lf.field.type, &data, tempString, null, null);
1555 lf.field.type._vTbl[__ecereVMethodID_class_OnGetString](lf.field.type, (void*)data, tempString, null, null);
1557 listRow.SetData(lf.dataField, tempString);
1559 if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1560 type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1561 if(type.type == structClass)
1564 else if(lf.field.type)
1566 char * n = lf.field.name;
1567 //char tempString[256];
1569 Class type = lf.field.type;
1570 if(type.type == unitClass && !type.typeSize)
1572 Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1576 if(type.type == structClass)
1577 data = (int64)new0 byte[type.structSize];
1578 ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1579 if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1580 listRow.SetData(lf.dataField, (void *)&data);
1582 listRow.SetData(lf.dataField, (void *)data);
1583 if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1584 type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1585 if(type.type == structClass)
1590 if(restoreSelection && !list.currentRow)
1593 if((select = list.FindRow(selectedId)))
1594 SelectListRow(select);
1598 Array<Id> SearchWordList()
1600 DebugLn("TableEditor::SearchWordList");
1601 #ifdef FULL_STRING_SEARCH
1607 WordEntry entries[256];
1608 Array<Id> results = null;
1609 if(searchTables && searchTables.count && searchString && searchString[0])
1611 char * searchCopy = CopyString(searchString);
1612 numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false);
1613 for(c = 0; c<numTokens; c++)
1615 len[c] = strlen(words[c]);
1617 entries[c] = (WordEntry)wordTree.FindString(words[c]);
1627 Map<Id, int> matches { };
1628 Map<Id, int> uniques { };
1629 MapNode<Id, int> mn;
1631 for(c = 0; c<numTokens; c++)
1633 if(entries[c] && entries[c].items && entries[c].items.count)
1635 for(i = 0; i<entries[c].items.count; i++)
1637 int count = uniques[entries[c].items.ids[i]];
1642 matches[entries[c].items.ids[i]]++;
1647 for(mn = matches.root.minimum; mn; mn = mn.next)
1650 results.Add(mn.key);
1656 else if(numTokens == 1)
1659 if(entries[0] && entries[0].items && entries[0].items.count)
1661 for(c = 0; c<entries[0].items.count; c++)
1662 results.Add(entries[0].items.ids[c]);
1674 // find a way to not load a tree for different searchFields
1675 // if the code that sets the searchFields has changed
1676 // store a search index signature containing following:
1677 // tables name, idField name and type, fields name and type
1678 void PrepareWordList()
1680 DebugLn("TableEditor::PrepareWordList");
1681 #ifdef FULL_STRING_SEARCH
1683 char filePath[MAX_FILENAME];
1686 sprintf(filePath, "%s.search", table.name);
1687 f = filePath ? FileOpenBuffered(filePath, read) : null;
1694 for(a = 0; a<26; a++)
1698 word[0] = 'a' + (char)a;
1701 letters[a] = (WordEntry)wordTree.FindString(word);
1702 for(b = 0; b<26; b++)
1704 word[1] = 'a' + (char)b;
1705 doubleLetters[a][b] = (WordEntry)wordTree.FindString(word);
1709 else if(searchTables && searchTables.count)
1714 for(a = 0; a<26; a++)
1718 word[0] = 'a' + (char)a;
1721 wordTree.Add((BTNode)(letters[a] = WordEntry { string = CopyString(word) }));
1722 for(b = 0; b<26; b++)
1724 word[1] = 'a' + (char)b;
1725 wordTree.Add((BTNode)(doubleLetters[a][b] = WordEntry { string = CopyString(word) }));
1730 wordListPrepTimer.tablesIndex = 0;
1731 wordListPrepTimer.Start();
1737 void ProcessWordListString(char * string, StringSearchIndexingMethod method, Id id)
1746 char asciiWord[1024];
1748 for(c = 0; ; c += nb)
1750 ch = UTF8GetChar(string + c, &nb);
1752 if(!ch || CharMatchCategories(ch, separators) ||
1753 (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
1758 asciiWord[numChars] = 0;
1762 AddWord(word, count, method == allSubstrings, id);
1763 if(count > numChars)
1764 AddWord(asciiWord, strlen(asciiWord), method == allSubstrings, id);
1768 if(!CharMatchCategories(ch, separators))
1771 for(cc = 0; cc < nb; cc++)
1772 word[count++] = string[c + cc];
1774 asciiWord[numChars++] = ToASCII(ch);
1782 for(cc = 0; cc < nb; cc++)
1783 word[count++] = string[c + cc];
1785 asciiWord[numChars++] = ToASCII(ch);
1791 /*static */WordEntryBinaryTree wordTree
1793 CompareKey = (void *)BinaryTree::CompareString;
1794 FreeKey = BinaryTree::FreeString;
1797 WordEntry letters[26];
1798 WordEntry doubleLetters[26][26];
1800 void AddWord(char * word, int count, bool addAllSubstrings, Id id)
1802 //DebugLn("TableEditor::AddWord");
1803 #ifdef FULL_STRING_SEARCH
1805 if(addAllSubstrings)
1808 WordEntry mainEntry = null;
1809 WordEntry sEntry = null;
1811 for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
1816 WordEntry lEntry = null;
1817 memcpy(subWord, word + s, count-s);
1818 subWord[count-s] = 0; // THIS IS REQUIRED!! THE WHILE LOOP BELOW CHECKED count-s FIRST!!
1821 for(l = count-s; l>0; l--)
1824 WordEntry start = null, wordEntry;
1826 while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
1831 if(ch1 >= 'a' && ch1 <= 'z')
1833 char ch2 = subWord[1];
1834 if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
1836 char ch2 = subWord[1];
1837 start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
1841 start = letters[ch1 - 'a'];
1848 while(start && (max = (WordEntry)((BTNode)start).maximum))
1850 if(strcmp(max.string, subWord) >= 0)
1852 start = start.parent;
1857 start = (WordEntry)wordTree.root;
1859 if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
1865 wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
1869 mainEntry = wordEntry;
1877 if(!wordEntry.words) wordEntry.words = IdList { };
1878 wordEntry.words.Add((Id)mainEntry);
1883 if(!wordEntry.words) wordEntry.words = IdList { };
1884 wordEntry.words.Add((Id)sEntry);
1888 if(!wordEntry.words) wordEntry.words = IdList { };
1889 wordEntry.words.Add((Id)lEntry);
1891 if(!wordEntry.items) wordEntry.items = IdList { };
1892 wordEntry.items.Add(id);
1898 WordEntry wordEntry;
1899 if(!(wordEntry = (WordEntry)(wordTree.root).FindString(word)))
1900 wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(word) }));
1901 if(!wordEntry.items) wordEntry.items = IdList { };
1902 wordEntry.items.Add(id);
1909 public class ListField : struct
1913 DataField dataField;
1914 Field lookupFindField;
1915 Field lookupValueField;
1916 Table lookupFindIndex;
1917 String (*CustomLookup)(Id);
1942 public class LookupEditor : struct
1945 subclass(TableEditor) editorClass;
1946 Window parentWindow;
1947 Field lookupValueField;
1948 Field lookupFindField;
1949 Field lookupIdField;
1950 Table lookupFindIndex;
1953 // all methods currently perform ascii conversion and all that jazz on every string added to the index
1954 public enum StringSearchIndexingMethod { fullString, allSubstrings };
1956 public class StringSearchField
1960 StringSearchIndexingMethod method;
1962 Field lookupFindField;
1963 Field lookupValueField;
1965 //String (*CustomRead)(Id);
1968 public class StringSearchTable
1973 Array<StringSearchField> searchFields;
1976 ~StringSearchTable()
1978 delete searchFields;
1982 public class SQLiteSearchField
1986 //StringSearchIndexingMethod method;
1989 public class SQLiteSearchTable
1994 Array<SQLiteSearchField> searchFields;
1997 ~SQLiteSearchTable()
1999 delete searchFields;
2003 static WordEntry * btnodes;
2005 struct WordEntryBinaryTree : BinaryTree
2007 WordEntry * entries;
2009 void OnSerialize(IOChannel channel)
2013 uint count = this.count;
2014 DebugLn("WordEntryBinaryTree::OnSerialize");
2015 for(id = 1, node = (WordEntry)root; node;)
2022 else if(node.parent)
2024 bool isLeft = node == node.parent.left;
2029 if(isLeft && node.right)
2035 isLeft = node == node.parent.left;
2044 channel.Serialize(id);
2045 channel.Serialize((WordEntry)root);
2048 void OnUnserialize(IOChannel channel)
2050 WordEntry root, node;
2052 DebugLn("WordEntryBinaryTree::OnUnserialize");
2053 channel.Unserialize(count);
2054 entries = new WordEntry[count];
2056 channel.Unserialize(root);
2057 this.root = (BTNode)root;
2058 // count = root ? this.root.count : 0;
2060 for(node = (WordEntry)root; node;)
2065 for(c = 0; c<node.words.count; c++)
2066 node.words.ids[c] = (Id)btnodes[node.words.ids[c] - 1];
2072 else if(node.parent)
2074 bool isLeft = node == node.parent.left;
2079 if(isLeft && node.right)
2085 isLeft = node == node.parent.left;
2097 class WordEntry : struct
2101 WordEntry left, right;
2114 void OnSerialize(IOChannel channel)
2116 #ifdef FULL_STRING_SEARCH
2119 channel.Serialize(id);
2120 channel.Serialize(string);
2121 channel.Serialize(items);
2126 channel.Serialize(words.count);
2127 for(c = 0; c < words.count; c++)
2129 uint id = ((WordEntry)words.ids[c]).id;
2130 channel.Serialize(id);
2136 channel.Serialize(none);
2139 // channel.Serialize(words);
2140 channel.Serialize(left);
2141 channel.Serialize(right);
2146 channel.Serialize(nothing);
2151 void OnUnserialize(IOChannel channel)
2153 #ifdef FULL_STRING_SEARCH
2155 channel.Unserialize(id);
2160 // TODO: Fix typed_object issues
2161 entry = btnodes[id - 1] = eInstance_New(class(WordEntry));
2162 this = (void *)entry;
2164 channel.Unserialize(string);
2165 channel.Unserialize(items);
2166 channel.Unserialize(words);
2168 channel.Unserialize(left);
2169 if(left) { left.parent = (void *)this; }
2170 channel.Unserialize(right);
2171 if(right) { right.parent = (void *)this; }
2173 // TODO: Precomp errors without extra brackets
2174 depth = ((BTNode)((void *)this)).depthProp;
2183 class ListEnumerationTimer : Timer
2191 Map<Id, int> uniques;
2194 class WordListPrepTimer : Timer
2202 class EnumerateThread : Thread
2221 bool abort, abortNow;
2228 //if(app.ProcessInput(true))
2231 Row r { editor.table };
2235 if(editor.listFields && editor.idField)
2237 /*for(c=0; c<matches.count && !abort; c++)
2239 if(r.Find(editor.idField, middle, nil, matches[c]))
2244 row = editor.list.AddRow();
2245 r.GetData(editor.idField, id);
2247 editor.SetListRowFields(r, row, true);
2251 DebugLn($"WordList match cannot be found in database.");
2254 else if(editor.idField && editor.stringField)
2256 /*for(c=0; c<matches.count && !abort; c++)
2258 if(r.Find(editor.idField, middle, nil, matches[c]))
2262 r.GetData(editor.idField, id);
2263 r.GetData(editor.stringField, s);
2265 editor.list.AddString(s).tag = id;
2270 DebugLn($"WordList match cannot be found in database.");
2276 else if(!editor.disabledFullListing)
2278 if(editor.listFields && editor.idField)
2281 while(r.Next() && !abort)
2286 r.GetData(editor.idField, id);
2287 //if(app.ProcessInput(true))
2291 row = editor.list.AddRow();
2293 editor.SetListRowFields(r, row, true);
2298 else if(editor.idField && editor.stringField)
2300 /*while(r.Next() && !abort)
2305 r.GetData(editor.idField, id);
2306 r.GetData(editor.stringField, s);
2307 editor.list.AddString(s).tag = id;
2319 editor.list.Sort(editor.listSortField, editor.listSortOrder);
2338 editor.list.Update(null);
2339 //app.Wait(); // Sleep(0.2f);
2340 //if(app.ProcessInput(true))
2343 //app.UpdateDisplay();
2350 static define app = ((GuiApplication)__thisModule);
2352 static inline void DebugLn(typed_object object, ...)
2354 #if defined(_DEBUG_LINE)
2357 va_start(args, object);
2358 PrintStdArgsToBuffer(buffer, sizeof(buffer), object, args);