bc93df08bf899c386244a087a746b2bc04358c52
[sdk] / eda / libeda / src / gui / TableEditor.ec
1 #include <stdarg.h>
2
3 import "idList"
4
5 import "FieldBox"
6
7 default:
8
9 extern int __ecereVMethodID_class_OnFree;
10 extern int __ecereVMethodID_class_OnGetString;
11 extern int __ecereVMethodID_class_OnGetDataFromString;
12
13 private:
14
15 #ifdef _DEBUG
16 // #define _DEBUG_LINE
17 #endif
18
19 #define FULL_STRING_SEARCH
20
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; }))
23
24 define newEntryStringDebug = $"New|id=";
25 define newEntryString = $"New";
26
27 public char ToASCII(unichar ch)
28 {
29    char asciiCH = 0;
30    if(ch > 127)
31    {
32       if(ch == 'À' || ch == 'Á' || ch == 'Â' || ch == 'Ã' || ch == 'Ä' || ch == 'Å')
33          asciiCH = 'A';
34       else if(ch == 'Ç')
35          asciiCH = 'C';
36       else if(ch == 'È' || ch == 'É' || ch == 'Ê' || ch == 'Ë')
37          asciiCH = 'E';
38       else if(ch == 'Ì' || ch == 'Í' || ch == 'Î' || ch == 'Ï')
39          asciiCH = 'I';
40       else if(ch == 'Ñ')
41          asciiCH = 'N';
42       else if(ch == 'Ò' || ch == 'Ó' || ch == 'Ô' || ch == 'Õ' || ch == 'Ö')
43          asciiCH = 'O';
44       else if(ch == 'Ù' || ch == 'Ú' || ch == 'Û' || ch == 'Ü')
45          asciiCH = 'U';
46       else if(ch == 'à' || ch == 'á' || ch == 'â' || ch == 'ã' || ch == 'ä' || ch == 'å')
47          asciiCH = 'a';
48       else if(ch == 'ç')
49          asciiCH = 'c';
50       else if(ch == 'è' || ch == 'é' || ch == 'ê' || ch == 'ë')
51          asciiCH = 'e';
52       else if(ch == 'ì' || ch == 'í' || ch == 'î' || ch == 'ï')
53          asciiCH = 'i';
54       else if(ch == 'ñ')
55          asciiCH = 'n';
56       else if(ch == 'ò' || ch == 'ó' || ch == 'ô' || ch == 'õ' || ch == 'ö')
57          asciiCH = 'o';
58       else if(ch == 'ù' || ch == 'ú' || ch == 'û' || ch == 'ü')
59          asciiCH = 'u';
60    }
61    else
62       asciiCH = (char)ch;
63    return asciiCH;
64 }
65
66 public char * ConvertToASCII(char * string, char * newString, int * len, bool lowerCase)
67 {
68    if(string)
69    {
70       unichar unich;
71       int nb;
72       int c, d = 0;
73       for(c = 0; (unich = UTF8GetChar(string + c, &nb)); c += nb)
74       {
75          char ch = ToASCII(unich);
76          if(ch < 128 && (ch == '-' || isalpha(ch) || ch == ','))
77             newString[d++] = lowerCase ? (char)tolower(ch) : (char)ch;
78       }
79       newString[d] = 0;
80       if(len) *len = d;
81    }
82    return null;
83 }
84
85 public class NoCaseAccent : SQLCustomFunction
86 {
87    void Process(char * text)
88    {
89       int len = text ? strlen(text) : 0;
90       array.size = len + 1;
91       ConvertToASCII(text ? text : "", array.array, &len, true);
92       array.count = len + 1;
93    }
94 }
95
96 public class MemberStringSample
97 {
98    String name;
99 }
100
101 default extern int __ecereVMethodID_class_OnUnserialize;
102
103 public class GetMemberString<class NT:void * = MemberStringSample, name = NT::name> : SQLCustomFunction
104 {
105    SerialBuffer buffer { };
106    void Process(char * text)
107    {
108       NT pn;
109       buffer.buffer = text;
110       buffer.count = MAXINT;
111       buffer.pos = 0;
112       class(NT)._vTbl[__ecereVMethodID_class_OnUnserialize](class(NT), &pn, buffer);
113       NoCaseAccent::Process((NoCaseAccent)this, (pn && pn.name) ? pn.name : null);
114       delete pn;
115       buffer.buffer = null;
116       buffer.count = 0;
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);
119    }
120 }
121
122 // Rename TableEditor to TableControl and move to eda/src/gui/controls
123 public class TableEditor : public Window
124 {
125    bool initialized;
126 public:
127    property Table table
128    {
129       set
130       {
131          DebugLn("TableEditor::table|set");
132          table = value;
133       }
134    }
135    Table table;
136
137    property Table index
138    {
139       set
140       {
141          DebugLn("TableEditor::index|set");
142          index = value;
143          filterRow.tbl = index;
144       }
145    }
146    Table index;
147
148    bool OnPostCreate()
149    {
150       DebugLn("TableEditor::OnPostCreate");
151       if(table)
152       {
153          if(!initialized)
154          {
155             ResetListFields();
156             if(searchTables)
157                PrepareWordList();
158             InitFieldsBoxes(); // IMPORTANT: table must be set *AFTER* all related FieldEditors have been initialized
159             {
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);
167                editRow.tbl = table;
168                if(searchTables)
169                   PrepareWordList();
170             }
171             initialized = true;
172             OnInitialize();
173          }
174          if(!listEnumerationTimer.hasCompleted)
175             Enumerate();
176          if(list && !list.currentRow)
177             list.SelectRow(list.firstRow); // should the tableeditor select method be used here?
178       }
179       return true;
180    }
181
182    bool OnClose(bool parentClosing)
183    {
184       bool result = NotifyClosing();
185       if(result)
186       {
187          EditClear();
188       }
189       return result;
190    }
191
192    // List
193    property ListBox list
194    {
195       set
196       {
197          DebugLn("TableEditor::list|set");
198          list = value;
199          //ResetListFields();
200       }
201    }
202    Field idField;
203    Field stringField;
204    Field indexFilterField;
205
206    ListBox list;
207    property Array<ListField> listFields
208    {
209       set
210       {
211          DebugLn("TableEditor::listFields|set");
212          listFields = value;
213          //ResetListFields();
214       }
215    }
216    Array<ListField> listFields;
217    int listSortOrder;
218    DataField listSortField;
219    bool disabledFullListing;
220
221    property Array<StringSearchField> searchFields
222    {
223       set
224       {
225          StringSearchTable searchTable { table, idField, value };
226          DebugLn("TableEditor::searchFields|set");
227          // The searchTables property will incref...
228          property::searchTables = { [ searchTable ] };
229       }
230    }
231
232    property Array<StringSearchTable> searchTables
233    {
234       set
235       {
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
238          incref value;
239          for(t : value) { incref t; }
240          DebugLn("TableEditor::searchTables|set");
241          if(searchTables) searchTables.Free();
242          delete searchTables;
243          searchTables = value;
244       }
245    }
246    Array<StringSearchTable> searchTables;
247
248    property Array<SQLiteSearchTable> sqliteSearchTables
249    {
250       set
251       {
252          incref value;
253          for(t : value) { incref t; }
254          DebugLn("TableEditor::searchTables|set");
255          if(sqliteSearchTables) sqliteSearchTables.Free();
256          delete sqliteSearchTables;
257          sqliteSearchTables = value;
258       }
259    }
260    Array<SQLiteSearchTable> sqliteSearchTables;
261
262    property String searchString
263    {
264       set
265       {
266          bool modified = modifiedDocument;
267          DebugLn("TableEditor::searchString|set");
268          switch(modified ? OnLeavingModifiedDocument() : no)
269          {
270             case cancel:
271                break;
272             case yes:
273                EditSave();
274             case no:
275                if(modified)
276                   EditLoad();
277                delete searchString;
278                if(value && value[0])
279                   searchString = CopyString(value);
280                Enumerate();
281                break;
282          }
283       }
284    }
285    String searchString;
286
287    Map<Table, Lookup> lookups;
288
289    Array<LookupEditor> dynamicLookupEditors;
290    property Array<LookupEditor> dynamicLookupEditors
291    {
292       set
293       {
294          DebugLn("TableEditor::dynamicLookupEditors|set");
295          dynamicLookupEditors = value;
296       }
297    }
298
299    // Fields Editor
300    property Id selectedId { get { return selectedId; } }
301
302    Array<FieldBox> fieldsBoxes { };
303    Array<TableEditor> tableEditors { };
304    Array<TableEditor> dynamicLookupTableEditors { };
305
306    bool readOnly;
307    
308    public virtual void OnInitialize();
309    public virtual void OnLoad();
310    public virtual void OnStateChanged();
311    bool internalModifications;
312    public void NotifyModifiedDocument()
313    {
314       DebugLn("TableEditor::NotifyModifiedDocument");
315       if(!internalModifications)
316          OnStateChanged();
317    }
318
319    //public virtual bool Window::NotifyNew(AltListSection listSection, Row r);
320    //virtual void Window::NotifyInitFields(AltEditSection editSection);
321    
322    public virtual DialogResult OnLeavingModifiedDocument()
323    {
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?"
327                   }.Modal();
328    }
329    
330    public virtual bool OnRemovalRequest()
331    {
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?"
336                   }.Modal() == yes;
337    }
338
339    //public virtual void Window::NotifyDeleting(ListSection listSection);
340    //public virtual void Window::NotifyDeleted(ListSection listSection);
341
342    public bool NotifyClosing()
343    {
344       bool result = true;
345       DebugLn("TableEditor::NotifyClosing");
346       if(modifiedDocument)
347       {
348          switch(OnLeavingModifiedDocument())
349          {
350             case cancel:
351                result = false;
352                break;
353             case yes:
354                EditSave();
355             case no:
356                EditLoad();
357                break;
358          }
359       }
360       if(result)
361       {
362          StopWordListPrepTimer();
363          StopListEnumerationTimer();
364       }
365       return result;
366    }
367
368    //public void List() // this gets called out of nowhere by some constructor thing...
369    public void Enumerate()
370    {
371       DebugLn("TableEditor::Enumerate");
372       if(list)
373       {
374          StopListEnumerationTimer();
375          list.Clear();
376          EditClear();
377       }
378       if(list || OnList != TableEditor::OnList)
379       {
380          Row r { table };
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);
395          OnList(r, matches);
396          delete matches;
397          delete r;
398       }
399       modifiedDocument = false; // setting this here is not really logical, enumeration and modified have nothing to do with eachother
400    }
401
402    virtual void OnList(Row r, Array<Id> matches)
403    {
404       DebugLn("TableEditor::OnList");
405       if(!listEnumerationTimer.started)
406       {
407          listEnumerationTimer.hasCompleted = false;
408          listEnumerationTimer.matchesIndex = 0;
409          listEnumerationTimer.tablesIndex = 0;
410          if(!listEnumerationTimer.sqliteSearch)
411             listEnumerationTimer.row = Row { r.tbl };
412          if(matches)
413          {
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);
418          }
419          else
420             listEnumerationTimer.matches = null;
421          listEnumerationTimer.Start();
422       }
423       else
424          DebugLn("TableEditor::OnList -- timer state error");
425    }
426
427    virtual void OnCreateDynamicLookupEditors()
428    {
429       DebugLn("TableEditor::OnCreateLookupEditors");
430       if(dynamicLookupEditors && dynamicLookupEditors.count)
431       {
432          for(f : dynamicLookupEditors)
433          {
434             if(f.editorClass && f.parentWindow && f.lookupFindField)
435             {
436                Row row { f.lookupFindIndex ? f.lookupFindIndex : f.lookupFindField.table };
437                // todo: make this work for all types
438                uint id = 0;
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())
442                {
443                   // todo: make this work for all types, although this is meant to be an id field
444                   uint id = 0;
445                   TableEditor editor = eInstance_New(f.editorClass);
446                   incref editor;
447                   editor.parent = f.parentWindow;
448                   editor.master = this;
449                   dynamicLookupTableEditors.Add(editor);
450                   editor.Create();
451                   row.GetData(f.lookupIdField, id);
452                   editor.Select(id);
453                }
454                delete row;
455             }
456          }
457       }
458    }
459
460    TableEditor masterEditor;
461
462    public property TableEditor masterEditor
463    {
464       set
465       {
466          if(value != masterEditor)
467          {
468             if(masterEditor)
469                masterEditor.RemoveTableEditor(this);
470             masterEditor = value;
471             if(value)
472                value.AddTableEditor(this);
473          }
474       }
475    }
476
477    watch(parent)
478    {
479       if(eClass_IsDerived(parent._class, class(TableEditor)))
480          property::masterEditor = (TableEditor)parent;
481    };
482
483    watch(master)
484    {
485       if(eClass_IsDerived(master._class, class(TableEditor)))
486          property::masterEditor = (TableEditor)master;
487    };
488
489    void CreateRow()
490    {
491       DebugLn("TableEditor::CreateRow");
492       //list.NotifySelect(this, list, null, 0);
493       if(!modifiedDocument)
494       {
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
500          DataRow row = null;
501          String newText;
502
503          /*uint count = editRow.tbl.GetRowsCount();
504
505          id = 0;
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
510          {
511             r.GetData(idField, id);
512             id++;
513          }
514          else
515             id = 1;
516
517          EditClear();
518          {
519             bool active = true;
520             r.Add();
521             {
522                // Patch for SQLite driver which auto-increments IDs
523                int curId = 0;
524                if(r.GetData(idField, curId))
525                   id = curId;
526                else
527                   r.SetData(idField, id);
528             }
529             /*if(fldActive)
530                r.SetData(fldActive, active);*/
531
532 #ifdef _DEBUG
533             newText = PrintString("[", newEntryStringDebug, id, "]");
534 #else
535             newText = PrintString("[", newEntryString, "]");
536 #endif
537
538             //if(NotifyNew(master, this, r))
539             if(listFields && idField)
540             {
541                for(lf : listFields)
542                {
543                   if(lf.dataField && lf.field)
544                   {
545                      if(lf.field.type == class(String))
546                         r.SetData(lf.field, newText);
547                      else // this whole block is new?
548                      {
549                         if(lf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])
550                         {
551                            Class dataType = lf.field.type;
552                            int64 dataHolder = 0;
553                            void * data = &dataHolder;
554
555                            if(dataType && dataType.type == structClass)
556                            {
557                               dataHolder = (int64)new0 byte[dataType.structSize];
558                               data = (void *)dataHolder;
559                            }
560                            /*else if(dataType && (dataType.type == noHeadClass || dataType.type == normalClass))
561                            {
562                               if(eClass_IsDerived(dataType, class(char*)))
563                                  dataHolder = (int64)CopyString("");
564                               else
565                                  dataHolder = (int64)eInstance_New(dataType);
566                               data = (void *)&dataHolder;
567                            }
568                            else
569                            {
570                               dataHolder = 0;
571                               data = &dataHolder;
572                            }*/
573                            if(data)
574                               dataType._vTbl[__ecereVMethodID_class_OnGetDataFromString](dataType, data, newText);
575
576
577                            /*dataType._vTbl[__ecereVMethodID_class_OnFree](dataType, dataHolder);
578                            if(dataType.type == structClass)
579                            {
580                               void * dataPtr = (void *)dataHolder;
581                               delete dataPtr;
582                            }
583                            dataHolder = 0;*/
584                         }
585                      }
586                   }
587                }
588                if(list)
589                {
590                   row = list.AddRow();
591                   row.tag = id;
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()
594                   //r.Next();
595                   //r.tbl.db.Commit();
596                   //editRow.Synch(r);
597                   //r.Last();
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);
601                }
602             }
603             else if(idField && stringField)
604             {
605                r.SetData(stringField, newText);
606                if(list)
607                {
608                   row = list.AddString(newText);
609                   row.tag = id;
610                }
611             }
612             //delete r;
613             delete newText;
614          }
615
616          if(list)
617          {
618             list.Sort(listSortField, listSortOrder);
619             if(row) SelectListRow(row);
620          }
621          OnStateChanged();
622       }
623    }
624
625    void Remove()
626    {
627       DebugLn("TableEditor::Remove");
628       if(editRow.sysID) //list && list.currentRow)
629       {
630          if(OnRemovalRequest())
631          {
632             editRow.Delete();
633             if(list)
634                list.DeleteRow(list.currentRow);
635             EditClear();
636             //NotifyDeleted(master, this);
637             if(list)
638                SelectListRow(list.currentRow);
639             OnStateChanged();
640          }
641       }
642    }
643
644    void Load()
645    {
646       DebugLn("TableEditor::Load");
647       EditLoad();
648    }
649
650    void Write()
651    {
652       DebugLn("TableEditor::Write");
653       EditSave();
654    }
655
656    bool ListSelect(DataRow row)
657    {
658       bool result = true;
659       DebugLn("TableEditor::ListSelect");
660       if(/*-row && -*/row != lastRow)
661       {
662          uint id;
663          if(modifiedDocument)
664          {
665             if(row)
666                list.currentRow = lastRow;
667             result = false;
668             switch(OnLeavingModifiedDocument())
669             {
670                case cancel:
671                   break;
672                case yes:
673                   EditSave();
674                case no:
675                   EditClear();
676                   list.currentRow = row;
677                   break;
678             }
679          }
680          if(list.currentRow == row)
681             SelectListRow(row);
682       }
683       return result;
684    }
685
686    bool Select(Id id)
687    {
688       bool result;
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))
692       {
693          //Id test = editRow.sysID;
694          selectedId = editRow.sysID;
695          EditLoad();
696          result = true;
697       }
698       else
699          result = false;
700       return result;
701    }
702
703    bool Filter(Id id)
704    {
705       bool result;
706       DebugLn("TableEditor::Filter");
707       if(selectedId && index && indexFilterField)
708       {
709          for(filterRow.Find(indexFilterField, middle, nil, id); !filterRow.nil; filterRow.Next())
710          {
711             Id id2;
712             filterRow.GetData(idField, id2);
713             if(id2 == selectedId)
714             {
715                filtered = true;
716                result = true;
717                break;
718             }
719          }
720       }
721       else
722          result = false;
723       return result;
724    }
725
726    bool SelectNext()
727    {
728       bool result;
729       bool wasNil = !editRow.sysID;
730       DebugLn("TableEditor::SelectNext");
731       // How about confirmation / saving before changing the entry?
732       if(filtered)
733       {
734          if(filterRow.Next())
735          {
736             if(wasNil && filterRow.sysID == selectedId)
737                filterRow.Next();
738             editRow.sysID = filterRow.sysID;
739          }
740          else
741             editRow.sysID = 0;
742       }
743       else
744          editRow.Next();
745       if(!editRow.nil)
746       {
747          //Id test = editRow.sysID;
748          selectedId = editRow.sysID;
749          EditLoad();
750          result = true;
751       }
752       else
753       {
754          result = false;
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();
758       }
759       return result;
760    }
761    
762    bool SelectPrevious()
763    {
764       bool result;
765       bool wasNil = !editRow.sysID;
766       DebugLn("TableEditor::SelectPrevious");
767       if(filtered)
768       {
769          if(filterRow.Previous())
770          {
771             if(wasNil && filterRow.sysID == selectedId)
772                filterRow.Previous();
773             editRow.sysID = filterRow.sysID;
774          }
775          else
776             editRow.sysID = 0;
777       }
778       else
779          editRow.Previous();
780       if(!editRow.nil)
781       {
782          //Id test = editRow.sysID;
783          selectedId = editRow.sysID;
784          EditLoad();
785          result = true;
786       }
787       else
788       {
789          result = false;
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
792          //editRow.Next();
793       }
794       return result;
795    }
796    
797    void SelectListRow(DataRow row)
798    {
799       // Time startTime = GetTime();
800       DebugLn("TableEditor::SelectListRow");
801       if(row)
802       {
803          selectedId = row.tag;
804          lastRow = row;
805
806          if(list.currentRow != row)
807             list.currentRow = row;
808          if(idField && editRow.Find(idField, middle, nil, selectedId))
809          {
810             //Id test = editRow.sysID;
811             listRow = row;
812             //NotifySelectListRow(master, this, selectedId);
813             EditLoad();
814          }
815       }
816       // Logf("SelectListRow took %f seconds\n", GetTime() - startTime);
817    }
818
819 private:
820    Row editRow { };
821    DataRow listRow;
822    DataRow lastRow;
823    Id selectedId;
824    Row filterRow { };
825    bool filtered;
826    Array<char> searchCI { };
827
828    ListEnumerationTimer listEnumerationTimer
829    {
830       userData = this, delay = 0.1f;
831       bool DelayExpired()
832       {
833          static Time startTime;
834          bool next = false;
835          int c;
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;
841
842          static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
843          ticks++;
844          if(ticks % 10 == 0)
845             PrintLn("listing... ");
846
847          if(matches)
848          {
849             int index = listEnumerationTimer.matchesIndex;
850             if(listFields && idField)
851             {
852                for(c=0; c<slice && (next = index++<matches.count); c++)
853                {
854                   if(row.Find(idField, middle, nil, matches[index]))
855                   {
856                      Id id = 0;
857                      DataRow dataRow = list.AddRow();
858                      row.GetData(idField, id);
859                      dataRow.tag = id;
860                      SetListRowFields(row, dataRow, true);
861                   }
862                   else
863                      DebugLn($"WordList match cannot be found in database.");
864                }
865                listEnumerationTimer.matchesIndex = index;
866                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
867             }
868             else if(idField && stringField)
869             {
870                for(c=0; c<slice && (next = index++<matches.count); c++)
871                {
872                   if(row.Find(idField, middle, nil, matches[index]))
873                   {
874                      Id id = 0;
875                      String s = null;
876                      row.GetData(idField, id);
877                      row.GetData(stringField, s);
878                      list.AddString(s).tag = id;
879                      delete s;
880                   }
881                   else
882                      DebugLn($"WordList match cannot be found in database.");
883                }
884                listEnumerationTimer.matchesIndex = index;
885                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
886             }
887          }
888          else if(listEnumerationTimer.sqliteSearch)
889          {
890             static SQLiteSearchTable st = null;
891             Row lookupRow { table };
892             Map<Id, int> uniques = listEnumerationTimer.uniques;
893             if(!row)
894             {
895                if(listEnumerationTimer.tablesIndex < sqliteSearchTables.count)
896                {
897                   char queryString[4096*4];
898
899                   if(!listEnumerationTimer.uniques)
900                      listEnumerationTimer.uniques = uniques = { };
901
902                   st = sqliteSearchTables[listEnumerationTimer.tablesIndex];
903                   if(st.table && st.idField && st.searchFields && st.searchFields.count)
904                   {
905                      wordListPrepRowNum = 0;
906                      wordListPrepRowCount = st.table.rowsCount;
907
908                      if(st.table && st.idField && st.searchFields && st.searchFields.count &&
909                            st.searchFields[0].field)
910                      {
911                         bool first = true;
912                         int bindId = 0;
913
914                         listEnumerationTimer.row = row = { st.table };
915
916                         {
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;
921                         }
922
923                         sprintf(queryString, "SELECT ROWID, * FROM `%s`", st.table.name);
924                         strcat(queryString, " WHERE ");
925                         for(sf : st.searchFields)
926                         {
927                            if(sf.field)
928                            {
929                               if(!first)
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"))
934                               {
935 #ifdef PERSON_SEARCH_LIKE
936                                  String ln = null, fn = null, mn = null;
937                                  char * comma = strchr(searchCI.array, ',');
938                                  if(comma)
939                                  {
940                                     int len = comma - searchCI.array;
941                                     ln = new char[len + 1];
942                                     memcpy(ln, searchCI.array, len);
943                                     ln[len] = 0;
944
945                                     fn = CopyString(comma+1);
946                                     {
947                                        int c;
948                                        for(c = strlen(fn)-2; c > 0; c--)
949                                        {
950                                           if(fn[c] == ' ')
951                                           {
952                                              mn = CopyString(fn + c + 1);
953                                              fn[c] = 0;
954                                           }
955                                        }
956                                     }
957                                  }
958                                  else
959                                     ln = CopyString(searchCI.array);
960
961                                  /* We could simply do this if we had access to PersonName: (don't forget the delete pn; below)
962                                  PersonName pn;
963                                  pn.OnGetDataFromString(searchCI.array);
964                                  */
965                                  if(ln && !fn && !mn)
966                                  {
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);
970                                  }
971                                  else if(ln || fn || mn)
972                                  {
973                                     // Otherwise search is in respective fields only (all specified must match)
974                                     bool first = true;
975                                     strcatf(queryString, "(");
976                                     if(ln)
977                                     {
978                                        if(!first) strcatf(queryString, " AND ");
979                                        first = false;
980                                        strcatf(queryString, "PNLastName(`%s`) LIKE '%%%s%%'", sf.field.name, ln);
981                                     }
982                                     if(fn)
983                                     {
984                                        if(!first) strcatf(queryString, " AND ");
985                                        first = false;
986                                        strcatf(queryString, "PNFirstName(`%s`) LIKE '%%%s%%'", sf.field.name, fn);
987                                     }
988                                     if(mn)
989                                     {
990                                        if(!first) strcatf(queryString, " AND ");
991                                        first = false;
992                                        strcatf(queryString, "PNMiddleName(`%s`) LIKE '%%%s%%'", sf.field.name, mn);
993                                     }
994                                     strcatf(queryString, ")");
995                                  }
996                                  //delete pn;
997                                  delete ln; delete fn; delete mn;
998 #else
999                                  // To use CompPersonName::OnCompare:
1000                                  strcatf(queryString, "`%s` = ?", sf.field.name);
1001 #endif
1002                               }
1003                               else
1004                                  strcatf(queryString, "`%s` LIKE '%%%s%%'", sf.field.name, searchString);
1005                               first = false;
1006                            }
1007                         }
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)
1014                         {
1015                            if(sf.field)
1016                            {
1017                               if(strstr(sf.field.type.name, "PersonName"))
1018                               {
1019                                  void * pn;
1020                                  sf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString](sf.field.type, &pn, searchCI.array);
1021                                  row.SetQueryParamObject(++bindId, pn, sf.field.type);
1022                                  bindId++;
1023                                  delete pn;
1024                               }
1025                               first = false;
1026                            }
1027                         }
1028 #endif
1029                         if(bindId)
1030                            row.Next();
1031                      }
1032                   }
1033                }
1034             }
1035             if(row)
1036             {
1037                if(listFields && idField)
1038                {
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())
1043                   {
1044                      Id id = 0;
1045                      row.GetData(st.idField, id);
1046                      //if(uniques[id]++ == 0)
1047                      if(uniques[id] == 0)
1048                      {
1049                         DataRow dataRow = list.AddRow();
1050                         dataRow.tag = id;
1051                         if(st.table == table)
1052                            SetListRowFields(row, dataRow, true);
1053                         else if(lookupRow.Find(idField, middle, nil, id))
1054                            SetListRowFields(lookupRow, dataRow, true);
1055                         else
1056                            PrintLn("no");
1057                      }
1058                      uniques[id] = uniques[id] + 1;
1059                   }
1060                   if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1061                   else
1062                   {
1063                      delete listEnumerationTimer.row; row = null;
1064                      next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1065                   }
1066                }
1067                else if(idField && stringField)
1068                {
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())
1073                   {
1074                      Id id = 0;
1075                      row.GetData(st.idField, id);
1076                      //if(uniques[id]++ == 0)
1077                      if(uniques[id] == 0)
1078                      {
1079                         String s = null;
1080                         if(st.table == table)
1081                            row.GetData(stringField, s);
1082                         else if(lookupRow.Find(idField, middle, nil, id))
1083                            lookupRow.GetData(stringField, s);
1084                         else
1085                            PrintLn("no");
1086                         list.AddString(s).tag = id;
1087                         delete s;
1088                      }
1089                      uniques[id] = uniques[id] + 1;
1090                   }
1091                   if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1092                   else
1093                   {
1094                      delete listEnumerationTimer.row; row = null;
1095                      next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1096                   }
1097                }
1098             }
1099             delete lookupRow;
1100          }
1101          else if(!disabledFullListing)
1102          {
1103             if(listFields && idField)
1104             {
1105                for(c = 0; c<slice && (next = row.Next()); c++)
1106                {
1107                   Id id = 0;
1108                   DataRow dataRow = list.AddRow();
1109                   row.GetData(idField, id);
1110                   dataRow.tag = id;
1111                   SetListRowFields(row, dataRow, true);
1112                   //app.UpdateDisplay();
1113                }
1114                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1115             }
1116             else if(idField && stringField)
1117             {
1118                for(c = 0; c<slice && (next = row.Next()); c++)
1119                {
1120                   Id id = 0;
1121                   String s = null;
1122                   row.GetData(idField, id);
1123                   row.GetData(stringField, s);
1124                   list.AddString(s).tag = id;
1125                   delete s;
1126                }
1127                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1128             }
1129          }
1130
1131          list.Sort(listSortField, listSortOrder);
1132
1133          if(!next)
1134          {
1135             listEnumerationTimer.hasCompleted = true;
1136             StopListEnumerationTimer();
1137          }
1138          if(startTime) PrintLn("*** Search took ", (uint)((GetTime() - startTime) * 1000), "ms ***");
1139          return true;
1140       }
1141    };
1142
1143    void StopListEnumerationTimer()
1144    {
1145       listEnumerationTimer.Stop();
1146       listEnumerationTimer.matchesIndex = 0;
1147       listEnumerationTimer.tablesIndex = 0;
1148       delete listEnumerationTimer.row;
1149       delete listEnumerationTimer.matches;
1150       delete listEnumerationTimer.uniques;
1151    }
1152
1153    WordListPrepTimer wordListPrepTimer
1154    {
1155       userData = this, delay = 0.1f;
1156       bool DelayExpired()
1157       {
1158          bool next = false;
1159          Row row = wordListPrepTimer.row;
1160          static int slice = 512;
1161          static StringSearchTable st = null;
1162
1163          static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
1164
1165          if(!row)
1166          {
1167             if(wordListPrepTimer.tablesIndex < searchTables.count)
1168             {
1169                st = searchTables[wordListPrepTimer.tablesIndex];
1170                if(st.table && st.idField && st.searchFields && st.searchFields.count)
1171                {
1172                   wordListPrepRowNum = 0;
1173                   wordListPrepRowCount = st.table.rowsCount;
1174
1175                   wordListPrepTimer.row = row = { st.table };
1176                   DebugLn("building word list for ", st.table.name, " table ------------------------------------- ");
1177                }
1178             }
1179          }
1180          if(row)
1181          {
1182             int c;
1183             Time delay = wordListPrepTimer.delay;
1184             Time lastTime = GetTime();
1185
1186             ticks++;
1187             if(ticks % 10 == 0)
1188                PrintLn("indexing ", wordListPrepRowNum, " of ", wordListPrepRowCount, " --- slice is ", slice);
1189
1190             for(c = 0; c<slice && (next = row.Next()); c++)
1191             {
1192                Id id = 0;
1193                row.GetData(st.idField, id);
1194
1195                wordListPrepRowNum++;
1196
1197                for(sf : st.searchFields)
1198                {
1199                   Field field = sf.field;
1200                   StringSearchIndexingMethod method = sf.method;
1201                   if(field && field.type == class(String))
1202                   {
1203                      String string = null;
1204                      row.GetData(field, string);
1205
1206                      if(string && string[0])
1207                         ProcessWordListString(string, method, id);
1208                      delete string;
1209                   }
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)
1214                   {
1215                      char * n = field.name;
1216                      char tempString[MAX_F_STRING];
1217                      int64 data = 0;
1218                      Class type = field.type;
1219                      if(type.type == unitClass && !type.typeSize)
1220                      {
1221                         Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1222                         if(dataType)
1223                            type = dataType;
1224                      }
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);
1228
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);
1231                      else
1232                         field.type._vTbl[__ecereVMethodID_class_OnGetString](field.type, (void *)data, tempString, null, null);
1233
1234                      if(tempString[0])
1235                         ProcessWordListString(tempString, method, id);
1236
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)
1240                         delete data;
1241                   }
1242                }
1243             }
1244             if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1245             else
1246             {
1247                delete wordListPrepTimer.row; row = null;
1248                next = ++wordListPrepTimer.tablesIndex < searchTables.count;
1249             }
1250          }
1251
1252          if(!next)
1253          {
1254             char filePath[MAX_FILENAME];
1255             File f;
1256
1257             sprintf(filePath, "%s.search", table.name);
1258             // this doesn't want to work? :S :S :S
1259             // f == 0x0
1260             f = FileOpenBuffered(filePath, read);
1261             if(f)
1262             {
1263                f.Put(wordTree);
1264                delete f;
1265             }
1266
1267             wordListPrepTimer.hasCompleted = true;
1268             StopWordListPrepTimer();
1269          }
1270          return true;
1271       }
1272    };
1273
1274    void StopWordListPrepTimer()
1275    {
1276       wordListPrepTimer.Stop();
1277       wordListPrepTimer.tablesIndex = 0;
1278       delete wordListPrepTimer.row;
1279    }
1280
1281    ~TableEditor()
1282    {
1283       DebugLn("TableEditor::~()");
1284
1285       fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the incref in AddFieldBox? -- Free() does just that
1286       delete searchString;
1287       wordTree.Free();
1288
1289       delete listFields;
1290       delete lookups;
1291       delete dynamicLookupEditors;
1292       delete dynamicLookupTableEditors;
1293       if(searchTables) searchTables.Free();
1294       delete searchTables;
1295       if(sqliteSearchTables) sqliteSearchTables.Free();
1296       delete sqliteSearchTables;
1297    }
1298
1299    void ResetListFields()
1300    {
1301       DebugLn("TableEditor::ResetListFields");
1302       if(list && listFields && listFields.count)
1303       {
1304          bool c = list.created;
1305          list.ClearFields();
1306          for(lf : listFields)
1307          {
1308             list.AddField(lf.dataField);
1309             incref lf.dataField;
1310          }
1311       }
1312    }
1313
1314    void AddTableEditor(TableEditor tableEditor)
1315    {
1316       DebugLn("TableEditor::AddTableEditor");
1317       if(!tableEditors.Find(tableEditor))
1318       {
1319          tableEditors.Add(tableEditor);
1320          incref tableEditor;
1321       }
1322       else
1323          DebugLn("   TableEditor instance already added");
1324    }
1325
1326    void RemoveTableEditor(TableEditor tableEditor)
1327    {
1328       Iterator<TableEditor> it { tableEditors };
1329       DebugLn("TableEditor::RemoveTableEditor");
1330       if(it.Find(tableEditor))
1331       {
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...
1334       }
1335       else
1336          DebugLn("   TableEditor instance not found, no need to remove");
1337    }
1338
1339    void AddFieldBox(FieldBox fieldBox)
1340    {
1341       // I was putting this here to force autosize on the lists (e.g. the Radiologists fields):
1342       /*
1343       if(!fieldBox.autoSize)
1344          fieldBox.autoSize = true;
1345       */
1346       DebugLn("TableEditor::AddFieldBox");
1347       if(!fieldsBoxes.Find(fieldBox))
1348       {
1349          fieldsBoxes.Add(fieldBox);
1350          if(table)
1351             fieldBox.Init();
1352          incref fieldBox;
1353       }
1354       else
1355          DebugLn("   FieldBox instance already added");
1356    }
1357
1358    void RemoveFieldBox(FieldBox fieldBox)
1359    {
1360       Iterator<FieldBox> it { fieldsBoxes };
1361       DebugLn("TableEditor::RemoveFieldBox");
1362       if(it.Find(fieldBox))
1363       {
1364          it.Remove(); // fieldsBoxes.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1365       }
1366       else
1367          DebugLn("   FieldBox instance not found, no need to remove");
1368    }
1369
1370    void InitFieldsBoxes()
1371    {
1372       DebugLn("TableEditor::InitFieldsBoxes");
1373       if(readOnly)
1374       {
1375          for(fb : fieldsBoxes)
1376          {
1377             fb.readOnly = true;
1378             fb.Init();
1379          }
1380       }
1381       else
1382       {
1383          for(fb : fieldsBoxes)
1384             fb.Init();
1385       }
1386       //NotifyInitFields(master, this);
1387    }
1388
1389    void EditNew()
1390    {
1391       DebugLn("TableEditor::EditNew");
1392
1393       modifiedDocument = false;
1394    }
1395
1396    void EditSave()
1397    {
1398       DebugLn("TableEditor::EditSave");
1399       internalModifications = true;
1400       for(fb : fieldsBoxes)
1401          fb.Save();
1402
1403       if(idField && list && listFields && listFields.count)
1404       {
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);
1410       }
1411       internalModifications = false;
1412
1413       for(te : tableEditors)
1414          te.EditSave();
1415
1416       modifiedDocument = false;
1417       OnStateChanged();
1418    }
1419
1420    void EditLoad()
1421    {
1422       DebugLn("TableEditor::EditLoad");
1423       EditClear();
1424       OnLoad();
1425       internalModifications = true;
1426       for(lu : lookups)
1427       {
1428          if(&lu == table)
1429          {
1430             if(!lu.row)
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)))
1434             {
1435                Id id = 0;
1436                editRow.GetData(lu.valueField, id);
1437                lu.row.Find(lu.findField, middle, nil, id);
1438             }
1439          }
1440       }
1441       for(fb : fieldsBoxes)
1442          fb.Load();
1443       OnCreateDynamicLookupEditors();
1444       internalModifications = false;
1445       size = size;   // This seems to be required to fix autoSize on entering order screen
1446
1447       DebugLn("   TODO: implement virtual method TableEditor::OnSubEditorsLoad");
1448
1449       modifiedDocument = false;
1450       OnStateChanged();
1451    }
1452
1453    void EditClear()
1454    {
1455       DebugLn("TableEditor::EditClear");
1456       internalModifications = true;
1457       for(fb : fieldsBoxes)
1458          fb.Clear();
1459       for(e : dynamicLookupTableEditors)
1460          e.Destroy(0);
1461       for(e : tableEditors)
1462          e.Destroy(0);
1463       tableEditors.Free();
1464       dynamicLookupTableEditors.Free();
1465       //dynamicLookupTableEditors.size = 0;
1466       internalModifications = false;
1467
1468       DebugLn("   TODO: remove all sub table editors");
1469
1470       modifiedDocument = false;
1471       OnStateChanged();
1472    }
1473
1474    void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection)
1475    {
1476 //      DebugLn("TableEditor::SetListRowFields");
1477       for(lf : listFields)
1478       {
1479          if(lf.dataField && lf.field)
1480          {
1481             if(eClass_IsDerived(lf.field.type, class(char*)))
1482             {
1483                String s = null;
1484                dbRow.GetData(lf.field, s);
1485                listRow.SetData(lf.dataField, s);
1486                delete s;
1487             }
1488             else if(eClass_IsDerived(lf.field.type, class(Id)))
1489             {
1490                if(lf.CustomLookup)
1491                {
1492                   Id id = 0;
1493                   String s = null;
1494                   dbRow.GetData(lf.field, id);
1495                   s = lf.CustomLookup(id);
1496                   listRow.SetData(lf.dataField, s);
1497                   delete s; // ?
1498                }
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*)))
1502                {
1503                   Id id = 0;
1504                   String s = null;
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);
1510                   delete s;
1511                   delete lookupRow;
1512                }
1513             }
1514             else if(lf.CustomLookup && lf.field.type)
1515             {
1516                char * n = lf.field.name;
1517                int64 data = 0;
1518                String s = null;
1519                Class type = lf.field.type;
1520                if(type.type == unitClass && !type.typeSize)
1521                {
1522                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1523                   if(dataType)
1524                      type = dataType;
1525                }
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)
1534                   delete data;
1535                delete s; // ?
1536             }
1537             else if(lf.field.type && eClass_IsDerived(lf.dataField.dataType, class(char*)))
1538             {
1539                char * n = lf.field.name;
1540                char tempString[MAX_F_STRING];
1541                int64 data = 0;
1542                Class type = lf.field.type;
1543                if(type.type == unitClass && !type.typeSize)
1544                {
1545                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1546                   if(dataType)
1547                      type = dataType;
1548                }
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);
1554                else
1555                   lf.field.type._vTbl[__ecereVMethodID_class_OnGetString](lf.field.type, (void*)data, tempString, null, null);
1556
1557                listRow.SetData(lf.dataField, tempString);
1558
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)
1562                   delete data;
1563             }
1564             else if(lf.field.type)
1565             {
1566                char * n = lf.field.name;
1567                //char tempString[256];
1568                int64 data = 0;
1569                Class type = lf.field.type;
1570                if(type.type == unitClass && !type.typeSize)
1571                {
1572                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1573                   if(dataType)
1574                      type = dataType;
1575                }
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);
1581                else
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)
1586                   delete data;
1587             }
1588          }
1589       }
1590       if(restoreSelection && !list.currentRow)
1591       {
1592          DataRow select;
1593          if((select = list.FindRow(selectedId)))
1594             SelectListRow(select);
1595       }
1596    }
1597
1598    Array<Id> SearchWordList()
1599    {
1600       DebugLn("TableEditor::SearchWordList");
1601 #ifdef FULL_STRING_SEARCH
1602    {
1603       int c;
1604       int numTokens = 0;
1605       int len[256];
1606       char * words[256];
1607       WordEntry entries[256];
1608       Array<Id> results = null;
1609       if(searchTables && searchTables.count && searchString && searchString[0])
1610       {
1611          char * searchCopy = CopyString(searchString);
1612          numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false);
1613          for(c = 0; c<numTokens; c++)
1614          {
1615             len[c] = strlen(words[c]);
1616             strlwr(words[c]);
1617             entries[c] = (WordEntry)wordTree.FindString(words[c]);
1618          }
1619          delete searchCopy;
1620       }
1621       if(numTokens)
1622       {
1623          if(numTokens > 1)
1624          {
1625             // AND
1626             int i;
1627             Map<Id, int> matches { };
1628             Map<Id, int> uniques { };
1629             MapNode<Id, int> mn;
1630             results = { };
1631             for(c = 0; c<numTokens; c++)
1632             {
1633                if(entries[c] && entries[c].items && entries[c].items.count)
1634                {
1635                   for(i = 0; i<entries[c].items.count; i++)
1636                   {
1637                      int count = uniques[entries[c].items.ids[i]];
1638 #ifdef _DEBUG
1639                      if(count != 0)
1640                         DebugLn("Problem");
1641 #endif
1642                      matches[entries[c].items.ids[i]]++;
1643                   }
1644                   uniques.Free();
1645                }
1646             }
1647             for(mn = matches.root.minimum; mn; mn = mn.next)
1648             {
1649                if(mn.value > 1)
1650                   results.Add(mn.key);
1651             }
1652             matches.Free();
1653             delete matches;
1654             delete uniques;
1655          }
1656          else if(numTokens == 1)
1657          {
1658             results = { };
1659             if(entries[0] && entries[0].items && entries[0].items.count)
1660             {
1661                for(c = 0; c<entries[0].items.count; c++)
1662                   results.Add(entries[0].items.ids[c]);
1663             }
1664          }
1665       }
1666       return results;
1667    }
1668 #else
1669       return null;
1670 #endif
1671
1672    }
1673
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()
1679    {
1680       DebugLn("TableEditor::PrepareWordList");
1681 #ifdef FULL_STRING_SEARCH
1682    {
1683       char filePath[MAX_FILENAME];
1684       File f;
1685
1686       sprintf(filePath, "%s.search", table.name);
1687       f = filePath ? FileOpenBuffered(filePath, read) : null;
1688       if(f)
1689       {
1690          int a;
1691          f.Get(wordTree);
1692          delete f;
1693
1694          for(a = 0; a<26; a++)
1695          {
1696             int b;
1697             char word[3];
1698             word[0] = 'a' + (char)a;
1699             word[1] = 0;
1700             word[2] = 0;
1701             letters[a] = (WordEntry)wordTree.FindString(word);
1702             for(b = 0; b<26; b++)
1703             {
1704                word[1] = 'a' + (char)b;
1705                doubleLetters[a][b] = (WordEntry)wordTree.FindString(word);
1706             }
1707          }
1708       }
1709       else if(searchTables && searchTables.count)
1710       {
1711          if(!letters[0])
1712          {
1713             int a;
1714             for(a = 0; a<26; a++)
1715             {
1716                int b;
1717                char word[3];
1718                word[0] = 'a' + (char)a;
1719                word[1] = 0;
1720                word[2] = 0;
1721                wordTree.Add((BTNode)(letters[a] = WordEntry { string = CopyString(word) }));
1722                for(b = 0; b<26; b++)
1723                {
1724                   word[1] = 'a' + (char)b;
1725                   wordTree.Add((BTNode)(doubleLetters[a][b] = WordEntry { string = CopyString(word) }));
1726                }
1727             }
1728          }
1729
1730          wordListPrepTimer.tablesIndex = 0;
1731          wordListPrepTimer.Start();
1732       }
1733    }
1734 #endif
1735    }
1736
1737    void ProcessWordListString(char * string, StringSearchIndexingMethod method, Id id)
1738    {
1739       int c;
1740       unichar ch;
1741       unichar lastCh = 0;
1742       int count = 0;
1743       int numChars = 0;
1744       int nb;
1745       char word[1024];
1746       char asciiWord[1024];
1747
1748       for(c = 0; ; c += nb)
1749       {
1750          ch = UTF8GetChar(string + c, &nb);
1751
1752          if(!ch || CharMatchCategories(ch, separators) ||
1753             (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
1754          {
1755             if(count)
1756             {
1757                word[count] = 0;
1758                asciiWord[numChars] = 0;
1759                strlwr(word);
1760                strlwr(asciiWord);
1761
1762                AddWord(word, count, method == allSubstrings, id);
1763                if(count > numChars)
1764                   AddWord(asciiWord, strlen(asciiWord), method == allSubstrings, id);
1765                count = 0;
1766                numChars = 0;
1767             }
1768             if(!CharMatchCategories(ch, separators))
1769             {
1770                int cc;
1771                for(cc = 0; cc < nb; cc++)
1772                   word[count++] = string[c + cc];
1773
1774                asciiWord[numChars++] = ToASCII(ch);
1775             }
1776             if(!ch)
1777                break;
1778          }
1779          else
1780          {
1781             int cc;
1782             for(cc = 0; cc < nb; cc++)
1783                word[count++] = string[c + cc];
1784
1785             asciiWord[numChars++] = ToASCII(ch);
1786          }
1787          lastCh = ch;
1788       }
1789    }
1790
1791    /*static */WordEntryBinaryTree wordTree
1792    {
1793       CompareKey = (void *)BinaryTree::CompareString;
1794       FreeKey = BinaryTree::FreeString;
1795    };
1796
1797    WordEntry letters[26];
1798    WordEntry doubleLetters[26][26];
1799
1800    void AddWord(char * word, int count, bool addAllSubstrings, Id id)
1801    {
1802       //DebugLn("TableEditor::AddWord");
1803 #ifdef FULL_STRING_SEARCH
1804    {
1805       if(addAllSubstrings)
1806       {
1807          int s;
1808          WordEntry mainEntry = null;
1809          WordEntry sEntry = null;
1810
1811          for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
1812          {
1813             int l;
1814             char subWord[1024];
1815             char ch1;
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!!
1819             ch1 = subWord[0];
1820
1821             for(l = count-s; l>0; l--)
1822             {
1823                uint wid;
1824                WordEntry start = null, wordEntry;
1825
1826                while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
1827                if(!l) break;
1828
1829                subWord[l] = 0;
1830
1831                if(ch1 >= 'a' && ch1 <= 'z')
1832                {
1833                   char ch2 = subWord[1];
1834                   if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
1835                   {
1836                      char ch2 = subWord[1];
1837                      start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
1838                   }
1839                   else
1840                   {
1841                      start = letters[ch1 - 'a'];
1842                   }
1843                }
1844
1845                if(start)
1846                {
1847                   WordEntry max;
1848                   while(start && (max = (WordEntry)((BTNode)start).maximum))
1849                   {
1850                      if(strcmp(max.string, subWord) >= 0)
1851                         break;
1852                      start = start.parent;
1853                   }
1854                }
1855
1856                if(!start)
1857                   start = (WordEntry)wordTree.root;
1858
1859                if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
1860                {
1861
1862                }
1863                else
1864                {
1865                   wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
1866                }
1867                if(!mainEntry)
1868                {
1869                   mainEntry = wordEntry;
1870                   sEntry = wordEntry;
1871                   lEntry = wordEntry;
1872                }
1873                else if(!sEntry)
1874                {
1875                   sEntry = wordEntry;
1876                   lEntry = wordEntry;
1877                   if(!wordEntry.words) wordEntry.words = IdList { };
1878                   wordEntry.words.Add((Id)mainEntry);
1879                }
1880                else if(!lEntry)
1881                {
1882                   lEntry = wordEntry;
1883                   if(!wordEntry.words) wordEntry.words = IdList { };
1884                   wordEntry.words.Add((Id)sEntry);
1885                }
1886                else
1887                {
1888                   if(!wordEntry.words) wordEntry.words = IdList { };
1889                   wordEntry.words.Add((Id)lEntry);
1890                }
1891                if(!wordEntry.items) wordEntry.items = IdList { };
1892                wordEntry.items.Add(id);
1893             }
1894          }
1895       }
1896       else
1897       {
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);
1903       }
1904    }
1905 #endif
1906    }
1907 }
1908
1909 public class ListField : struct
1910 {
1911 public:
1912    Field field;
1913    DataField dataField;
1914    Field lookupFindField;
1915    Field lookupValueField;
1916    Table lookupFindIndex;
1917    String (*CustomLookup)(Id);
1918 private:
1919
1920    ~ListField()
1921    {
1922       delete dataField;
1923    }
1924 }
1925
1926 public class Lookup
1927 {
1928 public:
1929    Field valueField;
1930    Field findField;
1931    Table findIndex;
1932
1933 private:
1934    Row row;
1935
1936    ~Lookup()
1937    {
1938       delete row;
1939    }
1940 }
1941
1942 public class LookupEditor : struct
1943 {
1944 public:
1945    subclass(TableEditor) editorClass;
1946    Window parentWindow;
1947    Field lookupValueField;
1948    Field lookupFindField;
1949    Field lookupIdField;
1950    Table lookupFindIndex;
1951 }
1952
1953 // all methods currently perform ascii conversion and all that jazz on every string added to the index
1954 public enum StringSearchIndexingMethod { fullString, allSubstrings };
1955
1956 public class StringSearchField
1957 {
1958 public:
1959    Field field;
1960    StringSearchIndexingMethod method;
1961
1962    Field lookupFindField;
1963    Field lookupValueField;
1964
1965    //String (*CustomRead)(Id);
1966 };
1967
1968 public class StringSearchTable
1969 {
1970 public:
1971    Table table;
1972    Field idField;
1973    Array<StringSearchField> searchFields;
1974
1975 private:
1976    ~StringSearchTable()
1977    {
1978       delete searchFields;
1979    }
1980 }
1981
1982 public class SQLiteSearchField
1983 {
1984 public:
1985    Field field;
1986    //StringSearchIndexingMethod method;
1987 };
1988
1989 public class SQLiteSearchTable
1990 {
1991 public:
1992    Table table;
1993    Field idField;
1994    Array<SQLiteSearchField> searchFields;
1995
1996 private:
1997    ~SQLiteSearchTable()
1998    {
1999       delete searchFields;
2000    }
2001 }
2002
2003 static WordEntry * btnodes;
2004
2005 struct WordEntryBinaryTree : BinaryTree
2006 {
2007    WordEntry * entries;
2008    
2009    void OnSerialize(IOChannel channel)
2010    {
2011       WordEntry node;
2012       uint id;
2013       uint count = this.count;
2014       DebugLn("WordEntryBinaryTree::OnSerialize");
2015       for(id = 1, node = (WordEntry)root; node;)
2016       {
2017          node.id = id++;
2018          if(node.left)
2019             node = node.left;
2020          else if(node.right)
2021             node = node.right;
2022          else if(node.parent)
2023          {
2024             bool isLeft = node == node.parent.left;
2025             node = node.parent;
2026             
2027             while(node)
2028             {
2029                if(isLeft && node.right)
2030                {
2031                   node = node.right;
2032                   break;
2033                }
2034                if(node.parent)
2035                   isLeft = node == node.parent.left;
2036                node = node.parent;
2037             }
2038          }
2039          else
2040             node = null;
2041       }
2042
2043       id--;
2044       channel.Serialize(id);
2045       channel.Serialize((WordEntry)root);
2046    }
2047
2048    void OnUnserialize(IOChannel channel)
2049    {
2050       WordEntry root, node;
2051       uint count;
2052       DebugLn("WordEntryBinaryTree::OnUnserialize");
2053       channel.Unserialize(count);
2054       entries = new WordEntry[count];      
2055       btnodes = entries;
2056       channel.Unserialize(root);
2057       this.root = (BTNode)root;
2058       // count = root ? this.root.count : 0;      
2059       this.count = count;
2060       for(node = (WordEntry)root; node;)
2061       {
2062          if(node.words)
2063          {
2064             int c;
2065             for(c = 0; c<node.words.count; c++)
2066                node.words.ids[c] = (Id)btnodes[node.words.ids[c] - 1];
2067          }
2068          if(node.left)
2069             node = node.left;
2070          else if(node.right)
2071             node = node.right;
2072          else if(node.parent)
2073          {
2074             bool isLeft = node == node.parent.left;
2075             node = node.parent;
2076             
2077             while(node)
2078             {
2079                if(isLeft && node.right)
2080                {
2081                   node = node.right;
2082                   break;
2083                }
2084                if(node.parent)
2085                   isLeft = node == node.parent.left;
2086                node = node.parent;
2087             }
2088          }
2089          else
2090             node = null;
2091       }
2092       delete entries;
2093       btnodes = null;
2094    }
2095 };
2096
2097 class WordEntry : struct
2098 {
2099    String string;
2100    WordEntry parent;
2101    WordEntry left, right;
2102    int depth;
2103    
2104    IdList items;
2105    IdList words;
2106    uint id;
2107
2108    ~WordEntry()
2109    {
2110       delete items;
2111       delete words;
2112    }
2113
2114    void OnSerialize(IOChannel channel)
2115    {
2116 #ifdef FULL_STRING_SEARCH
2117       if(this)
2118       {
2119          channel.Serialize(id);
2120          channel.Serialize(string);
2121          channel.Serialize(items);
2122
2123          if(words)
2124          {
2125             int c;
2126             channel.Serialize(words.count);
2127             for(c = 0; c < words.count; c++)
2128             {
2129                uint id = ((WordEntry)words.ids[c]).id;
2130                channel.Serialize(id);
2131             }
2132          }
2133          else
2134          {
2135             Id none = MAXDWORD;
2136             channel.Serialize(none);
2137          }
2138
2139          // channel.Serialize(words);
2140          channel.Serialize(left);
2141          channel.Serialize(right);
2142       }
2143       else
2144       {
2145          uint nothing = 0;
2146          channel.Serialize(nothing);
2147       }
2148 #endif
2149    }
2150
2151    void OnUnserialize(IOChannel channel)
2152    {
2153 #ifdef FULL_STRING_SEARCH
2154       uint id;
2155       channel.Unserialize(id);
2156       if(id)
2157       {
2158          uint count;
2159          WordEntry entry;
2160          // TODO: Fix typed_object issues
2161          entry = btnodes[id - 1] = eInstance_New(class(WordEntry));
2162          this = (void *)entry;
2163          
2164          channel.Unserialize(string);
2165          channel.Unserialize(items);
2166          channel.Unserialize(words);
2167
2168          channel.Unserialize(left);
2169          if(left) { left.parent = (void *)this; }
2170          channel.Unserialize(right);
2171          if(right) { right.parent = (void *)this; }
2172
2173          // TODO: Precomp errors without extra brackets
2174          depth = ((BTNode)((void *)this)).depthProp;
2175       }
2176       else
2177          this = null;
2178 #endif
2179    }
2180 }
2181
2182
2183 class ListEnumerationTimer : Timer
2184 {
2185    bool hasCompleted;
2186    int matchesIndex;
2187    bool sqliteSearch;
2188    int tablesIndex;
2189    Array<Id> matches;
2190    Row row;
2191    Map<Id, int> uniques;
2192 }
2193
2194 class WordListPrepTimer : Timer
2195 {
2196    bool hasCompleted;
2197    int tablesIndex;
2198    Row row;
2199 }
2200
2201 #if 0
2202 class EnumerateThread : Thread
2203 {
2204 public:
2205    bool active;
2206    TableEditor editor;
2207    //Table table;
2208    //Row r;
2209    Array<Id> matches;
2210
2211    void Abort()
2212    {
2213       /*if(abort)
2214          abortNow = true;
2215       else*/
2216       if(active)
2217          abort = true;
2218    }
2219
2220 private:
2221    bool abort, abortNow;
2222
2223    unsigned int Main()
2224    {
2225       app.Wait();
2226       app.Lock();
2227
2228       //if(app.ProcessInput(true))
2229          //app.Wait();
2230       {
2231          Row r { editor.table };
2232          if(matches)
2233          {
2234             int c;
2235             if(editor.listFields && editor.idField)
2236             {
2237                /*for(c=0; c<matches.count && !abort; c++)
2238                {
2239                   if(r.Find(editor.idField, middle, nil, matches[c]))
2240                   {
2241                      Id id = 0;
2242                      DataRow row;
2243                      GuiLock();
2244                      row = editor.list.AddRow();
2245                      r.GetData(editor.idField, id);
2246                      row.tag = id;
2247                      editor.SetListRowFields(r, row, true);
2248                      GuiUnlock();
2249                   }
2250                   else
2251                      DebugLn($"WordList match cannot be found in database.");
2252                }*/
2253             }
2254             else if(editor.idField && editor.stringField)
2255             {
2256                /*for(c=0; c<matches.count && !abort; c++)
2257                {
2258                   if(r.Find(editor.idField, middle, nil, matches[c]))
2259                   {
2260                      Id id = 0;
2261                      String s = null;
2262                      r.GetData(editor.idField, id);
2263                      r.GetData(editor.stringField, s);
2264                      GuiLock();
2265                      editor.list.AddString(s).tag = id;
2266                      GuiUnlock();
2267                      delete s;
2268                   }
2269                   else
2270                      DebugLn($"WordList match cannot be found in database.");
2271                }*/
2272             }
2273             else
2274                ;//app.Unlock();
2275          }
2276          else if(!editor.disabledFullListing)
2277          {
2278             if(editor.listFields && editor.idField)
2279             {
2280                app.Unlock();
2281                while(r.Next() && !abort)
2282                {
2283                   Id id = 0;
2284                   DataRow row;
2285                app.Unlock();
2286                   r.GetData(editor.idField, id);
2287                   //if(app.ProcessInput(true))
2288                      //app.Wait();
2289                   //app.Wait();
2290                   app.Lock();
2291                      row = editor.list.AddRow();
2292                      row.tag = id;
2293                      editor.SetListRowFields(r, row, true);
2294                   //app.Unlock();
2295                }
2296                //app.Unlock();
2297             }
2298             else if(editor.idField && editor.stringField)
2299             {
2300                /*while(r.Next() && !abort)
2301                {
2302                   Id id = 0;
2303                   String s = null;
2304                   GuiLock();
2305                   r.GetData(editor.idField, id);
2306                   r.GetData(editor.stringField, s);
2307                   editor.list.AddString(s).tag = id;
2308                   GuiUnlock();
2309                   delete s;
2310                }*/
2311             }
2312             else
2313                ;//app.Unlock();
2314          }
2315          else
2316             ;//app.Unlock();
2317
2318          //app.Lock();
2319             editor.list.Sort(editor.listSortField, editor.listSortOrder);
2320          //app.Unlock();
2321       }
2322       active = false;
2323       abort = false;
2324
2325       app.Unlock();
2326       return 0;
2327    }
2328
2329    /*void GuiLock()
2330    {
2331       app.Wait();
2332       app.Lock();
2333    }*/
2334
2335    /*void GuiUnlock()
2336    {
2337       app.Unlock();
2338       editor.list.Update(null);
2339       //app.Wait(); // Sleep(0.2f);
2340       //if(app.ProcessInput(true))
2341          //app.Wait();
2342          // Update(null);
2343          //app.UpdateDisplay();
2344       //app.Wait();
2345       // app.Lock();
2346    }*/
2347 }
2348 #endif
2349
2350 static define app = ((GuiApplication)__thisModule);
2351
2352 static inline void DebugLn(typed_object object, ...)
2353 {
2354 #if defined(_DEBUG_LINE)
2355    va_list args;
2356    char buffer[4096];
2357    va_start(args, object);
2358    PrintStdArgsToBuffer(buffer, sizeof(buffer), object, args);
2359    va_end(args);
2360    puts(buffer);
2361 #endif
2362 }