79d6186d9a35a1f00af9572bbb7ab9b5d491a7af
[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       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                               dataType._vTbl[__ecereVMethodID_class_OnGetDataFromString](dataType, data, newText);
589
590
591                            /*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                                  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                      if(type.type == unitClass && !type.typeSize)
1259                      {
1260                         Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1261                         if(dataType)
1262                            type = dataType;
1263                      }
1264                      if(type.type == structClass)
1265                         data = (int64)new0 byte[type.structSize];
1266                      ((bool (*)())(void *)row.GetData)(row, field, type, (type.type == structClass) ? (void *)data : &data);
1267
1268                      if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1269                         field.type._vTbl[__ecereVMethodID_class_OnGetString](field.type, &data, tempString, null, null);
1270                      else
1271                         field.type._vTbl[__ecereVMethodID_class_OnGetString](field.type, (void *)data, tempString, null, null);
1272
1273                      if(tempString[0])
1274                         ProcessWordListString(tempString, method, id);
1275
1276                      if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1277                         type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1278                      if(type.type == structClass)
1279                         delete data;
1280                   }
1281                }
1282             }
1283             if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
1284             else
1285             {
1286                delete wordListPrepTimer.row; row = null;
1287                next = ++wordListPrepTimer.tablesIndex < searchTables.count;
1288             }
1289          }
1290
1291          if(!next)
1292          {
1293             char filePath[MAX_FILENAME];
1294             File f;
1295
1296             sprintf(filePath, "%s.search", table.name);
1297             // this doesn't want to work? :S :S :S
1298             // f == 0x0
1299             f = FileOpenBuffered(filePath, read);
1300             if(f)
1301             {
1302                f.Put(wordTree);
1303                delete f;
1304             }
1305
1306             wordListPrepTimer.hasCompleted = true;
1307             StopWordListPrepTimer();
1308          }
1309          return true;
1310       }
1311    };
1312
1313    void StopWordListPrepTimer()
1314    {
1315       wordListPrepTimer.Stop();
1316       wordListPrepTimer.tablesIndex = 0;
1317       delete wordListPrepTimer.row;
1318    }
1319
1320    ~TableEditor()
1321    {
1322       DebugLn("TableEditor::~()");
1323
1324       fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the incref in AddFieldBox? -- Free() does just that
1325       delete searchString;
1326       wordTree.Free();
1327
1328       delete listFields;
1329       delete lookups;
1330       delete dynamicLookupEditors;
1331       delete dynamicLookupTableEditors;
1332       if(searchTables) searchTables.Free();
1333       delete searchTables;
1334       if(sqliteSearchTables) sqliteSearchTables.Free();
1335       delete sqliteSearchTables;
1336    }
1337
1338    void ResetListFields()
1339    {
1340       DebugLn("TableEditor::ResetListFields");
1341       if(list && listFields && listFields.count)
1342       {
1343          bool c = list.created;
1344          list.ClearFields();
1345          for(lf : listFields)
1346          {
1347             list.AddField(lf.dataField);
1348             incref lf.dataField;
1349          }
1350       }
1351    }
1352
1353    void AddTableEditor(TableEditor tableEditor)
1354    {
1355       DebugLn("TableEditor::AddTableEditor");
1356       if(!tableEditors.Find(tableEditor))
1357       {
1358          tableEditors.Add(tableEditor);
1359          incref tableEditor;
1360       }
1361       else
1362          DebugLn("   TableEditor instance already added");
1363    }
1364
1365    void RemoveTableEditor(TableEditor tableEditor)
1366    {
1367       Iterator<TableEditor> it { tableEditors };
1368       DebugLn("TableEditor::RemoveTableEditor");
1369       if(it.Find(tableEditor))
1370       {
1371          it.Remove(); // tableEditors.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1372          delete tableEditor; // AddTableEditor increfs...
1373       }
1374       else
1375          DebugLn("   TableEditor instance not found, no need to remove");
1376    }
1377
1378    void AddFieldBox(FieldBox fieldBox)
1379    {
1380       // I was putting this here to force autosize on the lists (e.g. the Radiologists fields):
1381       /*
1382       if(!fieldBox.autoSize)
1383          fieldBox.autoSize = true;
1384       */
1385       DebugLn("TableEditor::AddFieldBox");
1386       if(!fieldsBoxes.Find(fieldBox))
1387       {
1388          fieldsBoxes.Add(fieldBox);
1389          if(table)
1390             fieldBox.Init();
1391          incref fieldBox;
1392       }
1393       else
1394          DebugLn("   FieldBox instance already added");
1395    }
1396
1397    void RemoveFieldBox(FieldBox fieldBox)
1398    {
1399       Iterator<FieldBox> it { fieldsBoxes };
1400       DebugLn("TableEditor::RemoveFieldBox");
1401       if(it.Find(fieldBox))
1402       {
1403          it.Remove(); // fieldsBoxes.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
1404       }
1405       else
1406          DebugLn("   FieldBox instance not found, no need to remove");
1407    }
1408
1409    void InitFieldsBoxes()
1410    {
1411       DebugLn("TableEditor::InitFieldsBoxes");
1412       if(readOnly)
1413       {
1414          for(fb : fieldsBoxes)
1415          {
1416             fb.readOnly = true;
1417             fb.Init();
1418          }
1419       }
1420       else
1421       {
1422          for(fb : fieldsBoxes)
1423             fb.Init();
1424       }
1425       //NotifyInitFields(master, this);
1426    }
1427
1428    void EditNew()
1429    {
1430       DebugLn("TableEditor::EditNew");
1431
1432       modifiedDocument = false;
1433    }
1434
1435    void EditSave()
1436    {
1437       DebugLn("TableEditor::EditSave");
1438       internalModifications = true;
1439       for(fb : fieldsBoxes)
1440          fb.Save();
1441
1442       if(idField && list && listFields && listFields.count)
1443       {
1444          DataRow listRow = list.currentRow;
1445          // ADDED THIS HERE FOR SQLITE TO REFRESH
1446          editRow.Find(idField, middle, nil, listRow.tag);
1447          SetListRowFields(editRow, listRow, false);
1448          list.Sort(listSortField, listSortOrder);
1449       }
1450       internalModifications = false;
1451
1452       for(te : tableEditors)
1453          te.EditSave();
1454
1455       modifiedDocument = false;
1456       OnStateChanged();
1457    }
1458
1459    void EditLoad()
1460    {
1461       Id selId = selectedId;
1462       DebugLn("TableEditor::EditLoad");
1463       EditClear();
1464       selectedId = selId;
1465       OnLoad();
1466       internalModifications = true;
1467       for(lu : lookups)
1468       {
1469          if(&lu == table)
1470          {
1471             if(!lu.row)
1472                lu.row = { lu.findIndex ? lu.findIndex : lu.findField.table };
1473             if(lu.valueField && eClass_IsDerived(lu.valueField.type, class(Id)) &&
1474                   lu.findField && eClass_IsDerived(lu.findField.type, class(Id)))
1475             {
1476                Id id = 0;
1477                editRow.GetData(lu.valueField, id);
1478                lu.row.Find(lu.findField, middle, nil, id);
1479             }
1480          }
1481       }
1482       for(fb : fieldsBoxes)
1483          fb.Load();
1484       OnCreateDynamicLookupEditors();
1485       internalModifications = false;
1486       size = size;   // This seems to be required to fix autoSize on entering order screen
1487
1488       DebugLn("   TODO: implement virtual method TableEditor::OnSubEditorsLoad");
1489
1490       modifiedDocument = false;
1491       OnStateChanged();
1492    }
1493
1494    void EditClear()
1495    {
1496       DebugLn("TableEditor::EditClear");
1497       selectedId = 0;
1498       internalModifications = true;
1499       for(fb : fieldsBoxes)
1500          fb.Clear();
1501       for(e : dynamicLookupTableEditors)
1502          e.Destroy(0);
1503       for(e : tableEditors)
1504          e.Destroy(0);
1505       tableEditors.Free();
1506       dynamicLookupTableEditors.Free();
1507       //dynamicLookupTableEditors.size = 0;
1508       internalModifications = false;
1509
1510       DebugLn("   TODO: remove all sub table editors");
1511
1512       modifiedDocument = false;
1513       OnStateChanged();
1514    }
1515
1516    void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection)
1517    {
1518 //      DebugLn("TableEditor::SetListRowFields");
1519       for(lf : listFields)
1520       {
1521          if(lf.dataField && lf.field)
1522          {
1523             if(eClass_IsDerived(lf.field.type, class(char*)))
1524             {
1525                String s = null;
1526                dbRow.GetData(lf.field, s);
1527                listRow.SetData(lf.dataField, s);
1528                delete s;
1529             }
1530             else if(eClass_IsDerived(lf.field.type, class(Id)))
1531             {
1532                if(lf.CustomLookup)
1533                {
1534                   Id id = 0;
1535                   String s = null;
1536                   dbRow.GetData(lf.field, id);
1537                   s = lf.CustomLookup(id);
1538                   listRow.SetData(lf.dataField, s);
1539                   delete s; // ?
1540                }
1541                else if(lf.lookupFindField && (lf.lookupFindIndex || lf.lookupFindField.table) && lf.lookupValueField &&
1542                      eClass_IsDerived(lf.lookupFindField.type, class(Id)) &&
1543                      eClass_IsDerived(lf.lookupValueField.type, class(char*)))
1544                {
1545                   Id id = 0;
1546                   String s = null;
1547                   Row lookupRow { lf.lookupFindIndex ? lf.lookupFindIndex : lf.lookupFindField.table };
1548                   dbRow.GetData(lf.field, id);
1549                   if(lookupRow.Find(lf.lookupFindField, middle, nil, id))
1550                      lookupRow.GetData(lf.lookupValueField, s);
1551                   listRow.SetData(lf.dataField, s);
1552                   delete s;
1553                   delete lookupRow;
1554                }
1555             }
1556             else if(lf.CustomLookup && lf.field.type)
1557             {
1558                char * n = lf.field.name;
1559                int64 data = 0;
1560                String s = null;
1561                Class type = lf.field.type;
1562                if(type.type == unitClass && !type.typeSize)
1563                {
1564                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1565                   if(dataType)
1566                      type = dataType;
1567                }
1568                if(type.type == structClass)
1569                   data = (int64)new0 byte[type.structSize];
1570                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1571                s = lf.CustomLookup((int)data);
1572                listRow.SetData(lf.dataField, s);
1573                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1574                   type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1575                if(type.type == structClass)
1576                   delete data;
1577                delete s; // ?
1578             }
1579             else if(lf.field.type && eClass_IsDerived(lf.dataField.dataType, class(char*)))
1580             {
1581                char * n = lf.field.name;
1582                char tempString[MAX_F_STRING];
1583                int64 data = 0;
1584                Class type = lf.field.type;
1585                if(type.type == unitClass && !type.typeSize)
1586                {
1587                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1588                   if(dataType)
1589                      type = dataType;
1590                }
1591                if(type.type == structClass)
1592                   data = (int64)new0 byte[type.structSize];
1593                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1594                if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1595                   lf.field.type._vTbl[__ecereVMethodID_class_OnGetString](lf.field.type, &data, tempString, null, null);
1596                else
1597                   lf.field.type._vTbl[__ecereVMethodID_class_OnGetString](lf.field.type, (void*)data, tempString, null, null);
1598
1599                listRow.SetData(lf.dataField, tempString);
1600
1601                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1602                   type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1603                if(type.type == structClass)
1604                   delete data;
1605             }
1606             else if(lf.field.type)
1607             {
1608                char * n = lf.field.name;
1609                //char tempString[256];
1610                int64 data = 0;
1611                Class type = lf.field.type;
1612                if(type.type == unitClass && !type.typeSize)
1613                {
1614                   Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
1615                   if(dataType)
1616                      type = dataType;
1617                }
1618                if(type.type == structClass)
1619                   data = (int64)new0 byte[type.structSize];
1620                ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
1621                if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
1622                   listRow.SetData(lf.dataField, (void *)&data);
1623                else
1624                   listRow.SetData(lf.dataField, (void *)data);
1625                if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
1626                   type._vTbl[__ecereVMethodID_class_OnFree](type, data);
1627                if(type.type == structClass)
1628                   delete data;
1629             }
1630          }
1631       }
1632       if(restoreSelection && !list.currentRow)
1633       {
1634          DataRow select;
1635          if((select = list.FindRow(selectedId)))
1636             SelectListRow(select);
1637       }
1638    }
1639
1640    Array<Id> SearchWordList()
1641    {
1642       DebugLn("TableEditor::SearchWordList");
1643 #ifdef FULL_STRING_SEARCH
1644    {
1645       int c;
1646       int numTokens = 0;
1647       int len[256];
1648       char * words[256];
1649       WordEntry entries[256];
1650       Array<Id> results = null;
1651       if(searchTables && searchTables.count && searchString && searchString[0])
1652       {
1653          char * searchCopy = CopyString(searchString);
1654          numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false);
1655          for(c = 0; c<numTokens; c++)
1656          {
1657             len[c] = strlen(words[c]);
1658             strlwr(words[c]);
1659             entries[c] = (WordEntry)wordTree.FindString(words[c]);
1660          }
1661          delete searchCopy;
1662       }
1663       if(numTokens)
1664       {
1665          if(numTokens > 1)
1666          {
1667             // AND
1668             int i;
1669             Map<Id, int> matches { };
1670             Map<Id, int> uniques { };
1671             MapNode<Id, int> mn;
1672             results = { };
1673             for(c = 0; c<numTokens; c++)
1674             {
1675                if(entries[c] && entries[c].items && entries[c].items.count)
1676                {
1677                   for(i = 0; i<entries[c].items.count; i++)
1678                   {
1679                      int count = uniques[entries[c].items.ids[i]];
1680 #ifdef _DEBUG
1681                      if(count != 0)
1682                         DebugLn("Problem");
1683 #endif
1684                      matches[entries[c].items.ids[i]]++;
1685                   }
1686                   uniques.Free();
1687                }
1688             }
1689             for(mn = matches.root.minimum; mn; mn = mn.next)
1690             {
1691                if(mn.value > 1)
1692                   results.Add(mn.key);
1693             }
1694             matches.Free();
1695             delete matches;
1696             delete uniques;
1697          }
1698          else if(numTokens == 1)
1699          {
1700             results = { };
1701             if(entries[0] && entries[0].items && entries[0].items.count)
1702             {
1703                for(c = 0; c<entries[0].items.count; c++)
1704                   results.Add(entries[0].items.ids[c]);
1705             }
1706          }
1707       }
1708       return results;
1709    }
1710 #else
1711       return null;
1712 #endif
1713
1714    }
1715
1716    // find a way to not load a tree for different searchFields
1717    // if the code that sets the searchFields has changed
1718    // store a search index signature containing following:
1719    // tables name, idField name and type, fields name and type
1720    void PrepareWordList()
1721    {
1722       DebugLn("TableEditor::PrepareWordList");
1723 #ifdef FULL_STRING_SEARCH
1724    {
1725       char filePath[MAX_FILENAME];
1726       File f;
1727
1728       sprintf(filePath, "%s.search", table.name);
1729       f = filePath ? FileOpenBuffered(filePath, read) : null;
1730       if(f)
1731       {
1732          int a;
1733          f.Get(wordTree);
1734          delete f;
1735
1736          for(a = 0; a<26; a++)
1737          {
1738             int b;
1739             char word[3];
1740             word[0] = 'a' + (char)a;
1741             word[1] = 0;
1742             word[2] = 0;
1743             letters[a] = (WordEntry)wordTree.FindString(word);
1744             for(b = 0; b<26; b++)
1745             {
1746                word[1] = 'a' + (char)b;
1747                doubleLetters[a][b] = (WordEntry)wordTree.FindString(word);
1748             }
1749          }
1750       }
1751       else if(searchTables && searchTables.count)
1752       {
1753          if(!letters[0])
1754          {
1755             int a;
1756             for(a = 0; a<26; a++)
1757             {
1758                int b;
1759                char word[3];
1760                word[0] = 'a' + (char)a;
1761                word[1] = 0;
1762                word[2] = 0;
1763                wordTree.Add((BTNode)(letters[a] = WordEntry { string = CopyString(word) }));
1764                for(b = 0; b<26; b++)
1765                {
1766                   word[1] = 'a' + (char)b;
1767                   wordTree.Add((BTNode)(doubleLetters[a][b] = WordEntry { string = CopyString(word) }));
1768                }
1769             }
1770          }
1771
1772          wordListPrepTimer.tablesIndex = 0;
1773          wordListPrepTimer.Start();
1774       }
1775    }
1776 #endif
1777    }
1778
1779    void ProcessWordListString(char * string, StringSearchIndexingMethod method, Id id)
1780    {
1781       int c;
1782       unichar ch;
1783       unichar lastCh = 0;
1784       int count = 0;
1785       int numChars = 0;
1786       int nb;
1787       char word[1024];
1788       char asciiWord[1024];
1789
1790       for(c = 0; ; c += nb)
1791       {
1792          ch = UTF8GetChar(string + c, &nb);
1793
1794          if(!ch || CharMatchCategories(ch, separators) ||
1795             (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
1796          {
1797             if(count)
1798             {
1799                word[count] = 0;
1800                asciiWord[numChars] = 0;
1801                strlwr(word);
1802                strlwr(asciiWord);
1803
1804                AddWord(word, count, method == allSubstrings, id);
1805                if(count > numChars)
1806                   AddWord(asciiWord, strlen(asciiWord), method == allSubstrings, id);
1807                count = 0;
1808                numChars = 0;
1809             }
1810             if(!CharMatchCategories(ch, separators))
1811             {
1812                int cc;
1813                for(cc = 0; cc < nb; cc++)
1814                   word[count++] = string[c + cc];
1815
1816                asciiWord[numChars++] = ToASCII(ch);
1817             }
1818             if(!ch)
1819                break;
1820          }
1821          else
1822          {
1823             int cc;
1824             for(cc = 0; cc < nb; cc++)
1825                word[count++] = string[c + cc];
1826
1827             asciiWord[numChars++] = ToASCII(ch);
1828          }
1829          lastCh = ch;
1830       }
1831    }
1832
1833    /*static */WordEntryBinaryTree wordTree
1834    {
1835       CompareKey = (void *)BinaryTree::CompareString;
1836       FreeKey = BinaryTree::FreeString;
1837    };
1838
1839    WordEntry letters[26];
1840    WordEntry doubleLetters[26][26];
1841
1842    void AddWord(char * word, int count, bool addAllSubstrings, Id id)
1843    {
1844       //DebugLn("TableEditor::AddWord");
1845 #ifdef FULL_STRING_SEARCH
1846    {
1847       if(addAllSubstrings)
1848       {
1849          int s;
1850          WordEntry mainEntry = null;
1851          WordEntry sEntry = null;
1852
1853          for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
1854          {
1855             int l;
1856             char subWord[1024];
1857             char ch1;
1858             WordEntry lEntry = null;
1859             memcpy(subWord, word + s, count-s);
1860             subWord[count-s] = 0;   // THIS IS REQUIRED!! THE WHILE LOOP BELOW CHECKED count-s FIRST!!
1861             ch1 = subWord[0];
1862
1863             for(l = count-s; l>0; l--)
1864             {
1865                uint wid;
1866                WordEntry start = null, wordEntry;
1867
1868                while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
1869                if(!l) break;
1870
1871                subWord[l] = 0;
1872
1873                if(ch1 >= 'a' && ch1 <= 'z')
1874                {
1875                   char ch2 = subWord[1];
1876                   if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
1877                   {
1878                      char ch2 = subWord[1];
1879                      start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
1880                   }
1881                   else
1882                   {
1883                      start = letters[ch1 - 'a'];
1884                   }
1885                }
1886
1887                if(start)
1888                {
1889                   WordEntry max;
1890                   while(start && (max = (WordEntry)((BTNode)start).maximum))
1891                   {
1892                      if(strcmp(max.string, subWord) >= 0)
1893                         break;
1894                      start = start.parent;
1895                   }
1896                }
1897
1898                if(!start)
1899                   start = (WordEntry)wordTree.root;
1900
1901                if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
1902                {
1903
1904                }
1905                else
1906                {
1907                   wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
1908                }
1909                if(!mainEntry)
1910                {
1911                   mainEntry = wordEntry;
1912                   sEntry = wordEntry;
1913                   lEntry = wordEntry;
1914                }
1915                else if(!sEntry)
1916                {
1917                   sEntry = wordEntry;
1918                   lEntry = wordEntry;
1919                   if(!wordEntry.words) wordEntry.words = IdList { };
1920                   wordEntry.words.Add((Id)mainEntry);
1921                }
1922                else if(!lEntry)
1923                {
1924                   lEntry = wordEntry;
1925                   if(!wordEntry.words) wordEntry.words = IdList { };
1926                   wordEntry.words.Add((Id)sEntry);
1927                }
1928                else
1929                {
1930                   if(!wordEntry.words) wordEntry.words = IdList { };
1931                   wordEntry.words.Add((Id)lEntry);
1932                }
1933                if(!wordEntry.items) wordEntry.items = IdList { };
1934                wordEntry.items.Add(id);
1935             }
1936          }
1937       }
1938       else
1939       {
1940          WordEntry wordEntry;
1941          if(!(wordEntry = (WordEntry)(wordTree.root).FindString(word)))
1942             wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(word) }));
1943          if(!wordEntry.items) wordEntry.items = IdList { };
1944          wordEntry.items.Add(id);
1945       }
1946    }
1947 #endif
1948    }
1949 }
1950
1951 public class ListField : struct
1952 {
1953 public:
1954    Field field;
1955    DataField dataField;
1956    Field lookupFindField;
1957    Field lookupValueField;
1958    Table lookupFindIndex;
1959    String (*CustomLookup)(Id);
1960 private:
1961
1962    ~ListField()
1963    {
1964       delete dataField;
1965    }
1966 }
1967
1968 public class Lookup
1969 {
1970 public:
1971    Field valueField;
1972    Field findField;
1973    Table findIndex;
1974
1975 private:
1976    Row row;
1977
1978    ~Lookup()
1979    {
1980       delete row;
1981    }
1982 }
1983
1984 public class LookupEditor : struct
1985 {
1986 public:
1987    subclass(TableEditor) editorClass;
1988    Window parentWindow;
1989    Field lookupValueField;
1990    Field lookupFindField;
1991    Field lookupIdField;
1992    Table lookupFindIndex;
1993 }
1994
1995 // all methods currently perform ascii conversion and all that jazz on every string added to the index
1996 public enum StringSearchIndexingMethod { fullString, allSubstrings };
1997
1998 public class StringSearchField
1999 {
2000 public:
2001    Field field;
2002    StringSearchIndexingMethod method;
2003
2004    Field lookupFindField;
2005    Field lookupValueField;
2006
2007    //String (*CustomRead)(Id);
2008 };
2009
2010 public class StringSearchTable
2011 {
2012 public:
2013    Table table;
2014    Field idField;
2015    Array<StringSearchField> searchFields;
2016
2017 private:
2018    ~StringSearchTable()
2019    {
2020       delete searchFields;
2021    }
2022 }
2023
2024 public class SQLiteSearchField
2025 {
2026 public:
2027    Field field;
2028    //StringSearchIndexingMethod method;
2029 };
2030
2031 public class SQLiteSearchTable
2032 {
2033 public:
2034    Table table;
2035    Field idField;
2036    Array<SQLiteSearchField> searchFields;
2037
2038 private:
2039    ~SQLiteSearchTable()
2040    {
2041       delete searchFields;
2042    }
2043 }
2044
2045 static WordEntry * btnodes;
2046
2047 struct WordEntryBinaryTree : BinaryTree
2048 {
2049    WordEntry * entries;
2050    
2051    void OnSerialize(IOChannel channel)
2052    {
2053       WordEntry node;
2054       uint id;
2055       uint count = this.count;
2056       DebugLn("WordEntryBinaryTree::OnSerialize");
2057       for(id = 1, node = (WordEntry)root; node;)
2058       {
2059          node.id = id++;
2060          if(node.left)
2061             node = node.left;
2062          else if(node.right)
2063             node = node.right;
2064          else if(node.parent)
2065          {
2066             bool isLeft = node == node.parent.left;
2067             node = node.parent;
2068             
2069             while(node)
2070             {
2071                if(isLeft && node.right)
2072                {
2073                   node = node.right;
2074                   break;
2075                }
2076                if(node.parent)
2077                   isLeft = node == node.parent.left;
2078                node = node.parent;
2079             }
2080          }
2081          else
2082             node = null;
2083       }
2084
2085       id--;
2086       channel.Serialize(id);
2087       channel.Serialize((WordEntry)root);
2088    }
2089
2090    void OnUnserialize(IOChannel channel)
2091    {
2092       WordEntry root, node;
2093       uint count;
2094       DebugLn("WordEntryBinaryTree::OnUnserialize");
2095       channel.Unserialize(count);
2096       entries = new WordEntry[count];      
2097       btnodes = entries;
2098       channel.Unserialize(root);
2099       this.root = (BTNode)root;
2100       // count = root ? this.root.count : 0;      
2101       this.count = count;
2102       for(node = (WordEntry)root; node;)
2103       {
2104          if(node.words)
2105          {
2106             int c;
2107             for(c = 0; c<node.words.count; c++)
2108                node.words.ids[c] = (Id)btnodes[node.words.ids[c] - 1];
2109          }
2110          if(node.left)
2111             node = node.left;
2112          else if(node.right)
2113             node = node.right;
2114          else if(node.parent)
2115          {
2116             bool isLeft = node == node.parent.left;
2117             node = node.parent;
2118             
2119             while(node)
2120             {
2121                if(isLeft && node.right)
2122                {
2123                   node = node.right;
2124                   break;
2125                }
2126                if(node.parent)
2127                   isLeft = node == node.parent.left;
2128                node = node.parent;
2129             }
2130          }
2131          else
2132             node = null;
2133       }
2134       delete entries;
2135       btnodes = null;
2136    }
2137 };
2138
2139 class WordEntry : struct
2140 {
2141    String string;
2142    WordEntry parent;
2143    WordEntry left, right;
2144    int depth;
2145    
2146    IdList items;
2147    IdList words;
2148    uint id;
2149
2150    ~WordEntry()
2151    {
2152       delete items;
2153       delete words;
2154    }
2155
2156    void OnSerialize(IOChannel channel)
2157    {
2158 #ifdef FULL_STRING_SEARCH
2159       if(this)
2160       {
2161          channel.Serialize(id);
2162          channel.Serialize(string);
2163          channel.Serialize(items);
2164
2165          if(words)
2166          {
2167             int c;
2168             channel.Serialize(words.count);
2169             for(c = 0; c < words.count; c++)
2170             {
2171                uint id = ((WordEntry)words.ids[c]).id;
2172                channel.Serialize(id);
2173             }
2174          }
2175          else
2176          {
2177             Id none = MAXDWORD;
2178             channel.Serialize(none);
2179          }
2180
2181          // channel.Serialize(words);
2182          channel.Serialize(left);
2183          channel.Serialize(right);
2184       }
2185       else
2186       {
2187          uint nothing = 0;
2188          channel.Serialize(nothing);
2189       }
2190 #endif
2191    }
2192
2193    void OnUnserialize(IOChannel channel)
2194    {
2195 #ifdef FULL_STRING_SEARCH
2196       uint id;
2197       channel.Unserialize(id);
2198       if(id)
2199       {
2200          uint count;
2201          WordEntry entry;
2202          // TODO: Fix typed_object issues
2203          entry = btnodes[id - 1] = eInstance_New(class(WordEntry));
2204          this = (void *)entry;
2205          
2206          channel.Unserialize(string);
2207          channel.Unserialize(items);
2208          channel.Unserialize(words);
2209
2210          channel.Unserialize(left);
2211          if(left) { left.parent = (void *)this; }
2212          channel.Unserialize(right);
2213          if(right) { right.parent = (void *)this; }
2214
2215          // TODO: Precomp errors without extra brackets
2216          depth = ((BTNode)((void *)this)).depthProp;
2217       }
2218       else
2219          this = null;
2220 #endif
2221    }
2222 }
2223
2224
2225 class ListEnumerationTimer : Timer
2226 {
2227    bool hasCompleted;
2228    int matchesIndex;
2229    bool sqliteSearch;
2230    int tablesIndex;
2231    Array<Id> matches;
2232    Row row;
2233    Map<Id, int> uniques;
2234 }
2235
2236 class WordListPrepTimer : Timer
2237 {
2238    bool hasCompleted;
2239    int tablesIndex;
2240    Row row;
2241 }
2242
2243 #if 0
2244 class EnumerateThread : Thread
2245 {
2246 public:
2247    bool active;
2248    TableEditor editor;
2249    //Table table;
2250    //Row r;
2251    Array<Id> matches;
2252
2253    void Abort()
2254    {
2255       /*if(abort)
2256          abortNow = true;
2257       else*/
2258       if(active)
2259          abort = true;
2260    }
2261
2262 private:
2263    bool abort, abortNow;
2264
2265    unsigned int Main()
2266    {
2267       app.Wait();
2268       app.Lock();
2269
2270       //if(app.ProcessInput(true))
2271          //app.Wait();
2272       {
2273          Row r { editor.table };
2274          if(matches)
2275          {
2276             int c;
2277             if(editor.listFields && editor.idField)
2278             {
2279                /*for(c=0; c<matches.count && !abort; c++)
2280                {
2281                   if(r.Find(editor.idField, middle, nil, matches[c]))
2282                   {
2283                      Id id = 0;
2284                      DataRow row;
2285                      GuiLock();
2286                      row = editor.list.AddRow();
2287                      r.GetData(editor.idField, id);
2288                      row.tag = id;
2289                      editor.SetListRowFields(r, row, true);
2290                      GuiUnlock();
2291                   }
2292                   else
2293                      DebugLn($"WordList match cannot be found in database.");
2294                }*/
2295             }
2296             else if(editor.idField && editor.stringField)
2297             {
2298                /*for(c=0; c<matches.count && !abort; c++)
2299                {
2300                   if(r.Find(editor.idField, middle, nil, matches[c]))
2301                   {
2302                      Id id = 0;
2303                      String s = null;
2304                      r.GetData(editor.idField, id);
2305                      r.GetData(editor.stringField, s);
2306                      GuiLock();
2307                      editor.list.AddString(s).tag = id;
2308                      GuiUnlock();
2309                      delete s;
2310                   }
2311                   else
2312                      DebugLn($"WordList match cannot be found in database.");
2313                }*/
2314             }
2315             else
2316                ;//app.Unlock();
2317          }
2318          else if(!editor.disabledFullListing)
2319          {
2320             if(editor.listFields && editor.idField)
2321             {
2322                app.Unlock();
2323                while(r.Next() && !abort)
2324                {
2325                   Id id = 0;
2326                   DataRow row;
2327                app.Unlock();
2328                   r.GetData(editor.idField, id);
2329                   //if(app.ProcessInput(true))
2330                      //app.Wait();
2331                   //app.Wait();
2332                   app.Lock();
2333                      row = editor.list.AddRow();
2334                      row.tag = id;
2335                      editor.SetListRowFields(r, row, true);
2336                   //app.Unlock();
2337                }
2338                //app.Unlock();
2339             }
2340             else if(editor.idField && editor.stringField)
2341             {
2342                /*while(r.Next() && !abort)
2343                {
2344                   Id id = 0;
2345                   String s = null;
2346                   GuiLock();
2347                   r.GetData(editor.idField, id);
2348                   r.GetData(editor.stringField, s);
2349                   editor.list.AddString(s).tag = id;
2350                   GuiUnlock();
2351                   delete s;
2352                }*/
2353             }
2354             else
2355                ;//app.Unlock();
2356          }
2357          else
2358             ;//app.Unlock();
2359
2360          //app.Lock();
2361             editor.list.Sort(editor.listSortField, editor.listSortOrder);
2362          //app.Unlock();
2363       }
2364       active = false;
2365       abort = false;
2366
2367       app.Unlock();
2368       return 0;
2369    }
2370
2371    /*void GuiLock()
2372    {
2373       app.Wait();
2374       app.Lock();
2375    }*/
2376
2377    /*void GuiUnlock()
2378    {
2379       app.Unlock();
2380       editor.list.Update(null);
2381       //app.Wait(); // Sleep(0.2f);
2382       //if(app.ProcessInput(true))
2383          //app.Wait();
2384          // Update(null);
2385          //app.UpdateDisplay();
2386       //app.Wait();
2387       // app.Lock();
2388    }*/
2389 }
2390 #endif
2391
2392 static define app = ((GuiApplication)__thisModule);
2393
2394 static inline void DebugLn(typed_object object, ...)
2395 {
2396 #if defined(_DEBUG_LINE)
2397    va_list args;
2398    char buffer[4096];
2399    va_start(args, object);
2400    PrintStdArgsToBuffer(buffer, sizeof(buffer), object, args);
2401    va_end(args);
2402    puts(buffer);
2403 #endif
2404 }