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