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