eda, compiler, documentor: Proper prototypes for invoking virtual methods directly
[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    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(table && editRow && editRow.tbl && !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             selectedId = id;
545
546 #ifdef _DEBUG
547             newText = PrintString("[", newEntryStringDebug, id, "]");
548 #else
549             newText = PrintString("[", newEntryString, "]");
550 #endif
551
552             //if(NotifyNew(master, this, r))
553             if(listFields && idField)
554             {
555                for(lf : listFields)
556                {
557                   if(lf.dataField && lf.field)
558                   {
559                      if(lf.field.type == class(String))
560                         r.SetData(lf.field, newText);
561                      else // this whole block is new?
562                      {
563                         if(lf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])
564                         {
565                            Class dataType = lf.field.type;
566                            int64 dataHolder = 0;
567                            void * data = &dataHolder;
568
569                            if(dataType && dataType.type == structClass)
570                            {
571                               dataHolder = (int64)new0 byte[dataType.structSize];
572                               data = (void *)dataHolder;
573                            }
574                            /*else if(dataType && (dataType.type == noHeadClass || dataType.type == normalClass))
575                            {
576                               if(eClass_IsDerived(dataType, class(char*)))
577                                  dataHolder = (int64)CopyString("");
578                               else
579                                  dataHolder = (int64)eInstance_New(dataType);
580                               data = (void *)&dataHolder;
581                            }
582                            else
583                            {
584                               dataHolder = 0;
585                               data = &dataHolder;
586                            }*/
587                            if(data)
588                               ((bool (*)(void *, void *, const char *))(void *)dataType._vTbl[__ecereVMethodID_class_OnGetDataFromString])(dataType, data, newText);
589
590
591                            /*((void (*)(void *, void *))(void *)dataType._vTbl[__ecereVMethodID_class_OnFree])(dataType, dataHolder);
592                            if(dataType.type == structClass)
593                            {
594                               void * dataPtr = (void *)dataHolder;
595                               delete dataPtr;
596                            }
597                            dataHolder = 0;*/
598                         }
599                      }
600                   }
601                }
602                if(list)
603                {
604                   row = list.AddRow();
605                   row.tag = id;
606                   // have a better technique than Row::Next(); Row::Find(...); to make sure Row::GetData() will work right after a Row::SetData()?
607                   // it seems we're missing Row::Update()
608                   //r.Next();
609                   //r.tbl.db.Commit();
610                   //editRow.Synch(r);
611                   //r.Last();
612                   // next line is a patch for SQLite not returning data from GetData right after a SetData
613                   if(idField && r.Find(idField, middle, nil, id))
614                      SetListRowFields(r, row, false);
615                }
616             }
617             else if(idField && stringField)
618             {
619                r.SetData(stringField, newText);
620                if(list)
621                {
622                   row = list.AddString(newText);
623                   row.tag = id;
624                }
625             }
626             //delete r;
627             delete newText;
628          }
629
630          if(list)
631          {
632             list.Sort(listSortField, listSortOrder);
633             if(row) SelectListRow(row);
634          }
635          OnStateChanged();
636       }
637    }
638
639    void Remove()
640    {
641       DebugLn("TableEditor::Remove");
642       if(editRow.sysID) //list && list.currentRow)
643       {
644          if(OnRemovalRequest())
645          {
646             editRow.Delete();
647             if(list)
648                list.DeleteRow(list.currentRow);
649             EditClear();
650             //NotifyDeleted(master, this);
651             if(list)
652                SelectListRow(list.currentRow);
653             OnStateChanged();
654          }
655       }
656    }
657
658    void Load()
659    {
660       DebugLn("TableEditor::Load");
661       EditLoad();
662    }
663
664    void Write()
665    {
666       DebugLn("TableEditor::Write");
667       EditSave();
668    }
669
670    bool ListSelect(DataRow row)
671    {
672       bool result = true;
673       DebugLn("TableEditor::ListSelect");
674       if(/*-row && -*/row != lastRow)
675       {
676          uint id;
677          if(modifiedDocument)
678          {
679             if(row)
680                list.currentRow = lastRow;
681             result = false;
682             switch(OnLeavingModifiedDocument())
683             {
684                case cancel:
685                   break;
686                case yes:
687                   EditSave();
688                case no:
689                   EditClear();
690                   list.currentRow = row;
691                   break;
692             }
693          }
694          if(list.currentRow == row)
695             SelectListRow(row);
696       }
697       return result;
698    }
699
700    bool Select(Id id)
701    {
702       bool result;
703       DebugLn("TableEditor::Select");
704       // 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)
705       if(idField && editRow && (editRow.sysID = id, !editRow.nil))// && editRow.Find(idField, middle, nil, id))
706       {
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(bool loopAround)
740    {
741       bool result = NotifyClosing();
742       bool wasNil = !editRow.sysID;
743       DebugLn("TableEditor::SelectNext");
744       if(result)
745       {
746          if(filtered)
747          {
748             if(!filterRow.Next() && loopAround)
749             {
750                //filterRow.First(); // Row::First doesn't behave properly in a filtered table
751                while(filterRow.Previous())
752                   ;
753                filterRow.Next();
754             }
755             if(!filterRow.nil)
756             {
757                if(wasNil && filterRow.sysID == selectedId)
758                   filterRow.Next();       // this whole wasNil thing makes no sense to me?
759                editRow.sysID = filterRow.sysID;
760             }
761             else
762                editRow.sysID = 0;
763          }
764          else
765          {
766             if(!editRow.Next() && loopAround)
767                editRow.Next();
768          }
769          if(!editRow.nil)
770          {
771             selectedId = editRow.sysID;
772             EditLoad();
773          }
774          else
775             result = false;
776       }
777       return result;
778    }
779    
780    bool SelectPrevious(bool loopAround)
781    {
782       bool result = NotifyClosing();
783       bool wasNil = !editRow.sysID;
784       DebugLn("TableEditor::SelectPrevious");
785       if(result)
786       {
787          if(filtered)
788          {
789             if(!filterRow.Previous() && loopAround)
790             {
791                //filterRow.Last(); // Row::Last doesn't behave properly in a filtered table
792                while(filterRow.Next())
793                   ;
794                filterRow.Previous();
795             }
796             if(!filterRow.nil)
797             {
798                if(wasNil && filterRow.sysID == selectedId)
799                   filterRow.Previous();       // this whole wasNil thing makes no sense to me?
800                editRow.sysID = filterRow.sysID;
801             }
802             else
803                editRow.sysID = 0;
804          }
805          else
806          {
807             if(!editRow.Previous() && loopAround)
808                editRow.Previous();
809          }
810          if(!editRow.nil)
811          {
812             selectedId = editRow.sysID;
813             EditLoad();
814          }
815          else
816             result = false;
817       }
818       return result;
819    }
820    
821    void SelectListRow(DataRow row)
822    {
823       // Time startTime = GetTime();
824       DebugLn("TableEditor::SelectListRow");
825       if(row)
826       {
827          // 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)
828          selectedId = (Id)row.tag;
829          lastRow = row;
830
831          if(list.currentRow != row)
832             list.currentRow = row;
833          if(idField && editRow.Find(idField, middle, nil, selectedId))
834          {
835             listRow = row;
836             //NotifySelectListRow(master, this, selectedId);
837             EditLoad();
838          }
839       }
840       // Logf("SelectListRow took %f seconds\n", GetTime() - startTime);
841    }
842
843 private:
844    Row editRow { };
845    DataRow listRow;
846    DataRow lastRow;
847    Id selectedId;
848    Row filterRow { };
849    bool filtered;
850    Array<char> searchCI { };
851
852    ListEnumerationTimer listEnumerationTimer
853    {
854       userData = this, delay = 0.1f;
855       bool DelayExpired()
856       {
857          static Time startTime;
858          bool next = false;
859          int c;
860          Row row = listEnumerationTimer.row;
861          Array<Id> matches = listEnumerationTimer.matches;
862          Time delay = listEnumerationTimer.delay;
863          Time lastTime = GetTime();
864          static int slice = 128;
865
866          static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
867          ticks++;
868          if(ticks % 10 == 0)
869             PrintLn("listing... ");
870
871          if(matches)
872          {
873             int index = listEnumerationTimer.matchesIndex;
874             if(listFields && idField)
875             {
876                for(c=0; c<slice && (next = index++<matches.count); c++)
877                {
878                   if(row.Find(idField, middle, nil, matches[index]))
879                   {
880                      Id id = 0;
881                      DataRow dataRow = list.AddRow();
882                      row.GetData(idField, id);
883                      dataRow.tag = id;
884                      SetListRowFields(row, dataRow, true);
885                   }
886                   else
887                      DebugLn($"WordList match cannot be found in database.");
888                }
889                listEnumerationTimer.matchesIndex = index;
890                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
891             }
892             else if(idField && stringField)
893             {
894                for(c=0; c<slice && (next = index++<matches.count); c++)
895                {
896                   if(row.Find(idField, middle, nil, matches[index]))
897                   {
898                      Id id = 0;
899                      String s = null;
900                      row.GetData(idField, id);
901                      row.GetData(stringField, s);
902                      list.AddString(s).tag = id;
903                      delete s;
904                   }
905                   else
906                      DebugLn($"WordList match cannot be found in database.");
907                }
908                listEnumerationTimer.matchesIndex = index;
909                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
910             }
911          }
912          else if(listEnumerationTimer.sqliteSearch)
913          {
914             static SQLiteSearchTable st = null;
915             Row lookupRow { table };
916             Map<Id, int> uniques = listEnumerationTimer.uniques;
917             if(!row)
918             {
919                if(listEnumerationTimer.tablesIndex < sqliteSearchTables.count)
920                {
921                   char queryString[4096*4];
922
923                   if(!listEnumerationTimer.uniques)
924                      listEnumerationTimer.uniques = uniques = { };
925
926                   st = sqliteSearchTables[listEnumerationTimer.tablesIndex];
927                   if(st.table && st.idField && st.searchFields && st.searchFields.count)
928                   {
929                      wordListPrepRowNum = 0;
930                      wordListPrepRowCount = st.table.rowsCount;
931
932                      if(st.table && st.idField && st.searchFields && st.searchFields.count &&
933                            st.searchFields[0].field)
934                      {
935                         bool first = true;
936                         int bindId = 0;
937
938                         listEnumerationTimer.row = row = { st.table };
939
940                         {
941                            int len = searchString ? strlen(searchString) : 0;
942                            searchCI.size = len + 1;
943                            ConvertToASCII(searchString ? searchString : "", searchCI.array, &len, true);
944                            searchCI.count = len + 1;
945                         }
946
947                         sprintf(queryString, "SELECT ROWID, * FROM `%s`", st.table.name);
948                         strcat(queryString, " WHERE ");
949                         for(sf : st.searchFields)
950                         {
951                            if(sf.field)
952                            {
953                               if(!first)
954                                  strcat(queryString, " OR ");
955 #define PERSON_SEARCH_LIKE
956                               // This code tries to implement the logic of PersonName::OnGetDataFromString because PersonName is inaccessible from here
957                               if(strstr(sf.field.type.name, "PersonName"))
958                               {
959 #ifdef PERSON_SEARCH_LIKE
960                                  String ln = null, fn = null, mn = null;
961                                  char * comma = strchr(searchCI.array, ',');
962                                  if(comma)
963                                  {
964                                     int len = comma - searchCI.array;
965                                     ln = new char[len + 1];
966                                     memcpy(ln, searchCI.array, len);
967                                     ln[len] = 0;
968
969                                     fn = CopyString(comma+1);
970                                     {
971                                        int c;
972                                        for(c = strlen(fn)-2; c > 0; c--)
973                                        {
974                                           if(fn[c] == ' ')
975                                           {
976                                              mn = CopyString(fn + c + 1);
977                                              fn[c] = 0;
978                                           }
979                                        }
980                                     }
981                                  }
982                                  else
983                                     ln = CopyString(searchCI.array);
984                                  if(ln)
985                                  {
986                                     TrimLSpaces(ln, ln);
987                                     TrimRSpaces(ln, ln);
988                                  }
989                                  if(fn)
990                                  {
991                                     TrimLSpaces(fn, fn);
992                                     TrimRSpaces(fn, fn);
993                                  }
994                                  if(mn)
995                                  {
996                                     TrimLSpaces(mn, mn);
997                                     TrimRSpaces(mn, mn);
998                                  }
999
1000                                  /* We could simply do this if we had access to PersonName: (don't forget the delete pn; below)
1001                                  PersonName pn;
1002                                  pn.OnGetDataFromString(searchCI.array);
1003                                  */
1004                                  if(ln && !fn && !mn)
1005                                  {
1006                                     // Only last name is pecified in search object, it is looked for in all fields
1007                                     strcatf(queryString, "(PNLastName(`%s`) LIKE '%%%s%%' OR PNFirstName(`%s`) LIKE '%%%s%%' OR PNMiddleName(`%s`) LIKE '%%%s%%')",
1008                                        sf.field.name, searchCI.array, sf.field.name, searchCI.array, sf.field.name, ln);
1009                                  }
1010                                  else if(ln || fn || mn)
1011                                  {
1012                                     // Otherwise search is in respective fields only (all specified must match)
1013                                     bool first = true;
1014                                     strcatf(queryString, "(");
1015                                     if(ln)
1016                                     {
1017                                        if(!first) strcatf(queryString, " AND ");
1018                                        first = false;
1019                                        strcatf(queryString, "PNLastName(`%s`) LIKE '%%%s%%'", sf.field.name, ln);
1020                                     }
1021                                     if(fn)
1022                                     {
1023                                        if(!first) strcatf(queryString, " AND ");
1024                                        first = false;
1025                                        strcatf(queryString, "PNFirstName(`%s`) LIKE '%%%s%%'", sf.field.name, fn);
1026                                     }
1027                                     if(mn)
1028                                     {
1029                                        if(!first) strcatf(queryString, " AND ");
1030                                        first = false;
1031                                        strcatf(queryString, "PNMiddleName(`%s`) LIKE '%%%s%%'", sf.field.name, mn);
1032                                     }
1033                                     strcatf(queryString, ")");
1034                                  }
1035                                  //delete pn;
1036                                  delete ln; delete fn; delete mn;
1037 #else
1038                                  // To use CompPersonName::OnCompare:
1039                                  strcatf(queryString, "`%s` = ?", sf.field.name);
1040 #endif
1041                               }
1042                               else
1043                                  strcatf(queryString, "`%s` LIKE '%%%s%%'", sf.field.name, searchString);
1044                               first = false;
1045                            }
1046                         }
1047                         PrintLn(queryString);
1048                         startTime = GetTime();
1049                         row.query = queryString;
1050 #ifndef PERSON_SEARCH_LIKE
1051                         // To use CompPersonName::OnCompare:
1052                         for(sf : st.searchFields)
1053                         {
1054                            if(sf.field)
1055                            {
1056                               if(strstr(sf.field.type.name, "PersonName"))
1057                               {
1058                                  void * pn;
1059                                  ((bool (*)(void *, void *, const char *))(void *)sf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])(sf.field.type, &pn, searchCI.array);
1060                                  row.SetQueryParamObject(++bindId, pn, sf.field.type);
1061                                  bindId++;
1062                                  delete pn;
1063                               }
1064                               first = false;
1065                            }
1066                         }
1067 #endif
1068                         if(bindId)
1069                            row.Next();
1070                      }
1071                   }
1072                }
1073             }
1074             if(row)
1075             {
1076                if(listFields && idField)
1077                {
1078                   // should we not start with a Next() ??? :S  -- right now, query = does not need a Next(), unless it had parameters (SetQueryParam), See #591
1079                   // when going through all the rows in a table you always start with Next() no?
1080                   // is this different for query results?
1081                   for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
1082                   {
1083                      Id id = 0;
1084                      row.GetData(st.idField, id);
1085                      //if(uniques[id]++ == 0)
1086                      if(uniques[id] == 0)
1087                      {
1088                         DataRow dataRow = list.AddRow();
1089                         dataRow.tag = id;
1090                         if(st.table == table)
1091                            SetListRowFields(row, dataRow, true);
1092                         else if(lookupRow.Find(idField, middle, nil, id))
1093                            SetListRowFields(lookupRow, dataRow, true);
1094                         else
1095                            PrintLn("no");
1096                      }
1097                      uniques[id] = uniques[id] + 1;
1098                   }
1099                   if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1100                   else
1101                   {
1102                      delete listEnumerationTimer.row; row = null;
1103                      next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1104                   }
1105                }
1106                else if(idField && stringField)
1107                {
1108                   // should we not start with a Next() ??? :S
1109                   // when going through all the rows in a table you always start with Next() no?
1110                   // is this different for query results?
1111                   for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
1112                   {
1113                      Id id = 0;
1114                      row.GetData(st.idField, id);
1115                      //if(uniques[id]++ == 0)
1116                      if(uniques[id] == 0)
1117                      {
1118                         String s = null;
1119                         if(st.table == table)
1120                            row.GetData(stringField, s);
1121                         else if(lookupRow.Find(idField, middle, nil, id))
1122                            lookupRow.GetData(stringField, s);
1123                         else
1124                            PrintLn("no");
1125                         list.AddString(s).tag = id;
1126                         delete s;
1127                      }
1128                      uniques[id] = uniques[id] + 1;
1129                   }
1130                   if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1131                   else
1132                   {
1133                      delete listEnumerationTimer.row; row = null;
1134                      next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
1135                   }
1136                }
1137             }
1138             delete lookupRow;
1139          }
1140          else if(!disabledFullListing)
1141          {
1142             if(listFields && idField)
1143             {
1144                for(c = 0; c<slice && (next = row.Next()); c++)
1145                {
1146                   Id id = 0;
1147                   DataRow dataRow = list.AddRow();
1148                   row.GetData(idField, id);
1149                   dataRow.tag = id;
1150                   SetListRowFields(row, dataRow, true);
1151                   //app.UpdateDisplay();
1152                }
1153                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1154             }
1155             else if(idField && stringField)
1156             {
1157                for(c = 0; c<slice && (next = row.Next()); c++)
1158                {
1159                   Id id = 0;
1160                   String s = null;
1161                   row.GetData(idField, id);
1162                   row.GetData(stringField, s);
1163                   list.AddString(s).tag = id;
1164                   delete s;
1165                }
1166                if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1167             }
1168          }
1169
1170          list.Sort(listSortField, listSortOrder);
1171
1172          if(!next)
1173          {
1174             listEnumerationTimer.hasCompleted = true;
1175             StopListEnumerationTimer();
1176          }
1177          if(startTime) PrintLn("*** Search took ", (uint)((GetTime() - startTime) * 1000), "ms ***");
1178          return true;
1179       }
1180    };
1181
1182    void StopListEnumerationTimer()
1183    {
1184       listEnumerationTimer.Stop();
1185       listEnumerationTimer.matchesIndex = 0;
1186       listEnumerationTimer.tablesIndex = 0;
1187       delete listEnumerationTimer.row;
1188       delete listEnumerationTimer.matches;
1189       delete listEnumerationTimer.uniques;
1190    }
1191
1192    WordListPrepTimer wordListPrepTimer
1193    {
1194       userData = this, delay = 0.1f;
1195       bool DelayExpired()
1196       {
1197          bool next = false;
1198          Row row = wordListPrepTimer.row;
1199          static int slice = 512;
1200          static StringSearchTable st = null;
1201
1202          static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
1203
1204          if(!row)
1205          {
1206             if(wordListPrepTimer.tablesIndex < searchTables.count)
1207             {
1208                st = searchTables[wordListPrepTimer.tablesIndex];
1209                if(st.table && st.idField && st.searchFields && st.searchFields.count)
1210                {
1211                   wordListPrepRowNum = 0;
1212                   wordListPrepRowCount = st.table.rowsCount;
1213
1214                   wordListPrepTimer.row = row = { st.table };
1215                   DebugLn("building word list for ", st.table.name, " table ------------------------------------- ");
1216                }
1217             }
1218          }
1219          if(row)
1220          {
1221             int c;
1222             Time delay = wordListPrepTimer.delay;
1223             Time lastTime = GetTime();
1224
1225             ticks++;
1226             if(ticks % 10 == 0)
1227                PrintLn("indexing ", wordListPrepRowNum, " of ", wordListPrepRowCount, " --- slice is ", slice);
1228
1229             for(c = 0; c<slice && (next = row.Next()); c++)
1230             {
1231                Id id = 0;
1232                row.GetData(st.idField, id);
1233
1234                wordListPrepRowNum++;
1235
1236                for(sf : st.searchFields)
1237                {
1238                   Field field = sf.field;
1239                   StringSearchIndexingMethod method = sf.method;
1240                   if(field && field.type == class(String))
1241                   {
1242                      String string = null;
1243                      row.GetData(field, string);
1244
1245                      if(string && string[0])
1246                         ProcessWordListString(string, method, id);
1247                      delete string;
1248                   }
1249                   // todo: need to improve on this...
1250                   // else ... call OnGetString of type ... etc...
1251                      //PrintLn("todo: support other field types for string search");
1252                   else if(field && field.type)
1253                   {
1254                      char * n = field.name;
1255                      char tempString[MAX_F_STRING];
1256                      int64 data = 0;
1257                      Class type = field.type;
1258                      String s;
1259                      if(type.type == unitClass && !type.typeSize)
1260                      {
1261                         Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1262                         if(dataType)
1263                            type = dataType;
1264                      }
1265                      if(type.type == structClass)
1266                         data = (int64)new0 byte[type.structSize];
1267                      ((bool (*)())(void *)row.GetData)(row, field, type, (type.type == structClass) ? (void *)data : &data);
1268
1269                      if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1270                         s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)field.type._vTbl[__ecereVMethodID_class_OnGetString])(field.type, &data, tempString, null, null);
1271                      else
1272                         s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)field.type._vTbl[__ecereVMethodID_class_OnGetString])(field.type, (void *)data, tempString, null, null);
1273
1274                      if(s && s[0])
1275                         ProcessWordListString(s, method, id);
1276
1277                      if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1278                         ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
1279                      if(type.type == structClass)
1280                         delete data;
1281                   }
1282                }
1283             }
1284             if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1285             else
1286             {
1287                delete wordListPrepTimer.row; row = null;
1288                next = ++wordListPrepTimer.tablesIndex < searchTables.count;
1289             }
1290          }
1291
1292          if(!next)
1293          {
1294             char filePath[MAX_FILENAME];
1295             File f;
1296
1297             sprintf(filePath, "%s.search", table.name);
1298             // this doesn't want to work? :S :S :S
1299             // f == 0x0
1300             f = FileOpenBuffered(filePath, read);
1301             if(f)
1302             {
1303                f.Put(wordTree);
1304                delete f;
1305             }
1306
1307             wordListPrepTimer.hasCompleted = true;
1308             StopWordListPrepTimer();
1309          }
1310          return true;
1311       }
1312    };
1313
1314    void StopWordListPrepTimer()
1315    {
1316       wordListPrepTimer.Stop();
1317       wordListPrepTimer.tablesIndex = 0;
1318       delete wordListPrepTimer.row;
1319    }
1320
1321    ~TableEditor()
1322    {
1323       DebugLn("TableEditor::~()");
1324
1325       fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the incref in AddFieldBox? -- Free() does just that
1326       delete searchString;
1327       wordTree.Free();
1328
1329       delete listFields;
1330       delete lookups;
1331       delete dynamicLookupEditors;
1332       delete dynamicLookupTableEditors;
1333       if(searchTables) searchTables.Free();
1334       delete searchTables;
1335       if(sqliteSearchTables) sqliteSearchTables.Free();
1336       delete sqliteSearchTables;
1337    }
1338
1339    void ResetListFields()
1340    {
1341       DebugLn("TableEditor::ResetListFields");
1342       if(list && listFields && listFields.count)
1343       {
1344          bool c = list.created;
1345          list.ClearFields();
1346          for(lf : listFields)
1347          {
1348             list.AddField(lf.dataField);
1349             incref lf.dataField;
1350          }
1351       }
1352    }
1353
1354    void AddTableEditor(TableEditor tableEditor)
1355    {
1356       DebugLn("TableEditor::AddTableEditor");
1357       if(!tableEditors.Find(tableEditor))
1358       {
1359          tableEditors.Add(tableEditor);
1360          incref tableEditor;
1361       }
1362       else
1363          DebugLn("   TableEditor instance already added");
1364    }
1365
1366    void RemoveTableEditor(TableEditor tableEditor)
1367    {
1368       Iterator<TableEditor> it { tableEditors };
1369       DebugLn("TableEditor::RemoveTableEditor");
1370       if(it.Find(tableEditor))
1371       {
1372          it.Remove(); // tableEditors.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1373          delete tableEditor; // AddTableEditor increfs...
1374       }
1375       else
1376          DebugLn("   TableEditor instance not found, no need to remove");
1377    }
1378
1379    void AddFieldBox(FieldBox fieldBox)
1380    {
1381       // I was putting this here to force autosize on the lists (e.g. the Radiologists fields):
1382       /*
1383       if(!fieldBox.autoSize)
1384          fieldBox.autoSize = true;
1385       */
1386       DebugLn("TableEditor::AddFieldBox");
1387       if(!fieldsBoxes.Find(fieldBox))
1388       {
1389          fieldsBoxes.Add(fieldBox);
1390          if(table)
1391             fieldBox.Init();
1392          incref fieldBox;
1393       }
1394       else
1395          DebugLn("   FieldBox instance already added");
1396    }
1397
1398    void RemoveFieldBox(FieldBox fieldBox)
1399    {
1400       Iterator<FieldBox> it { fieldsBoxes };
1401       DebugLn("TableEditor::RemoveFieldBox");
1402       if(it.Find(fieldBox))
1403       {
1404          it.Remove(); // fieldsBoxes.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1405       }
1406       else
1407          DebugLn("   FieldBox instance not found, no need to remove");
1408    }
1409
1410    void InitFieldsBoxes()
1411    {
1412       DebugLn("TableEditor::InitFieldsBoxes");
1413       if(readOnly)
1414       {
1415          for(fb : fieldsBoxes)
1416          {
1417             fb.readOnly = true;
1418             fb.Init();
1419          }
1420       }
1421       else
1422       {
1423          for(fb : fieldsBoxes)
1424             fb.Init();
1425       }
1426       //NotifyInitFields(master, this);
1427    }
1428
1429    void EditNew()
1430    {
1431       DebugLn("TableEditor::EditNew");
1432
1433       modifiedDocument = false;
1434    }
1435
1436    void EditSave()
1437    {
1438       DebugLn("TableEditor::EditSave");
1439       internalModifications = true;
1440       for(fb : fieldsBoxes)
1441          fb.Save();
1442
1443       if(idField && list && listFields && listFields.count)
1444       {
1445          DataRow listRow = list.currentRow;
1446          // ADDED THIS HERE FOR SQLITE TO REFRESH
1447          editRow.Find(idField, middle, nil, listRow.tag);
1448          SetListRowFields(editRow, listRow, false);
1449          list.Sort(listSortField, listSortOrder);
1450       }
1451       internalModifications = false;
1452
1453       for(te : tableEditors)
1454          te.EditSave();
1455
1456       modifiedDocument = false;
1457       OnStateChanged();
1458    }
1459
1460    void EditLoad()
1461    {
1462       Id selId = selectedId;
1463       DebugLn("TableEditor::EditLoad");
1464       EditClear();
1465       selectedId = selId;
1466       OnLoad();
1467       internalModifications = true;
1468       for(lu : lookups)
1469       {
1470          if(&lu == table)
1471          {
1472             if(!lu.row)
1473                lu.row = { lu.findIndex ? lu.findIndex : lu.findField.table };
1474             if(lu.valueField && eClass_IsDerived(lu.valueField.type, class(Id)) &&
1475                   lu.findField && eClass_IsDerived(lu.findField.type, class(Id)))
1476             {
1477                Id id = 0;
1478                editRow.GetData(lu.valueField, id);
1479                lu.row.Find(lu.findField, middle, nil, id);
1480             }
1481          }
1482       }
1483       for(fb : fieldsBoxes)
1484          fb.Load();
1485       OnCreateDynamicLookupEditors();
1486       internalModifications = false;
1487       size = size;   // This seems to be required to fix autoSize on entering order screen
1488
1489       DebugLn("   TODO: implement virtual method TableEditor::OnSubEditorsLoad");
1490
1491       modifiedDocument = false;
1492       OnStateChanged();
1493    }
1494
1495    void EditClear()
1496    {
1497       DebugLn("TableEditor::EditClear");
1498       selectedId = 0;
1499       internalModifications = true;
1500       for(fb : fieldsBoxes)
1501          fb.Clear();
1502       for(e : dynamicLookupTableEditors)
1503          e.Destroy(0);
1504       for(e : tableEditors)
1505          e.Destroy(0);
1506       tableEditors.Free();
1507       dynamicLookupTableEditors.Free();
1508       //dynamicLookupTableEditors.size = 0;
1509       internalModifications = false;
1510
1511       DebugLn("   TODO: remove all sub table editors");
1512
1513       modifiedDocument = false;
1514       OnStateChanged();
1515    }
1516
1517    void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection)
1518    {
1519 //      DebugLn("TableEditor::SetListRowFields");
1520       for(lf : listFields)
1521       {
1522          if(lf.dataField && lf.field)
1523          {
1524             if(eClass_IsDerived(lf.field.type, class(char*)))
1525             {
1526                String s = null;
1527                dbRow.GetData(lf.field, s);
1528                listRow.SetData(lf.dataField, s);
1529                delete s;
1530             }
1531             else if(eClass_IsDerived(lf.field.type, class(Id)))
1532             {
1533                if(lf.CustomLookup)
1534                {
1535                   Id id = 0;
1536                   String s = null;
1537                   dbRow.GetData(lf.field, id);
1538                   s = lf.CustomLookup(id);
1539                   listRow.SetData(lf.dataField, s);
1540                   delete s; // ?
1541                }
1542                else if(lf.lookupFindField && (lf.lookupFindIndex || lf.lookupFindField.table) && lf.lookupValueField &&
1543                      eClass_IsDerived(lf.lookupFindField.type, class(Id)) &&
1544                      eClass_IsDerived(lf.lookupValueField.type, class(char*)))
1545                {
1546                   Id id = 0;
1547                   String s = null;
1548                   Row lookupRow { lf.lookupFindIndex ? lf.lookupFindIndex : lf.lookupFindField.table };
1549                   dbRow.GetData(lf.field, id);
1550                   if(lookupRow.Find(lf.lookupFindField, middle, nil, id))
1551                      lookupRow.GetData(lf.lookupValueField, s);
1552                   listRow.SetData(lf.dataField, s);
1553                   delete s;
1554                   delete lookupRow;
1555                }
1556             }
1557             else if(lf.CustomLookup && lf.field.type)
1558             {
1559                char * n = lf.field.name;
1560                int64 data = 0;
1561                String s = null;
1562                Class type = lf.field.type;
1563                if(type.type == unitClass && !type.typeSize)
1564                {
1565                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1566                   if(dataType)
1567                      type = dataType;
1568                }
1569                if(type.type == structClass)
1570                   data = (int64)new0 byte[type.structSize];
1571                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1572                s = lf.CustomLookup((int)data);
1573                listRow.SetData(lf.dataField, s);
1574                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1575                   ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
1576                if(type.type == structClass)
1577                   delete data;
1578                delete s; // ?
1579             }
1580             else if(lf.field.type && eClass_IsDerived(lf.dataField.dataType, class(char*)))
1581             {
1582                char * n = lf.field.name;
1583                char tempString[MAX_F_STRING];
1584                int64 data = 0;
1585                Class type = lf.field.type;
1586                String s;
1587                if(type.type == unitClass && !type.typeSize)
1588                {
1589                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1590                   if(dataType)
1591                      type = dataType;
1592                }
1593                if(type.type == structClass)
1594                   data = (int64)new0 byte[type.structSize];
1595                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1596                if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1597                   s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)lf.field.type._vTbl[__ecereVMethodID_class_OnGetString])(lf.field.type, &data, tempString, null, null);
1598                else
1599                   s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)lf.field.type._vTbl[__ecereVMethodID_class_OnGetString])(lf.field.type, (void*)data, tempString, null, null);
1600
1601                listRow.SetData(lf.dataField, s);
1602
1603                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1604                   ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
1605                if(type.type == structClass)
1606                   delete data;
1607             }
1608             else if(lf.field.type)
1609             {
1610                char * n = lf.field.name;
1611                //char tempString[256];
1612                int64 data = 0;
1613                Class type = lf.field.type;
1614                if(type.type == unitClass && !type.typeSize)
1615                {
1616                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1617                   if(dataType)
1618                      type = dataType;
1619                }
1620                if(type.type == structClass)
1621                   data = (int64)new0 byte[type.structSize];
1622                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1623                if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1624                   listRow.SetData(lf.dataField, (void *)&data);
1625                else
1626                   listRow.SetData(lf.dataField, (void *)data);
1627                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1628                   ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
1629                if(type.type == structClass)
1630                   delete data;
1631             }
1632          }
1633       }
1634       if(restoreSelection && !list.currentRow)
1635       {
1636          DataRow select;
1637          if((select = list.FindRow(selectedId)))
1638             SelectListRow(select);
1639       }
1640    }
1641
1642    Array<Id> SearchWordList()
1643    {
1644       DebugLn("TableEditor::SearchWordList");
1645 #ifdef FULL_STRING_SEARCH
1646    {
1647       int c;
1648       int numTokens = 0;
1649       int len[256];
1650       char * words[256];
1651       WordEntry entries[256];
1652       Array<Id> results = null;
1653       if(searchTables && searchTables.count && searchString && searchString[0])
1654       {
1655          char * searchCopy = CopyString(searchString);
1656          numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false);
1657          for(c = 0; c<numTokens; c++)
1658          {
1659             len[c] = strlen(words[c]);
1660             strlwr(words[c]);
1661             entries[c] = (WordEntry)wordTree.FindString(words[c]);
1662          }
1663          delete searchCopy;
1664       }
1665       if(numTokens)
1666       {
1667          if(numTokens > 1)
1668          {
1669             // AND
1670             int i;
1671             Map<Id, int> matches { };
1672             Map<Id, int> uniques { };
1673             MapNode<Id, int> mn;
1674             results = { };
1675             for(c = 0; c<numTokens; c++)
1676             {
1677                if(entries[c] && entries[c].items && entries[c].items.count)
1678                {
1679                   for(i = 0; i<entries[c].items.count; i++)
1680                   {
1681                      int count = uniques[entries[c].items.ids[i]];
1682 #ifdef _DEBUG
1683                      if(count != 0)
1684                         DebugLn("Problem");
1685 #endif
1686                      matches[entries[c].items.ids[i]]++;
1687                   }
1688                   uniques.Free();
1689                }
1690             }
1691             for(mn = matches.root.minimum; mn; mn = mn.next)
1692             {
1693                if(mn.value > 1)
1694                   results.Add(mn.key);
1695             }
1696             matches.Free();
1697             delete matches;
1698             delete uniques;
1699          }
1700          else if(numTokens == 1)
1701          {
1702             results = { };
1703             if(entries[0] && entries[0].items && entries[0].items.count)
1704             {
1705                for(c = 0; c<entries[0].items.count; c++)
1706                   results.Add(entries[0].items.ids[c]);
1707             }
1708          }
1709       }
1710       return results;
1711    }
1712 #else
1713       return null;
1714 #endif
1715
1716    }
1717
1718    // find a way to not load a tree for different searchFields
1719    // if the code that sets the searchFields has changed
1720    // store a search index signature containing following:
1721    // tables name, idField name and type, fields name and type
1722    void PrepareWordList()
1723    {
1724       DebugLn("TableEditor::PrepareWordList");
1725 #ifdef FULL_STRING_SEARCH
1726    {
1727       char filePath[MAX_FILENAME];
1728       File f;
1729
1730       sprintf(filePath, "%s.search", table.name);
1731       f = filePath ? FileOpenBuffered(filePath, read) : null;
1732       if(f)
1733       {
1734          int a;
1735          f.Get(wordTree);
1736          delete f;
1737
1738          for(a = 0; a<26; a++)
1739          {
1740             int b;
1741             char word[3];
1742             word[0] = 'a' + (char)a;
1743             word[1] = 0;
1744             word[2] = 0;
1745             letters[a] = (WordEntry)wordTree.FindString(word);
1746             for(b = 0; b<26; b++)
1747             {
1748                word[1] = 'a' + (char)b;
1749                doubleLetters[a][b] = (WordEntry)wordTree.FindString(word);
1750             }
1751          }
1752       }
1753       else if(searchTables && searchTables.count)
1754       {
1755          if(!letters[0])
1756          {
1757             int a;
1758             for(a = 0; a<26; a++)
1759             {
1760                int b;
1761                char word[3];
1762                word[0] = 'a' + (char)a;
1763                word[1] = 0;
1764                word[2] = 0;
1765                wordTree.Add((BTNode)(letters[a] = WordEntry { string = CopyString(word) }));
1766                for(b = 0; b<26; b++)
1767                {
1768                   word[1] = 'a' + (char)b;
1769                   wordTree.Add((BTNode)(doubleLetters[a][b] = WordEntry { string = CopyString(word) }));
1770                }
1771             }
1772          }
1773
1774          wordListPrepTimer.tablesIndex = 0;
1775          wordListPrepTimer.Start();
1776       }
1777    }
1778 #endif
1779    }
1780
1781    void ProcessWordListString(char * string, StringSearchIndexingMethod method, Id id)
1782    {
1783       int c;
1784       unichar ch;
1785       unichar lastCh = 0;
1786       int count = 0;
1787       int numChars = 0;
1788       int nb;
1789       char word[1024];
1790       char asciiWord[1024];
1791
1792       for(c = 0; ; c += nb)
1793       {
1794          ch = UTF8GetChar(string + c, &nb);
1795
1796          if(!ch || CharMatchCategories(ch, separators) ||
1797             (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
1798          {
1799             if(count)
1800             {
1801                word[count] = 0;
1802                asciiWord[numChars] = 0;
1803                strlwr(word);
1804                strlwr(asciiWord);
1805
1806                AddWord(word, count, method == allSubstrings, id);
1807                if(count > numChars)
1808                   AddWord(asciiWord, strlen(asciiWord), method == allSubstrings, id);
1809                count = 0;
1810                numChars = 0;
1811             }
1812             if(!CharMatchCategories(ch, separators))
1813             {
1814                int cc;
1815                for(cc = 0; cc < nb; cc++)
1816                   word[count++] = string[c + cc];
1817
1818                asciiWord[numChars++] = ToASCII(ch);
1819             }
1820             if(!ch)
1821                break;
1822          }
1823          else
1824          {
1825             int cc;
1826             for(cc = 0; cc < nb; cc++)
1827                word[count++] = string[c + cc];
1828
1829             asciiWord[numChars++] = ToASCII(ch);
1830          }
1831          lastCh = ch;
1832       }
1833    }
1834
1835    /*static */WordEntryBinaryTree wordTree
1836    {
1837       CompareKey = (void *)BinaryTree::CompareString;
1838       FreeKey = BinaryTree::FreeString;
1839    };
1840
1841    WordEntry letters[26];
1842    WordEntry doubleLetters[26][26];
1843
1844    void AddWord(char * word, int count, bool addAllSubstrings, Id id)
1845    {
1846       //DebugLn("TableEditor::AddWord");
1847 #ifdef FULL_STRING_SEARCH
1848    {
1849       if(addAllSubstrings)
1850       {
1851          int s;
1852          WordEntry mainEntry = null;
1853          WordEntry sEntry = null;
1854
1855          for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
1856          {
1857             int l;
1858             char subWord[1024];
1859             char ch1;
1860             WordEntry lEntry = null;
1861             memcpy(subWord, word + s, count-s);
1862             subWord[count-s] = 0;   // THIS IS REQUIRED!! THE WHILE LOOP BELOW CHECKED count-s FIRST!!
1863             ch1 = subWord[0];
1864
1865             for(l = count-s; l>0; l--)
1866             {
1867                uint wid;
1868                WordEntry start = null, wordEntry;
1869
1870                while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
1871                if(!l) break;
1872
1873                subWord[l] = 0;
1874
1875                if(ch1 >= 'a' && ch1 <= 'z')
1876                {
1877                   char ch2 = subWord[1];
1878                   if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
1879                   {
1880                      char ch2 = subWord[1];
1881                      start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
1882                   }
1883                   else
1884                   {
1885                      start = letters[ch1 - 'a'];
1886                   }
1887                }
1888
1889                if(start)
1890                {
1891                   WordEntry max;
1892                   while(start && (max = (WordEntry)((BTNode)start).maximum))
1893                   {
1894                      if(strcmp(max.string, subWord) >= 0)
1895                         break;
1896                      start = start.parent;
1897                   }
1898                }
1899
1900                if(!start)
1901                   start = (WordEntry)wordTree.root;
1902
1903                if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
1904                {
1905
1906                }
1907                else
1908                {
1909                   wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
1910                }
1911                if(!mainEntry)
1912                {
1913                   mainEntry = wordEntry;
1914                   sEntry = wordEntry;
1915                   lEntry = wordEntry;
1916                }
1917                else if(!sEntry)
1918                {
1919                   sEntry = wordEntry;
1920                   lEntry = wordEntry;
1921                   if(!wordEntry.words) wordEntry.words = IdList { };
1922                   wordEntry.words.Add((Id)mainEntry);
1923                }
1924                else if(!lEntry)
1925                {
1926                   lEntry = wordEntry;
1927                   if(!wordEntry.words) wordEntry.words = IdList { };
1928                   wordEntry.words.Add((Id)sEntry);
1929                }
1930                else
1931                {
1932                   if(!wordEntry.words) wordEntry.words = IdList { };
1933                   wordEntry.words.Add((Id)lEntry);
1934                }
1935                if(!wordEntry.items) wordEntry.items = IdList { };
1936                wordEntry.items.Add(id);
1937             }
1938          }
1939       }
1940       else
1941       {
1942          WordEntry wordEntry;
1943          if(!(wordEntry = (WordEntry)(wordTree.root).FindString(word)))
1944             wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(word) }));
1945          if(!wordEntry.items) wordEntry.items = IdList { };
1946          wordEntry.items.Add(id);
1947       }
1948    }
1949 #endif
1950    }
1951 }
1952
1953 public class ListField : struct
1954 {
1955 public:
1956    Field field;
1957    DataField dataField;
1958    Field lookupFindField;
1959    Field lookupValueField;
1960    Table lookupFindIndex;
1961    String (*CustomLookup)(Id);
1962 private:
1963
1964    ~ListField()
1965    {
1966       delete dataField;
1967    }
1968 }
1969
1970 public class Lookup
1971 {
1972 public:
1973    Field valueField;
1974    Field findField;
1975    Table findIndex;
1976
1977 private:
1978    Row row;
1979
1980    ~Lookup()
1981    {
1982       delete row;
1983    }
1984 }
1985
1986 public class LookupEditor : struct
1987 {
1988 public:
1989    subclass(TableEditor) editorClass;
1990    Window parentWindow;
1991    Field lookupValueField;
1992    Field lookupFindField;
1993    Field lookupIdField;
1994    Table lookupFindIndex;
1995 }
1996
1997 // all methods currently perform ascii conversion and all that jazz on every string added to the index
1998 public enum StringSearchIndexingMethod { fullString, allSubstrings };
1999
2000 public class StringSearchField
2001 {
2002 public:
2003    Field field;
2004    StringSearchIndexingMethod method;
2005
2006    Field lookupFindField;
2007    Field lookupValueField;
2008
2009    //String (*CustomRead)(Id);
2010 };
2011
2012 public class StringSearchTable
2013 {
2014 public:
2015    Table table;
2016    Field idField;
2017    Array<StringSearchField> searchFields;
2018
2019 private:
2020    ~StringSearchTable()
2021    {
2022       delete searchFields;
2023    }
2024 }
2025
2026 public class SQLiteSearchField
2027 {
2028 public:
2029    Field field;
2030    //StringSearchIndexingMethod method;
2031 };
2032
2033 public class SQLiteSearchTable
2034 {
2035 public:
2036    Table table;
2037    Field idField;
2038    Array<SQLiteSearchField> searchFields;
2039
2040 private:
2041    ~SQLiteSearchTable()
2042    {
2043       delete searchFields;
2044    }
2045 }
2046
2047 static WordEntry * btnodes;
2048
2049 struct WordEntryBinaryTree : BinaryTree
2050 {
2051    WordEntry * entries;
2052    
2053    void OnSerialize(IOChannel channel)
2054    {
2055       WordEntry node;
2056       uint id;
2057       uint count = this.count;
2058       DebugLn("WordEntryBinaryTree::OnSerialize");
2059       for(id = 1, node = (WordEntry)root; node;)
2060       {
2061          node.id = id++;
2062          if(node.left)
2063             node = node.left;
2064          else if(node.right)
2065             node = node.right;
2066          else if(node.parent)
2067          {
2068             bool isLeft = node == node.parent.left;
2069             node = node.parent;
2070             
2071             while(node)
2072             {
2073                if(isLeft && node.right)
2074                {
2075                   node = node.right;
2076                   break;
2077                }
2078                if(node.parent)
2079                   isLeft = node == node.parent.left;
2080                node = node.parent;
2081             }
2082          }
2083          else
2084             node = null;
2085       }
2086
2087       id--;
2088       channel.Serialize(id);
2089       channel.Serialize((WordEntry)root);
2090    }
2091
2092    void OnUnserialize(IOChannel channel)
2093    {
2094       WordEntry root, node;
2095       uint count;
2096       DebugLn("WordEntryBinaryTree::OnUnserialize");
2097       channel.Unserialize(count);
2098       entries = new WordEntry[count];      
2099       btnodes = entries;
2100       channel.Unserialize(root);
2101       this.root = (BTNode)root;
2102       // count = root ? this.root.count : 0;      
2103       this.count = count;
2104       for(node = (WordEntry)root; node;)
2105       {
2106          if(node.words)
2107          {
2108             int c;
2109             for(c = 0; c<node.words.count; c++)
2110                node.words.ids[c] = (Id)btnodes[node.words.ids[c] - 1];
2111          }
2112          if(node.left)
2113             node = node.left;
2114          else if(node.right)
2115             node = node.right;
2116          else if(node.parent)
2117          {
2118             bool isLeft = node == node.parent.left;
2119             node = node.parent;
2120             
2121             while(node)
2122             {
2123                if(isLeft && node.right)
2124                {
2125                   node = node.right;
2126                   break;
2127                }
2128                if(node.parent)
2129                   isLeft = node == node.parent.left;
2130                node = node.parent;
2131             }
2132          }
2133          else
2134             node = null;
2135       }
2136       delete entries;
2137       btnodes = null;
2138    }
2139 };
2140
2141 class WordEntry : struct
2142 {
2143    String string;
2144    WordEntry parent;
2145    WordEntry left, right;
2146    int depth;
2147    
2148    IdList items;
2149    IdList words;
2150    uint id;
2151
2152    ~WordEntry()
2153    {
2154       delete items;
2155       delete words;
2156    }
2157
2158    void OnSerialize(IOChannel channel)
2159    {
2160 #ifdef FULL_STRING_SEARCH
2161       if(this)
2162       {
2163          channel.Serialize(id);
2164          channel.Serialize(string);
2165          channel.Serialize(items);
2166
2167          if(words)
2168          {
2169             int c;
2170             channel.Serialize(words.count);
2171             for(c = 0; c < words.count; c++)
2172             {
2173                uint id = ((WordEntry)words.ids[c]).id;
2174                channel.Serialize(id);
2175             }
2176          }
2177          else
2178          {
2179             Id none = MAXDWORD;
2180             channel.Serialize(none);
2181          }
2182
2183          // channel.Serialize(words);
2184          channel.Serialize(left);
2185          channel.Serialize(right);
2186       }
2187       else
2188       {
2189          uint nothing = 0;
2190          channel.Serialize(nothing);
2191       }
2192 #endif
2193    }
2194
2195    void OnUnserialize(IOChannel channel)
2196    {
2197 #ifdef FULL_STRING_SEARCH
2198       uint id;
2199       channel.Unserialize(id);
2200       if(id)
2201       {
2202          uint count;
2203          WordEntry entry;
2204          // TODO: Fix typed_object issues
2205          entry = btnodes[id - 1] = eInstance_New(class(WordEntry));
2206          this = (void *)entry;
2207          
2208          channel.Unserialize(string);
2209          channel.Unserialize(items);
2210          channel.Unserialize(words);
2211
2212          channel.Unserialize(left);
2213          if(left) { left.parent = (void *)this; }
2214          channel.Unserialize(right);
2215          if(right) { right.parent = (void *)this; }
2216
2217          // TODO: Precomp errors without extra brackets
2218          depth = ((BTNode)((void *)this)).depthProp;
2219       }
2220       else
2221          this = null;
2222 #endif
2223    }
2224 }
2225
2226
2227 class ListEnumerationTimer : Timer
2228 {
2229    bool hasCompleted;
2230    int matchesIndex;
2231    bool sqliteSearch;
2232    int tablesIndex;
2233    Array<Id> matches;
2234    Row row;
2235    Map<Id, int> uniques;
2236 }
2237
2238 class WordListPrepTimer : Timer
2239 {
2240    bool hasCompleted;
2241    int tablesIndex;
2242    Row row;
2243 }
2244
2245 #if 0
2246 class EnumerateThread : Thread
2247 {
2248 public:
2249    bool active;
2250    TableEditor editor;
2251    //Table table;
2252    //Row r;
2253    Array<Id> matches;
2254
2255    void Abort()
2256    {
2257       /*if(abort)
2258          abortNow = true;
2259       else*/
2260       if(active)
2261          abort = true;
2262    }
2263
2264 private:
2265    bool abort, abortNow;
2266
2267    unsigned int Main()
2268    {
2269       app.Wait();
2270       app.Lock();
2271
2272       //if(app.ProcessInput(true))
2273          //app.Wait();
2274       {
2275          Row r { editor.table };
2276          if(matches)
2277          {
2278             int c;
2279             if(editor.listFields && editor.idField)
2280             {
2281                /*for(c=0; c<matches.count && !abort; c++)
2282                {
2283                   if(r.Find(editor.idField, middle, nil, matches[c]))
2284                   {
2285                      Id id = 0;
2286                      DataRow row;
2287                      GuiLock();
2288                      row = editor.list.AddRow();
2289                      r.GetData(editor.idField, id);
2290                      row.tag = id;
2291                      editor.SetListRowFields(r, row, true);
2292                      GuiUnlock();
2293                   }
2294                   else
2295                      DebugLn($"WordList match cannot be found in database.");
2296                }*/
2297             }
2298             else if(editor.idField && editor.stringField)
2299             {
2300                /*for(c=0; c<matches.count && !abort; c++)
2301                {
2302                   if(r.Find(editor.idField, middle, nil, matches[c]))
2303                   {
2304                      Id id = 0;
2305                      String s = null;
2306                      r.GetData(editor.idField, id);
2307                      r.GetData(editor.stringField, s);
2308                      GuiLock();
2309                      editor.list.AddString(s).tag = id;
2310                      GuiUnlock();
2311                      delete s;
2312                   }
2313                   else
2314                      DebugLn($"WordList match cannot be found in database.");
2315                }*/
2316             }
2317             else
2318                ;//app.Unlock();
2319          }
2320          else if(!editor.disabledFullListing)
2321          {
2322             if(editor.listFields && editor.idField)
2323             {
2324                app.Unlock();
2325                while(r.Next() && !abort)
2326                {
2327                   Id id = 0;
2328                   DataRow row;
2329                app.Unlock();
2330                   r.GetData(editor.idField, id);
2331                   //if(app.ProcessInput(true))
2332                      //app.Wait();
2333                   //app.Wait();
2334                   app.Lock();
2335                      row = editor.list.AddRow();
2336                      row.tag = id;
2337                      editor.SetListRowFields(r, row, true);
2338                   //app.Unlock();
2339                }
2340                //app.Unlock();
2341             }
2342             else if(editor.idField && editor.stringField)
2343             {
2344                /*while(r.Next() && !abort)
2345                {
2346                   Id id = 0;
2347                   String s = null;
2348                   GuiLock();
2349                   r.GetData(editor.idField, id);
2350                   r.GetData(editor.stringField, s);
2351                   editor.list.AddString(s).tag = id;
2352                   GuiUnlock();
2353                   delete s;
2354                }*/
2355             }
2356             else
2357                ;//app.Unlock();
2358          }
2359          else
2360             ;//app.Unlock();
2361
2362          //app.Lock();
2363             editor.list.Sort(editor.listSortField, editor.listSortOrder);
2364          //app.Unlock();
2365       }
2366       active = false;
2367       abort = false;
2368
2369       app.Unlock();
2370       return 0;
2371    }
2372
2373    /*void GuiLock()
2374    {
2375       app.Wait();
2376       app.Lock();
2377    }*/
2378
2379    /*void GuiUnlock()
2380    {
2381       app.Unlock();
2382       editor.list.Update(null);
2383       //app.Wait(); // Sleep(0.2f);
2384       //if(app.ProcessInput(true))
2385          //app.Wait();
2386          // Update(null);
2387          //app.UpdateDisplay();
2388       //app.Wait();
2389       // app.Lock();
2390    }*/
2391 }
2392 #endif
2393
2394 static define app = ((GuiApplication)__thisModule);
2395
2396 static inline void DebugLn(typed_object object, ...)
2397 {
2398 #if defined(_DEBUG_LINE)
2399    va_list args;
2400    char buffer[4096];
2401    va_start(args, object);
2402    PrintStdArgsToBuffer(buffer, sizeof(buffer), object, args);
2403    va_end(args);
2404    puts(buffer);
2405 #endif
2406 }