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