wip II
[sdk] / eda / libeda / src / gui / TableEditor.ec
index a59fe93..300e8b6 100644 (file)
+#include <stdarg.h>
+
 import "idList"
 
 import "FieldBox"
 
+default:
+
+extern int __ecereVMethodID_class_OnFree;
+extern int __ecereVMethodID_class_OnGetString;
+extern int __ecereVMethodID_class_OnGetDataFromString;
+
+private:
+
+#ifdef _DEBUG
+// #define _DEBUG_LINE
+#endif
+
+#define FULL_STRING_SEARCH
+
 #define UTF8_IS_FIRST(x)   (__extension__({ byte b = x; (!(b) || !((b) & 0x80) || ((b) & 0x40)); }))
 #define UTF8_NUM_BYTES(x)  (__extension__({ byte b = x; (b & 0x80 && b & 0x40) ? ((b & 0x20) ? ((b & 0x10) ? 4 : 3) : 2) : 1; }))
 
-public class TableEditor
+define newEntryStringDebug = $"New|id=";
+define newEntryString = $"New";
+
+public char ToASCII(unichar ch)
+{
+   char asciiCH = 0;
+   if(ch > 127)
+   {
+      if(ch == 'À' || ch == 'Á' || ch == 'Â' || ch == 'Ã' || ch == 'Ä' || ch == 'Å')
+         asciiCH = 'A';
+      else if(ch == 'Ç')
+         asciiCH = 'C';
+      else if(ch == 'È' || ch == 'É' || ch == 'Ê' || ch == 'Ë')
+         asciiCH = 'E';
+      else if(ch == 'Ì' || ch == 'Í' || ch == 'Î' || ch == 'Ï')
+         asciiCH = 'I';
+      else if(ch == 'Ñ')
+         asciiCH = 'N';
+      else if(ch == 'Ò' || ch == 'Ó' || ch == 'Ô' || ch == 'Õ' || ch == 'Ö')
+         asciiCH = 'O';
+      else if(ch == 'Ù' || ch == 'Ú' || ch == 'Û' || ch == 'Ü')
+         asciiCH = 'U';
+      else if(ch == 'à' || ch == 'á' || ch == 'â' || ch == 'ã' || ch == 'ä' || ch == 'å')
+         asciiCH = 'a';
+      else if(ch == 'ç')
+         asciiCH = 'c';
+      else if(ch == 'è' || ch == 'é' || ch == 'ê' || ch == 'ë')
+         asciiCH = 'e';
+      else if(ch == 'ì' || ch == 'í' || ch == 'î' || ch == 'ï')
+         asciiCH = 'i';
+      else if(ch == 'ñ')
+         asciiCH = 'n';
+      else if(ch == 'ò' || ch == 'ó' || ch == 'ô' || ch == 'õ' || ch == 'ö')
+         asciiCH = 'o';
+      else if(ch == 'ù' || ch == 'ú' || ch == 'û' || ch == 'ü')
+         asciiCH = 'u';
+   }
+   else
+      asciiCH = (char)ch;
+   return asciiCH;
+}
+
+public char * ConvertToASCII(char * string, char * newString, int * len, bool lowerCase)
+{
+   if(string)
+   {
+      unichar unich;
+      int nb;
+      int c, d = 0;
+      for(c = 0; (unich = UTF8GetChar(string + c, &nb)); c += nb)
+      {
+         char ch = ToASCII(unich);
+         if(ch < 128)
+            newString[d++] = lowerCase ? (char)tolower(ch) : (char)ch;
+      }
+      newString[d] = 0;
+      if(len) *len = d;
+   }
+   return null;
+}
+
+public class NoCaseAccent : SQLCustomFunction
+{
+   Array<char> array { minAllocSize = 1024 };
+public:
+   String function(String text)
+   {
+      int len = text ? strlen(text) : 0;
+      array.size = len + 1;
+      ConvertToASCII(text ? text : "", array.array, &len, true);
+      return array.array;
+   }
+}
+
+public class MemberStringSample
+{
+   String name;
+}
+
+default extern int __ecereVMethodID_class_OnUnserialize;
+
+public class GetMemberString<class NT:void * = MemberStringSample, name = NT::name> : NoCaseAccent
+{
+public:
+   String function(NT pn)
+   {
+      return NoCaseAccent::function((pn && pn.name) ? pn.name : null);
+   }
+
+/*
+   // The old way is still possible...
+   SerialBuffer buffer { };
+   String function(void * ptr)
+   {
+      String result;
+      NT pn;
+      buffer.buffer = ptr;
+      buffer.count = MAXINT;
+      buffer.pos = 0;
+      ((void (*)(void *, void *, void *))(void *)class(NT)._vTbl[__ecereVMethodID_class_OnUnserialize])(class(NT), &pn, buffer);
+      result = NoCaseAccent::function((pn && pn.name) ? pn.name : null);
+      delete pn;
+      buffer.buffer = null;
+      buffer.count = 0;
+      // TOFIX: If we name GetName's type 'T', the following name confuses with Array's 'T'
+      //ConvertToASCII(s ? s : "", array.array, &len, true);
+      return result;
+   }
+*/
+}
+
+// Rename TableEditor to TableControl and move to eda/src/gui/controls
+public class TableEditor : public Window
 {
 public:
-   Window window;
    property Table table
    {
       set
       {
+         DebugLn("TableEditor::table|set");
          table = value;
-         if(table)
+      }
+      get { return table; }
+   }
+
+   property Table index
+   {
+      set
+      {
+         DebugLn("TableEditor::index|set");
+         index = value;
+         filterRow.tbl = index;
+      }
+      get { return index; }
+   }
+
+   bool OnPostCreate()
+   {
+      DebugLn("TableEditor::OnPostCreate");
+      if(table)
+      {
+         if(!initialized)
          {
+            ResetListFields();
+            if(searchTables)
+               PrepareWordList();
             InitFieldsBoxes(); // IMPORTANT: table must be set *AFTER* all related FieldEditors have been initialized
             {
                Field fldId = idField, fldName = stringField, fldActive = null;
@@ -26,60 +177,110 @@ public:
                indexedFields[0] = { fldId };
                table.Index(1, indexedFields);
                editRow.tbl = table;
-               Enumerate();
-               {
-                  char name[MAX_FILENAME];
-                  sprintf(name, "%s.search", value.name);
-                  PrepareWordList(name);
-               }
+               if(searchTables)
+                  PrepareWordList();
             }
+            initialized = true;
+            OnInitialize();
          }
+         if(!listEnumerationTimer.hasCompleted)
+            Enumerate();
+         if(list && !list.currentRow)
+            list.SelectRow(list.firstRow); // should the tableeditor select method be used here?
       }
+      return true;
+   }
+
+   bool OnClose(bool parentClosing)
+   {
+      bool result = NotifyClosing();
+      if(result)
+      {
+         EditClear();
+      }
+      return result;
    }
-   Table table;
 
    // List
    property ListBox list
    {
       set
       {
+         DebugLn("TableEditor::list|set");
          list = value;
-         ResetListFields();
+         //ResetListFields();
       }
    }
-   ListBox list;
-   Field idField;
-   Field stringField;
    property Array<ListField> listFields
    {
       set
       {
+         DebugLn("TableEditor::listFields|set");
          listFields = value;
-         ResetListFields();
+         //ResetListFields();
       }
    }
-   Array<ListField> listFields;
-   int listSortOrder;
-   DataField listSortField;
-   bool disabledFullListing;
-   property Array<Field> searchFields
+   property int listSortOrder
+   {
+      set { listSortOrder = value; }
+      get { return listSortOrder; }
+   }
+   property DataField listSortField
+   {
+      set { listSortField = value; }
+      get { return listSortField; }      
+   }
+   property bool disabledFullListing
+   {
+      set { disabledFullListing = value; }
+      get { return disabledFullListing; }
+   }
+
+   property Array<StringSearchField> searchFields
    {
       set
       {
-         searchFields = value;
-         {
-            char name[MAX_FILENAME];
-            sprintf(name, "%s.search", table.name);
-            PrepareWordList(name);
-         }
+         StringSearchTable searchTable { table, idField, value };
+         DebugLn("TableEditor::searchFields|set");
+         // The searchTables property will incref...
+         property::searchTables = { [ searchTable ] };
+      }
+   }
+
+   property Array<StringSearchTable> searchTables
+   {
+      set
+      {
+         // This API is not very clear on ownership of search tables array/search table/field...
+         // Right now both the array and tables themselves are incref'ed here
+         incref value;
+         for(t : value) { incref t; }
+         DebugLn("TableEditor::searchTables|set");
+         if(searchTables) searchTables.Free();
+         delete searchTables;
+         searchTables = value;
       }
    }
-   Array<Field> searchFields;
+
+   property Array<SQLiteSearchTable> sqliteSearchTables
+   {
+      set
+      {
+         incref value;
+         for(t : value) { incref t; }
+         DebugLn("TableEditor::searchTables|set");
+         if(sqliteSearchTables) sqliteSearchTables.Free();
+         delete sqliteSearchTables;
+         sqliteSearchTables = value;
+      }
+   }
+
    property String searchString
    {
       set
       {
-         bool modified = window && window.modifiedDocument;
+         bool modified = modifiedDocument;
+         DebugLn("TableEditor::searchString|set");
          switch(modified ? OnLeavingModifiedDocument() : no)
          {
             case cancel:
@@ -97,46 +298,79 @@ public:
          }
       }
    }
-   String searchString;
+   property Array<LookupEditor> dynamicLookupEditors
+   {
+      set
+      {
+         DebugLn("TableEditor::dynamicLookupEditors|set");
+         dynamicLookupEditors = value;
+      }
+   }
 
    // Fields Editor
    property Id selectedId { get { return selectedId; } }
 
-   Array<FieldBox> fieldsBoxes { };
-   
-   public virtual void Window::OnStateChanged();
-   bool internalModifications;
-   public void NotifyModifiedDocument()
+   property Field idField
+   {
+      set { idField = value; }
+      get { return idField; }
+   }
+   property Field stringField
+   {
+      set { stringField = value; }
+      get { return stringField; }
+   }
+   property Field indexFilterField
+   {
+      set { indexFilterField = value; }
+      get { return indexFilterField; }
+   }
+
+   property bool readOnly
+   {
+      set { readOnly = value; }
+      get { return readOnly; }
+   }
+
+   virtual void OnInitialize();
+   virtual void OnLoad();
+   virtual void OnStateChanged();
+
+   void NotifyModifiedDocument()
    {
+      DebugLn("TableEditor::NotifyModifiedDocument");
       if(!internalModifications)
-         OnStateChanged(window);
+         OnStateChanged();
    }
 
-   //public virtual bool Window::NotifyNew(AltListSection listSection, Row r);
+   //virtual bool Window::NotifyNew(AltListSection listSection, Row r);
    //virtual void Window::NotifyInitFields(AltEditSection editSection);
    
-   public virtual DialogResult OnLeavingModifiedDocument()
+   virtual DialogResult OnLeavingModifiedDocument()
    {
-      return MessageBox { master = window, type = yesNoCancel, text = window ? window.text : "Table Editor",
-                          contents = "You have modified this entry. Would you like to save it before proceeding?"
+      DebugLn("TableEditor::OnLeavingModifiedDocument");
+      return MessageBox { master = this, type = yesNoCancel, text = text && text[0] ? text : $"Table Editor",
+                          contents = $"You have modified this entry. Would you like to save it before proceeding?"
                   }.Modal();
    }
    
-   public virtual bool OnRemovalRequest()
+   virtual bool OnRemovalRequest()
    {
-      return MessageBox { master = window, type = yesNo, text = window ? window.text : "Table Editor", 
-                          contents =  "You are about to permanently remove an entry.\n"
-                                      "Do you wish to continue?"
+      DebugLn("TableEditor::OnRemovalRequest");
+      return MessageBox { master = this, type = yesNo, text = text && text[0] ? text : $"Table Editor",
+                          contents =  $"You are about to permanently remove an entry.\n"
+                                       "Do you wish to continue?"
                   }.Modal() == yes;
    }
 
-   //public virtual void Window::NotifyDeleting(ListSection listSection);
-   //public virtual void Window::NotifyDeleted(ListSection listSection);
+   //virtual void Window::NotifyDeleting(ListSection listSection);
+   //virtual void Window::NotifyDeleted(ListSection listSection);
 
-   public bool NotifyClosing()
+   bool NotifyClosing()
    {
       bool result = true;
-      if(window && window.modifiedDocument)
+      DebugLn("TableEditor::NotifyClosing");
+      if(modifiedDocument)
       {
          switch(OnLeavingModifiedDocument())
          {
@@ -146,112 +380,163 @@ public:
             case yes:
                EditSave();
             case no:
-               window.modifiedDocument = false;
+               EditLoad();
                break;
          }
       }
+      if(result)
+      {
+         StopWordListPrepTimer();
+         StopListEnumerationTimer();
+      }
       return result;
    }
 
-   //public void List() // this gets called out of nowhere by some constructor thing...
-   public void Enumerate()
+   //void List() // this gets called out of nowhere by some constructor thing...
+   void Enumerate()
    {
+      DebugLn("TableEditor::Enumerate");
       if(list)
       {
-         DataRow select;
+         StopListEnumerationTimer();
          list.Clear();
+         EditClear();
+      }
+      if(list || OnList != TableEditor::OnList)
+      {
+         Row r { table };
+         Array<Id> matches = null;
+         listEnumerationTimer.sqliteSearch = false;
+         if(searchTables && searchTables.count)
+            matches = SearchWordList();
+         //else if(sqliteSearchTables && sqliteSearchTables.count)
+            //matches = SearchSQLite();
+         else if(searchString && searchString[0] &&
+               sqliteSearchTables && sqliteSearchTables.count &&
+               sqliteSearchTables[0].table && sqliteSearchTables[0].idField &&
+               sqliteSearchTables[0].searchFields && sqliteSearchTables[0].searchFields.count &&
+               sqliteSearchTables[0].searchFields[0].field)
+            listEnumerationTimer.sqliteSearch = true;
+         if(matches && matches.count)
+            PrintLn("results count: ", matches.count);
+         OnList(r, matches);
+         delete matches;
+         delete r;
+      }
+      modifiedDocument = false; // setting this here is not really logical, enumeration and modified have nothing to do with eachother
+   }
+
+   virtual void OnList(Row r, Array<Id> matches)
+   {
+      DebugLn("TableEditor::OnList");
+      if(!listEnumerationTimer.started)
+      {
+         listEnumerationTimer.hasCompleted = false;
+         listEnumerationTimer.matchesIndex = 0;
+         listEnumerationTimer.tablesIndex = 0;
+         if(!listEnumerationTimer.sqliteSearch)
+            listEnumerationTimer.row = Row { r.tbl };
+         if(matches)
          {
-            Row r { table };
-            Array<Id> matches = SearchWordList();
-            OnList(this, r, matches);
-            delete matches;
-            delete r;
+            listEnumerationTimer.matches = { };
+            // TOFIX: This (void *) cast here should NOT be required... Fix this Container warning:
+            // warning: incompatible expression matches (ecere::com::Array<eda::Id>); expected ecere::com::Container<T>
+            listEnumerationTimer.matches.Copy((void *)matches);
          }
-         list.Sort(listSortField, listSortOrder);
-         if((select = list.FindRow(selectedId)))
-            SelectListRow(select);
          else
-            EditClear();
+            listEnumerationTimer.matches = null;
+         listEnumerationTimer.Start();
       }
-      if(window)
-         window.modifiedDocument = false;
+      else
+         DebugLn("TableEditor::OnList -- timer state error");
    }
 
-   virtual void TableEditor::OnList(Row r, Array<Id> matches)
+   virtual void OnCreateDynamicLookupEditors()
    {
-      if(matches)
+      DebugLn("TableEditor::OnCreateLookupEditors");
+      if(dynamicLookupEditors && dynamicLookupEditors.count)
       {
-         int c;
-         if(listFields && idField)
+         for(f : dynamicLookupEditors)
          {
-            for(c=0; c<matches.count; c++)
+            if(f.editorClass && f.parentWindow && f.lookupFindField)
             {
-               if(r.Find(idField, middle, nil, matches[c]))
+               Row row { f.lookupFindIndex ? f.lookupFindIndex : f.lookupFindField.table };
+               // todo: make this work for all types
+               uint id = 0;
+               editRow.GetData(f.lookupValueField, id);
+               // TODO: add alternative class instance for creation when no rows are found via lookup
+               for(row.Find(f.lookupFindField, middle, nil, id); !row.nil; row.Next())
                {
-                  Id id = 0;
-                  DataRow row = list.AddRow();
-                  r.GetData(idField, id);
-                  row.tag = id;
-                  SetListRowFields(r, row);
+                  // todo: make this work for all types, although this is meant to be an id field
+                  uint id = 0;
+                  TableEditor editor = eInstance_New(f.editorClass);
+                  incref editor;
+                  editor.parent = f.parentWindow;
+                  editor.master = this;
+                  dynamicLookupTableEditors.Add(editor);
+                  editor.Create();
+                  row.GetData(f.lookupIdField, id);
+                  editor.Select(id);
                }
-               else
-                  PrintLn("WordList match cannot be found in database.");
-            }
-         }
-         else if(idField && stringField)
-         {
-            for(c=0; c<matches.count; c++)
-            {
-               if(r.Find(idField, middle, nil, matches[c]))
-               {
-                  Id id = 0;
-                  String s = null;
-                  r.GetData(idField, id);
-                  r.GetData(stringField, s);
-                  list.AddString(s).tag = id;
-                  delete s;
-               }
-               else
-                  PrintLn("WordList match cannot be found in database.");
+               delete row;
             }
          }
       }
-      else if(!disabledFullListing)
+   }
+
+   property TableEditor masterEditor
+   {
+      set
       {
-         if(listFields && idField)
+         if(value != masterEditor)
          {
-            while(r.Next())
-            {
-               Id id = 0;
-               DataRow row = list.AddRow();
-               r.GetData(idField, id);
-               row.tag = id;
-               SetListRowFields(r, row);
-            }
-         }
-         else if(idField && stringField)
-         {
-            while(r.Next())
-            {
-               Id id = 0;
-               String s = null;
-               r.GetData(idField, id);
-               r.GetData(stringField, s);
-               list.AddString(s).tag = id;
-               delete s;
-            }
+            if(masterEditor)
+               masterEditor.RemoveTableEditor(this);
+            masterEditor = value;
+            if(value)
+               value.AddTableEditor(this);
          }
       }
+      get { return masterEditor; }
    }
 
-   void Create()
+   watch(parent)
+   {
+      if(eClass_IsDerived(parent._class, class(TableEditor)))
+         property::masterEditor = (TableEditor)parent;
+   };
+
+   watch(master)
+   {
+      if(eClass_IsDerived(master._class, class(TableEditor)))
+         property::masterEditor = (TableEditor)master;
+   };
+
+   watch(modifiedDocument)
    {
+      NotifyModifiedDocument();
+   };
+
+   void CreateRow()
+   {
+      DebugLn("TableEditor::CreateRow");
       //list.NotifySelect(this, list, null, 0);
-      if(window && !window.modifiedDocument)
+      if(table && editRow && editRow.tbl && !modifiedDocument)
       {
          uint id; // = table.rowsCount + 1; // this is bad with deleted rows, won't work, how to have unique id? 
-         Row r = editRow;// { table };
-      
+                               // I think the 3 following comment lines apply to the old sqlite driver before many fix we done for wsms
+         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...
+         //Row r { editRow.tbl };                    // for example, Row::Last() here is not using the proper sqlite statement and fails to
+                                                   // return false when no rows are present in a table
+         DataRow row = null;
+         String newText;
+
+         /*uint count = editRow.tbl.GetRowsCount();
+
+         id = 0;
+         // r.Last() is returning true even if there are not rows in this table (SQLite)
+         if(count && !(r.Last() || r.Last()))
+            DebugLn("PROBLEM");*/
          if(r.Last())   // this will reuse ids in cases where the item(s) with the last id have been deleted
          {
             r.GetData(idField, id);
@@ -259,60 +544,119 @@ public:
          }
          else
             id = 1;
-      
+
          EditClear();
          {
             bool active = true;
             r.Add();
             {
                // Patch for SQLite driver which auto-increments IDs
-               int curID = 0;
-               if(r.GetData(idField, curID))
-                  id = curID;
+               int curId = 0;
+               if(r.GetData(idField, curId))
+                  id = curId;
                else
                   r.SetData(idField, id);
-            }               
+            }
             /*if(fldActive)
                r.SetData(fldActive, active);*/
+            selectedId = id;
+
+#ifdef _DEBUG
+            newText = PrintString("[", newEntryStringDebug, id, "]");
+#else
+            newText = PrintString("[", newEntryString, "]");
+#endif
 
             //if(NotifyNew(master, this, r))
             if(listFields && idField)
             {
-               DataRow row;
                for(lf : listFields)
                {
                   if(lf.dataField && lf.field)
                   {
                      if(lf.field.type == class(String))
+                        r.SetData(lf.field, newText);
+                     else // this whole block is new?
                      {
-                        r.SetData(lf.field, "[New]");
+                        if(lf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])
+                        {
+                           Class dataType = lf.field.type;
+                           int64 dataHolder = 0;
+                           void * data = &dataHolder;
+
+                           if(dataType && dataType.type == structClass)
+                           {
+                              dataHolder = (int64)new0 byte[dataType.structSize];
+                              data = (void *)dataHolder;
+                           }
+                           /*else if(dataType && (dataType.type == noHeadClass || dataType.type == normalClass))
+                           {
+                              if(eClass_IsDerived(dataType, class(char*)))
+                                 dataHolder = (int64)CopyString("");
+                              else
+                                 dataHolder = (int64)eInstance_New(dataType);
+                              data = (void *)&dataHolder;
+                           }
+                           else
+                           {
+                              dataHolder = 0;
+                              data = &dataHolder;
+                           }*/
+                           if(data)
+                              ((bool (*)(void *, void *, const char *))(void *)dataType._vTbl[__ecereVMethodID_class_OnGetDataFromString])(dataType, data, newText);
+
+
+                           /*((void (*)(void *, void *))(void *)dataType._vTbl[__ecereVMethodID_class_OnFree])(dataType, dataHolder);
+                           if(dataType.type == structClass)
+                           {
+                              void * dataPtr = (void *)dataHolder;
+                              delete dataPtr;
+                           }
+                           dataHolder = 0;*/
+                        }
                      }
                   }
                }
-               row = list.AddRow();
-               row.tag = id;
-               SetListRowFields(r, row);
+               if(list)
+               {
+                  row = list.AddRow();
+                  row.tag = id;
+                  // have a better technique than Row::Next(); Row::Find(...); to make sure Row::GetData() will work right after a Row::SetData()?
+                  // it seems we're missing Row::Update()
+                  //r.Next();
+                  //r.tbl.db.Commit();
+                  //editRow.Synch(r);
+                  //r.Last();
+                  // next line is a patch for SQLite not returning data from GetData right after a SetData
+                  if(idField && r.Find(idField, middle, nil, id))
+                     SetListRowFields(r, row, false);
+               }
             }
             else if(idField && stringField)
             {
-               r.SetData(stringField, "[New]");
-               list.AddString("[New]").tag = id;
+               r.SetData(stringField, newText);
+               if(list)
+               {
+                  row = list.AddString(newText);
+                  row.tag = id;
+               }
             }
-            delete r;
+            //delete r;
+            delete newText;
          }
 
          if(list)
          {
             list.Sort(listSortField, listSortOrder);
-            list.currentRow.tag = id;
-            SelectListRow(list.currentRow);
+            if(row) SelectListRow(row);
          }
-         OnStateChanged(window);
+         OnStateChanged();
       }
    }
 
    void Remove()
    {
+      DebugLn("TableEditor::Remove");
       if(editRow.sysID) //list && list.currentRow)
       {
          if(OnRemovalRequest())
@@ -324,33 +668,31 @@ public:
             //NotifyDeleted(master, this);
             if(list)
                SelectListRow(list.currentRow);
-            OnStateChanged(window);
+            OnStateChanged();
          }
       }
    }
 
    void Load()
    {
+      DebugLn("TableEditor::Load");
       EditLoad();
    }
 
    void Write()
    {
+      DebugLn("TableEditor::Write");
       EditSave();
    }
 
-   void Test()
-   {
-      PrintLn("Test");
-   }
-
    bool ListSelect(DataRow row)
    {
       bool result = true;
+      DebugLn("TableEditor::ListSelect");
       if(/*-row && -*/row != lastRow)
       {
          uint id;
-         if(window && window.modifiedDocument)
+         if(modifiedDocument)
          {
             if(row)
                list.currentRow = lastRow;
@@ -376,9 +718,10 @@ public:
    bool Select(Id id)
    {
       bool result;
-      if(idField && editRow.Find(idField, middle, nil, id))
+      DebugLn("TableEditor::Select");
+      // 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)
+      if(idField && editRow && (editRow.sysID = id, !editRow.nil))// && editRow.Find(idField, middle, nil, id))
       {
-         //Id test = editRow.sysID;
          selectedId = editRow.sysID;
          EditLoad();
          result = true;
@@ -388,43 +731,107 @@ public:
       return result;
    }
 
-   bool SelectNext()
+   bool Filter(Id id)
    {
       bool result;
-      // How about confirmation / saving before changing the entry?
-      if(editRow.Next())
+      DebugLn("TableEditor::Filter");
+      if(selectedId && index && indexFilterField)
       {
-         //Id test = editRow.sysID;
-         selectedId = editRow.sysID;
-         EditLoad();
-         result = true;
+         for(filterRow.Find(indexFilterField, middle, nil, id); !filterRow.nil; filterRow.Next())
+         {
+            Id id2;
+            filterRow.GetData(idField, id2);
+            if(id2 == selectedId)
+            {
+               filtered = true;
+               result = true;
+               break;
+            }
+         }
       }
       else
-      {
          result = false;
-         // Wrap around after 2 Next if commented out (1st time could inform user of reaching the end)
-         // The first Next() bring the row to a null row (rowID = 0), a Next() on a rowID = 0 starts at first row
-         //editRow.Previous();
-      }
       return result;
    }
-   
-   bool SelectPrevious()
+
+   bool SelectNext(bool loopAround)
    {
-      bool result;
-      if(editRow.Previous())
+      bool result = NotifyClosing();
+      bool wasNil = !editRow.sysID;
+      DebugLn("TableEditor::SelectNext");
+      if(result)
       {
-         //Id test = editRow.sysID;
-         selectedId = editRow.sysID;
-         EditLoad();
-         result = true;
+         if(filtered)
+         {
+            if(!filterRow.Next() && loopAround)
+            {
+               //filterRow.First(); // Row::First doesn't behave properly in a filtered table
+               while(filterRow.Previous())
+                  ;
+               filterRow.Next();
+            }
+            if(!filterRow.nil)
+            {
+               if(wasNil && filterRow.sysID == selectedId)
+                  filterRow.Next();       // this whole wasNil thing makes no sense to me?
+               editRow.sysID = filterRow.sysID;
+            }
+            else
+               editRow.sysID = 0;
+         }
+         else
+         {
+            if(!editRow.Next() && loopAround)
+               editRow.Next();
+         }
+         if(!editRow.nil)
+         {
+            selectedId = editRow.sysID;
+            EditLoad();
+         }
+         else
+            result = false;
       }
-      else
+      return result;
+   }
+   
+   bool SelectPrevious(bool loopAround)
+   {
+      bool result = NotifyClosing();
+      bool wasNil = !editRow.sysID;
+      DebugLn("TableEditor::SelectPrevious");
+      if(result)
       {
-         result = false;
-         // Wrap around after 2 Prev if commented out (1st time could inform user of reaching the end)
-         // The first Prev() bring the row to a null row (rowID = 0), a Prev() on a rowID = 0 starts at last row
-         //editRow.Next();
+         if(filtered)
+         {
+            if(!filterRow.Previous() && loopAround)
+            {
+               //filterRow.Last(); // Row::Last doesn't behave properly in a filtered table
+               while(filterRow.Next())
+                  ;
+               filterRow.Previous();
+            }
+            if(!filterRow.nil)
+            {
+               if(wasNil && filterRow.sysID == selectedId)
+                  filterRow.Previous();       // this whole wasNil thing makes no sense to me?
+               editRow.sysID = filterRow.sysID;
+            }
+            else
+               editRow.sysID = 0;
+         }
+         else
+         {
+            if(!editRow.Previous() && loopAround)
+               editRow.Previous();
+         }
+         if(!editRow.nil)
+         {
+            selectedId = editRow.sysID;
+            EditLoad();
+         }
+         else
+            result = false;
       }
       return result;
    }
@@ -432,16 +839,17 @@ public:
    void SelectListRow(DataRow row)
    {
       // Time startTime = GetTime();
+      DebugLn("TableEditor::SelectListRow");
       if(row)
       {
-         selectedId = row.tag;
+         // 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)
+         selectedId = (Id)row.tag;
          lastRow = row;
 
          if(list.currentRow != row)
             list.currentRow = row;
          if(idField && editRow.Find(idField, middle, nil, selectedId))
          {
-            //Id test = editRow.sysID;
             listRow = row;
             //NotifySelectListRow(master, this, selectedId);
             EditLoad();
@@ -451,50 +859,630 @@ public:
    }
 
 private:
+   Table table;
+   Table index;
+   Field idField;
+   Field stringField;
+   Field indexFilterField;
+
+   ListBox list;
+   Array<StringSearchTable> searchTables;
+   Array<SQLiteSearchTable> sqliteSearchTables;
+   String searchString;
+
+   Map<Table, Lookup> lookups;
+
+   Array<LookupEditor> dynamicLookupEditors;
+   Array<FieldBox> fieldsBoxes { };
+   Array<TableEditor> tableEditors { };
+   Array<TableEditor> dynamicLookupTableEditors { };
+
+   bool readOnly;
+   bool internalModifications;
+   TableEditor masterEditor;
+
    Row editRow { };
    DataRow listRow;
    DataRow lastRow;
    Id selectedId;
+   Row filterRow { };
+   bool filtered;
+   Array<char> searchCI { };
+   WordEntry letters[26];
+   WordEntry doubleLetters[26][26];
+   bool initialized;
+   Array<ListField> listFields;
+   int listSortOrder;
+   DataField listSortField;
+   bool disabledFullListing;
+
+   ListEnumerationTimer listEnumerationTimer
+   {
+      userData = this, delay = 0.1f;
+      bool DelayExpired()
+      {
+         static Time startTime;
+         bool next = false;
+         int c;
+         Row row = listEnumerationTimer.row;
+         Array<Id> matches = listEnumerationTimer.matches;
+         Time delay = listEnumerationTimer.delay;
+         Time lastTime = GetTime();
+         static int slice = 128;
+
+         static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
+         ticks++;
+         if(ticks % 10 == 0)
+            PrintLn("listing... ");
+
+         if(matches)
+         {
+            int index = listEnumerationTimer.matchesIndex;
+            if(listFields && idField)
+            {
+               for(c=0; c<slice && (next = index++<matches.count); c++)
+               {
+                  if(row.Find(idField, middle, nil, matches[index]))
+                  {
+                     Id id = 0;
+                     DataRow dataRow = list.AddRow();
+                     row.GetData(idField, id);
+                     dataRow.tag = id;
+                     SetListRowFields(row, dataRow, true);
+                  }
+                  else
+                     DebugLn($"WordList match cannot be found in database.");
+               }
+               listEnumerationTimer.matchesIndex = index;
+               if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+            }
+            else if(idField && stringField)
+            {
+               for(c=0; c<slice && (next = index++<matches.count); c++)
+               {
+                  if(row.Find(idField, middle, nil, matches[index]))
+                  {
+                     Id id = 0;
+                     String s = null;
+                     row.GetData(idField, id);
+                     row.GetData(stringField, s);
+                     list.AddString(s).tag = id;
+                     delete s;
+                  }
+                  else
+                     DebugLn($"WordList match cannot be found in database.");
+               }
+               listEnumerationTimer.matchesIndex = index;
+               if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+            }
+         }
+         else if(listEnumerationTimer.sqliteSearch)
+         {
+            static SQLiteSearchTable st = null;
+            Row lookupRow { table };
+            Map<Id, int> uniques = listEnumerationTimer.uniques;
+            if(!row)
+            {
+               if(listEnumerationTimer.tablesIndex < sqliteSearchTables.count)
+               {
+                  char queryString[4096*4];
+
+                  if(!listEnumerationTimer.uniques)
+                     listEnumerationTimer.uniques = uniques = { };
+
+                  st = sqliteSearchTables[listEnumerationTimer.tablesIndex];
+                  if(st.table && st.idField && st.searchFields && st.searchFields.count)
+                  {
+                     wordListPrepRowNum = 0;
+                     wordListPrepRowCount = st.table.rowsCount;
+
+                     if(st.table && st.idField && st.searchFields && st.searchFields.count &&
+                           st.searchFields[0].field)
+                     {
+                        bool first = true;
+                        int bindId = 0;
+
+                        listEnumerationTimer.row = row = { st.table };
+
+                        {
+                           int len = searchString ? strlen(searchString) : 0;
+                           searchCI.size = len + 1;
+                           ConvertToASCII(searchString ? searchString : "", searchCI.array, &len, true);
+                           searchCI.count = len + 1;
+                        }
+
+                        sprintf(queryString, "SELECT ROWID, * FROM `%s`", st.table.name);
+                        strcat(queryString, " WHERE ");
+                        for(sf : st.searchFields)
+                        {
+                           if(sf.field)
+                           {
+                              if(!first)
+                                 strcat(queryString, " OR ");
+#define PERSON_SEARCH_LIKE
+                              // This code tries to implement the logic of PersonName::OnGetDataFromString because PersonName is inaccessible from here
+                              if(strstr(sf.field.type.name, "PersonName"))
+                              {
+#ifdef PERSON_SEARCH_LIKE
+                                 String ln = null, fn = null, mn = null;
+                                 char * comma = strchr(searchCI.array, ',');
+                                 if(comma)
+                                 {
+                                    int len = comma - searchCI.array;
+                                    ln = new char[len + 1];
+                                    memcpy(ln, searchCI.array, len);
+                                    ln[len] = 0;
+
+                                    fn = CopyString(comma+1);
+                                    {
+                                       int c;
+                                       for(c = strlen(fn)-2; c > 0; c--)
+                                       {
+                                          if(fn[c] == ' ')
+                                          {
+                                             mn = CopyString(fn + c + 1);
+                                             fn[c] = 0;
+                                          }
+                                       }
+                                    }
+                                 }
+                                 else
+                                    ln = CopyString(searchCI.array);
+                                 if(ln)
+                                 {
+                                    TrimLSpaces(ln, ln);
+                                    TrimRSpaces(ln, ln);
+                                 }
+                                 if(fn)
+                                 {
+                                    TrimLSpaces(fn, fn);
+                                    TrimRSpaces(fn, fn);
+                                 }
+                                 if(mn)
+                                 {
+                                    TrimLSpaces(mn, mn);
+                                    TrimRSpaces(mn, mn);
+                                 }
+
+                                 /* We could simply do this if we had access to PersonName: (don't forget the delete pn; below)
+                                 PersonName pn;
+                                 pn.OnGetDataFromString(searchCI.array);
+                                 */
+                                 if(ln && !fn && !mn)
+                                 {
+                                    // Only last name is pecified in search object, it is looked for in all fields
+                                    strcatf(queryString, "(PNLastName(`%s`) LIKE '%%%s%%' OR PNFirstName(`%s`) LIKE '%%%s%%' OR PNMiddleName(`%s`) LIKE '%%%s%%')",
+                                       sf.field.name, searchCI.array, sf.field.name, searchCI.array, sf.field.name, ln);
+                                 }
+                                 else if(ln || fn || mn)
+                                 {
+                                    // Otherwise search is in respective fields only (all specified must match)
+                                    bool first = true;
+                                    strcatf(queryString, "(");
+                                    if(ln)
+                                    {
+                                       if(!first) strcatf(queryString, " AND ");
+                                       first = false;
+                                       strcatf(queryString, "PNLastName(`%s`) LIKE '%%%s%%'", sf.field.name, ln);
+                                    }
+                                    if(fn)
+                                    {
+                                       if(!first) strcatf(queryString, " AND ");
+                                       first = false;
+                                       strcatf(queryString, "PNFirstName(`%s`) LIKE '%%%s%%'", sf.field.name, fn);
+                                    }
+                                    if(mn)
+                                    {
+                                       if(!first) strcatf(queryString, " AND ");
+                                       first = false;
+                                       strcatf(queryString, "PNMiddleName(`%s`) LIKE '%%%s%%'", sf.field.name, mn);
+                                    }
+                                    strcatf(queryString, ")");
+                                 }
+                                 //delete pn;
+                                 delete ln; delete fn; delete mn;
+#else
+                                 // To use CompPersonName::OnCompare:
+                                 strcatf(queryString, "`%s` = ?", sf.field.name);
+#endif
+                              }
+                              else
+                                 strcatf(queryString, "`%s` LIKE '%%%s%%'", sf.field.name, searchString);
+                              first = false;
+                           }
+                        }
+                        PrintLn(queryString);
+                        startTime = GetTime();
+                        row.query = queryString;
+#ifndef PERSON_SEARCH_LIKE
+                        // To use CompPersonName::OnCompare:
+                        for(sf : st.searchFields)
+                        {
+                           if(sf.field)
+                           {
+                              if(strstr(sf.field.type.name, "PersonName"))
+                              {
+                                 void * pn;
+                                 ((bool (*)(void *, void *, const char *))(void *)sf.field.type._vTbl[__ecereVMethodID_class_OnGetDataFromString])(sf.field.type, &pn, searchCI.array);
+                                 row.SetQueryParamObject(++bindId, pn, sf.field.type);
+                                 bindId++;
+                                 delete pn;
+                              }
+                              first = false;
+                           }
+                        }
+#endif
+                        if(bindId)
+                           row.Next();
+                     }
+                  }
+               }
+            }
+            if(row)
+            {
+               if(listFields && idField)
+               {
+                  // should we not start with a Next() ??? :S  -- right now, query = does not need a Next(), unless it had parameters (SetQueryParam), See #591
+                  // when going through all the rows in a table you always start with Next() no?
+                  // is this different for query results?
+                  for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
+                  {
+                     Id id = 0;
+                     row.GetData(st.idField, id);
+                     //if(uniques[id]++ == 0)
+                     if(uniques[id] == 0)
+                     {
+                        DataRow dataRow = list.AddRow();
+                        dataRow.tag = id;
+                        if(st.table == table)
+                           SetListRowFields(row, dataRow, true);
+                        else if(lookupRow.Find(idField, middle, nil, id))
+                           SetListRowFields(lookupRow, dataRow, true);
+                        else
+                           PrintLn("no");
+                     }
+                     uniques[id] = uniques[id] + 1;
+                  }
+                  if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+                  else
+                  {
+                     delete listEnumerationTimer.row; row = null;
+                     next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
+                  }
+               }
+               else if(idField && stringField)
+               {
+                  // should we not start with a Next() ??? :S
+                  // when going through all the rows in a table you always start with Next() no?
+                  // is this different for query results?
+                  for(c = 0, next = !row.nil; c<slice && next; c++, next = row.Next())
+                  {
+                     Id id = 0;
+                     row.GetData(st.idField, id);
+                     //if(uniques[id]++ == 0)
+                     if(uniques[id] == 0)
+                     {
+                        String s = null;
+                        if(st.table == table)
+                           row.GetData(stringField, s);
+                        else if(lookupRow.Find(idField, middle, nil, id))
+                           lookupRow.GetData(stringField, s);
+                        else
+                           PrintLn("no");
+                        list.AddString(s).tag = id;
+                        delete s;
+                     }
+                     uniques[id] = uniques[id] + 1;
+                  }
+                  if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+                  else
+                  {
+                     delete listEnumerationTimer.row; row = null;
+                     next = ++listEnumerationTimer.tablesIndex < sqliteSearchTables.count;
+                  }
+               }
+            }
+            delete lookupRow;
+         }
+         else if(!disabledFullListing)
+         {
+            if(listFields && idField)
+            {
+               for(c = 0; c<slice && (next = row.Next()); c++)
+               {
+                  Id id = 0;
+                  DataRow dataRow = list.AddRow();
+                  row.GetData(idField, id);
+                  dataRow.tag = id;
+                  SetListRowFields(row, dataRow, true);
+                  //app.UpdateDisplay();
+               }
+               if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+            }
+            else if(idField && stringField)
+            {
+               for(c = 0; c<slice && (next = row.Next()); c++)
+               {
+                  Id id = 0;
+                  String s = null;
+                  row.GetData(idField, id);
+                  row.GetData(stringField, s);
+                  list.AddString(s).tag = id;
+                  delete s;
+               }
+               if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+            }
+         }
+
+         list.Sort(listSortField, listSortOrder);
+
+         if(!next)
+         {
+            listEnumerationTimer.hasCompleted = true;
+            StopListEnumerationTimer();
+         }
+         if(startTime) PrintLn("*** Search took ", (uint)((GetTime() - startTime) * 1000), "ms ***");
+         return true;
+      }
+   };
+
+   void StopListEnumerationTimer()
+   {
+      listEnumerationTimer.Stop();
+      listEnumerationTimer.matchesIndex = 0;
+      listEnumerationTimer.tablesIndex = 0;
+      delete listEnumerationTimer.row;
+      delete listEnumerationTimer.matches;
+      delete listEnumerationTimer.uniques;
+   }
+
+   WordListPrepTimer wordListPrepTimer
+   {
+      userData = this, delay = 0.1f;
+      bool DelayExpired()
+      {
+         bool next = false;
+         Row row = wordListPrepTimer.row;
+         static int slice = 512;
+         static StringSearchTable st = null;
+
+         static int wordListPrepRowCount = 0, wordListPrepRowNum = 0, ticks = 0;
+
+         if(!row)
+         {
+            if(wordListPrepTimer.tablesIndex < searchTables.count)
+            {
+               st = searchTables[wordListPrepTimer.tablesIndex];
+               if(st.table && st.idField && st.searchFields && st.searchFields.count)
+               {
+                  wordListPrepRowNum = 0;
+                  wordListPrepRowCount = st.table.rowsCount;
+
+                  wordListPrepTimer.row = row = { st.table };
+                  DebugLn("building word list for ", st.table.name, " table ------------------------------------- ");
+               }
+            }
+         }
+         if(row)
+         {
+            int c;
+            Time delay = wordListPrepTimer.delay;
+            Time lastTime = GetTime();
+
+            ticks++;
+            if(ticks % 10 == 0)
+               PrintLn("indexing ", wordListPrepRowNum, " of ", wordListPrepRowCount, " --- slice is ", slice);
+
+            for(c = 0; c<slice && (next = row.Next()); c++)
+            {
+               Id id = 0;
+               row.GetData(st.idField, id);
+
+               wordListPrepRowNum++;
+
+               for(sf : st.searchFields)
+               {
+                  Field field = sf.field;
+                  StringSearchIndexingMethod method = sf.method;
+                  if(field && field.type == class(String))
+                  {
+                     String string = null;
+                     row.GetData(field, string);
+
+                     if(string && string[0])
+                        ProcessWordListString(string, method, id);
+                     delete string;
+                  }
+                  // todo: need to improve on this...
+                  // else ... call OnGetString of type ... etc...
+                     //PrintLn("todo: support other field types for string search");
+                  else if(field && field.type)
+                  {
+                     char * n = field.name;
+                     char tempString[MAX_F_STRING];
+                     int64 data = 0;
+                     Class type = field.type;
+                     String s;
+                     if(type.type == unitClass && !type.typeSize)
+                     {
+                        Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
+                        if(dataType)
+                           type = dataType;
+                     }
+                     if(type.type == structClass)
+                        data = (int64)new0 byte[type.structSize];
+                     ((bool (*)())(void *)row.GetData)(row, field, type, (type.type == structClass) ? (void *)data : &data);
+
+                     if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
+                        s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)field.type._vTbl[__ecereVMethodID_class_OnGetString])(field.type, &data, tempString, null, null);
+                     else
+                        s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)field.type._vTbl[__ecereVMethodID_class_OnGetString])(field.type, (void *)data, tempString, null, null);
+
+                     if(s && s[0])
+                        ProcessWordListString(s, method, id);
+
+                     if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
+                        ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
+                     if(type.type == structClass)
+                        delete data;
+                  }
+               }
+            }
+            if(next) slice = Max(32, (int)(slice * (delay / (GetTime() - lastTime))));
+            else
+            {
+               delete wordListPrepTimer.row; row = null;
+               next = ++wordListPrepTimer.tablesIndex < searchTables.count;
+            }
+         }
+
+         if(!next)
+         {
+            char filePath[MAX_FILENAME];
+            File f;
+
+            sprintf(filePath, "%s.search", table.name);
+            // this doesn't want to work? :S :S :S
+            // f == 0x0
+            f = FileOpenBuffered(filePath, read);
+            if(f)
+            {
+               f.Put(wordTree);
+               delete f;
+            }
+
+            wordListPrepTimer.hasCompleted = true;
+            StopWordListPrepTimer();
+         }
+         return true;
+      }
+   };
+
+   void StopWordListPrepTimer()
+   {
+      wordListPrepTimer.Stop();
+      wordListPrepTimer.tablesIndex = 0;
+      delete wordListPrepTimer.row;
+   }
 
    ~TableEditor()
    {
-      fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the increb in AddFieldBox?
+      DebugLn("TableEditor::~()");
+
+      fieldsBoxes.Free(); // TOCHECK: do I need to delete each to oppose the incref in AddFieldBox? -- Free() does just that
       delete searchString;
+      wordTree.Free();
+
+      delete listFields;
+      delete lookups;
+      delete dynamicLookupEditors;
+      delete dynamicLookupTableEditors;
+      if(searchTables) searchTables.Free();
+      delete searchTables;
+      if(sqliteSearchTables) sqliteSearchTables.Free();
+      delete sqliteSearchTables;
    }
 
    void ResetListFields()
    {
+      DebugLn("TableEditor::ResetListFields");
       if(list && listFields && listFields.count)
       {
+         bool c = list.created;
          list.ClearFields();
          for(lf : listFields)
+         {
             list.AddField(lf.dataField);
+            incref lf.dataField;
+         }
+      }
+   }
+
+   void AddTableEditor(TableEditor tableEditor)
+   {
+      DebugLn("TableEditor::AddTableEditor");
+      if(!tableEditors.Find(tableEditor))
+      {
+         tableEditors.Add(tableEditor);
+         incref tableEditor;
+      }
+      else
+         DebugLn("   TableEditor instance already added");
+   }
+
+   void RemoveTableEditor(TableEditor tableEditor)
+   {
+      Iterator<TableEditor> it { tableEditors };
+      DebugLn("TableEditor::RemoveTableEditor");
+      if(it.Find(tableEditor))
+      {
+         it.Remove(); // tableEditors.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
+         delete tableEditor; // AddTableEditor increfs...
       }
+      else
+         DebugLn("   TableEditor instance not found, no need to remove");
    }
 
    void AddFieldBox(FieldBox fieldBox)
    {
-      fieldsBoxes.Add(fieldBox);
-      if(table)
-         fieldBox.Init();
-      incref fieldBox;
+      // I was putting this here to force autosize on the lists (e.g. the Radiologists fields):
+      /*
+      if(!fieldBox.autoSize)
+         fieldBox.autoSize = true;
+      */
+      DebugLn("TableEditor::AddFieldBox");
+      if(!fieldsBoxes.Find(fieldBox))
+      {
+         fieldsBoxes.Add(fieldBox);
+         if(table)
+            fieldBox.Init();
+         incref fieldBox;
+      }
+      else
+         DebugLn("   FieldBox instance already added");
+   }
+
+   void RemoveFieldBox(FieldBox fieldBox)
+   {
+      Iterator<FieldBox> it { fieldsBoxes };
+      DebugLn("TableEditor::RemoveFieldBox");
+      if(it.Find(fieldBox))
+      {
+         it.Remove(); // fieldsBoxes.Remove(it.pointer); // <-- any reason why we would want to do that instead? -- It's the same.
+      }
+      else
+         DebugLn("   FieldBox instance not found, no need to remove");
    }
 
    void InitFieldsBoxes()
    {
-      for(fb : fieldsBoxes)
-         fb.Init();
+      DebugLn("TableEditor::InitFieldsBoxes");
+      if(readOnly)
+      {
+         for(fb : fieldsBoxes)
+         {
+            fb.readOnly = true;
+            fb.Init();
+         }
+      }
+      else
+      {
+         for(fb : fieldsBoxes)
+            fb.Init();
+      }
       //NotifyInitFields(master, this);
    }
 
    void EditNew()
    {
-      if(window)
-         window.modifiedDocument = false;
+      DebugLn("TableEditor::EditNew");
+
+      modifiedDocument = false;
    }
 
    void EditSave()
    {
+      DebugLn("TableEditor::EditSave");
       internalModifications = true;
       for(fb : fieldsBoxes)
          fb.Save();
@@ -504,63 +1492,212 @@ private:
          DataRow listRow = list.currentRow;
          // ADDED THIS HERE FOR SQLITE TO REFRESH
          editRow.Find(idField, middle, nil, listRow.tag);
-         SetListRowFields(editRow, listRow);
+         SetListRowFields(editRow, listRow, false);
          list.Sort(listSortField, listSortOrder);
       }
       internalModifications = false;
-      if(window)
-         window.modifiedDocument = false;
-      OnStateChanged(window);
+
+      for(te : tableEditors)
+         te.EditSave();
+
+      modifiedDocument = false;
+      OnStateChanged();
    }
 
    void EditLoad()
    {
+      Id selId = selectedId;
+      DebugLn("TableEditor::EditLoad");
+      EditClear();
+      selectedId = selId;
+      OnLoad();
       internalModifications = true;
+      for(lu : lookups)
+      {
+         if(&lu == table)
+         {
+            if(!lu.row)
+               lu.row = { lu.findIndex ? lu.findIndex : lu.findField.table };
+            if(lu.valueField && eClass_IsDerived(lu.valueField.type, class(Id)) &&
+                  lu.findField && eClass_IsDerived(lu.findField.type, class(Id)))
+            {
+               Id id = 0;
+               editRow.GetData(lu.valueField, id);
+               lu.row.Find(lu.findField, middle, nil, id);
+            }
+         }
+      }
       for(fb : fieldsBoxes)
          fb.Load();
+      OnCreateDynamicLookupEditors();
       internalModifications = false;
-      if(window)
-         window.modifiedDocument = false;
-      OnStateChanged(window);
+      size = size;   // This seems to be required to fix autoSize on entering order screen
+
+      DebugLn("   TODO: implement virtual method TableEditor::OnSubEditorsLoad");
+
+      modifiedDocument = false;
+      OnStateChanged();
    }
 
    void EditClear()
    {
+      DebugLn("TableEditor::EditClear");
+      selectedId = 0;
       internalModifications = true;
       for(fb : fieldsBoxes)
          fb.Clear();
-      if(window)
-         window.modifiedDocument = false;
+      for(e : dynamicLookupTableEditors)
+         e.Destroy(0);
+      for(e : tableEditors)
+         e.Destroy(0);
+      tableEditors.Free();
+      dynamicLookupTableEditors.Free();
+      //dynamicLookupTableEditors.size = 0;
       internalModifications = false;
-      OnStateChanged(window);
+
+      DebugLn("   TODO: remove all sub table editors");
+
+      modifiedDocument = false;
+      OnStateChanged();
    }
 
-   void SetListRowFields(Row dbRow, DataRow listRow)
+   void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection)
    {
+//      DebugLn("TableEditor::SetListRowFields");
       for(lf : listFields)
       {
          if(lf.dataField && lf.field)
          {
-            if(lf.field.type == class(String))
+            if(eClass_IsDerived(lf.field.type, class(char*)))
             {
                String s = null;
                dbRow.GetData(lf.field, s);
                listRow.SetData(lf.dataField, s);
-               delete s;
+               delete s;
+            }
+            else if(eClass_IsDerived(lf.field.type, class(Id)))
+            {
+               if(lf.CustomLookup)
+               {
+                  Id id = 0;
+                  String s = null;
+                  dbRow.GetData(lf.field, id);
+                  s = lf.CustomLookup(id);
+                  listRow.SetData(lf.dataField, s);
+                  delete s; // ?
+               }
+               else if(lf.lookupFindField && (lf.lookupFindIndex || lf.lookupFindField.table) && lf.lookupValueField &&
+                     eClass_IsDerived(lf.lookupFindField.type, class(Id)) &&
+                     eClass_IsDerived(lf.lookupValueField.type, class(char*)))
+               {
+                  Id id = 0;
+                  String s = null;
+                  Row lookupRow { lf.lookupFindIndex ? lf.lookupFindIndex : lf.lookupFindField.table };
+                  dbRow.GetData(lf.field, id);
+                  if(lookupRow.Find(lf.lookupFindField, middle, nil, id))
+                     lookupRow.GetData(lf.lookupValueField, s);
+                  listRow.SetData(lf.dataField, s);
+                  delete s;
+                  delete lookupRow;
+               }
+            }
+            else if(lf.CustomLookup && lf.field.type)
+            {
+               char * n = lf.field.name;
+               int64 data = 0;
+               String s = null;
+               Class type = lf.field.type;
+               if(type.type == unitClass && !type.typeSize)
+               {
+                  Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
+                  if(dataType)
+                     type = dataType;
+               }
+               if(type.type == structClass)
+                  data = (int64)new0 byte[type.structSize];
+               ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
+               s = lf.CustomLookup((int)data);
+               listRow.SetData(lf.dataField, s);
+               if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
+                  ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
+               if(type.type == structClass)
+                  delete data;
+               delete s; // ?
+            }
+            else if(lf.field.type && eClass_IsDerived(lf.dataField.dataType, class(char*)))
+            {
+               char * n = lf.field.name;
+               char tempString[MAX_F_STRING];
+               int64 data = 0;
+               Class type = lf.field.type;
+               String s;
+               if(type.type == unitClass && !type.typeSize)
+               {
+                  Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
+                  if(dataType)
+                     type = dataType;
+               }
+               if(type.type == structClass)
+                  data = (int64)new0 byte[type.structSize];
+               ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
+               if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
+                  s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)lf.field.type._vTbl[__ecereVMethodID_class_OnGetString])(lf.field.type, &data, tempString, null, null);
+               else
+                  s = ((char *(*)(void *, void *, char *, void *, bool *))(void *)lf.field.type._vTbl[__ecereVMethodID_class_OnGetString])(lf.field.type, (void*)data, tempString, null, null);
+
+               listRow.SetData(lf.dataField, s);
+
+               if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
+                  ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
+               if(type.type == structClass)
+                  delete data;
+            }
+            else if(lf.field.type)
+            {
+               char * n = lf.field.name;
+               //char tempString[256];
+               int64 data = 0;
+               Class type = lf.field.type;
+               if(type.type == unitClass && !type.typeSize)
+               {
+                  Class dataType = eSystem_FindClass(type.module, type.dataTypeString);
+                  if(dataType)
+                     type = dataType;
+               }
+               if(type.type == structClass)
+                  data = (int64)new0 byte[type.structSize];
+               ((bool (*)())(void *)dbRow.GetData)(dbRow, lf.field, type, (type.type == structClass) ? (void *)data : &data);
+               if(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass)
+                  listRow.SetData(lf.dataField, (void *)&data);
+               else
+                  listRow.SetData(lf.dataField, (void *)data);
+               if(!(type.type == systemClass || type.type == unitClass || type.type == bitClass || type.type == enumClass))
+                  ((void (*)(void *, void *))(void *)type._vTbl[__ecereVMethodID_class_OnFree])(type, (void *)data);
+               if(type.type == structClass)
+                  delete data;
             }
          }
       }
+      if(restoreSelection && !list.currentRow)
+      {
+         DataRow select;
+         if((select = list.FindRow(selectedId)))
+            SelectListRow(select);
+      }
    }
 
    Array<Id> SearchWordList()
    {
+      DebugLn("TableEditor::SearchWordList");
+#ifdef FULL_STRING_SEARCH
+   {
       int c;
       int numTokens = 0;
       int len[256];
       char * words[256];
       WordEntry entries[256];
       Array<Id> results = null;
-      if(searchFields && searchFields.count && searchString && searchString[0])
+      if(searchTables && searchTables.count && searchString && searchString[0])
       {
          char * searchCopy = CopyString(searchString);
          numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false);
@@ -589,8 +1726,10 @@ private:
                   for(i = 0; i<entries[c].items.count; i++)
                   {
                      int count = uniques[entries[c].items.ids[i]];
+#ifdef _DEBUG
                      if(count != 0)
-                        PrintLn("Problem");
+                        DebugLn("Problem");
+#endif
                      matches[entries[c].items.ids[i]]++;
                   }
                   uniques.Free();
@@ -617,11 +1756,26 @@ private:
       }
       return results;
    }
+#else
+      return null;
+#endif
+
+   }
 
-   void PrepareWordList(char * filePath)
+   // find a way to not load a tree for different searchFields
+   // if the code that sets the searchFields has changed
+   // store a search index signature containing following:
+   // tables name, idField name and type, fields name and type
+   void PrepareWordList()
    {
-      Row r { table };
-      File f = filePath ? FileOpenBuffered(filePath, read) : null;
+      DebugLn("TableEditor::PrepareWordList");
+#ifdef FULL_STRING_SEARCH
+   {
+      char filePath[MAX_FILENAME];
+      File f;
+
+      sprintf(filePath, "%s.search", table.name);
+      f = filePath ? FileOpenBuffered(filePath, read) : null;
       if(f)
       {
          int a;
@@ -643,7 +1797,7 @@ private:
             }
          }
       }
-      else if(r && idField && searchFields && searchFields.count)
+      else if(searchTables && searchTables.count)
       {
          if(!letters[0])
          {
@@ -664,85 +1818,65 @@ private:
             }
          }
 
-         while(r.Next())
-         {
-            Id id = 0;
-            r.GetData(idField, id);
-
-            for(field : searchFields)
-            {
-               if(field && field.type == class(String))
-               {
-                  int c;
-                  unichar ch;
-                  unichar lastCh = 0;
-                  int count = 0;
-                  int numChars = 0;
-                  int nb;
-                  char word[1024];
-                  char asciiWord[1024];
-                  
-                  String string = null;
-                  r.GetData(field, string);
-                  
-                  if(string && string[0])
-                  {
-                     for(c = 0; ; c += nb)
-                     {
-                        ch = UTF8GetChar(string + c, &nb);
-
-                        if(!ch || CharMatchCategories(ch, separators) || 
-                           (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
-                        {
-                           if(count)
-                           {
-                              word[count] = 0;
-                              asciiWord[numChars] = 0;
-                              strlwr(word);
-                              strlwr(asciiWord);
-
-                              AddWord(word, count, id);
-                              if(count > numChars)
-                                 AddWord(asciiWord, strlen(asciiWord), id);
-                              count = 0;
-                              numChars = 0;
-                           }
-                           if(!CharMatchCategories(ch, separators))
-                           {
-                              int cc;
-                              for(cc = 0; cc < nb; cc++)
-                                 word[count++] = string[c + cc];
+         wordListPrepTimer.tablesIndex = 0;
+         wordListPrepTimer.Start();
+      }
+   }
+#endif
+   }
 
-                              asciiWord[numChars++] = ToASCII(ch);
-                           }
-                           if(!ch)
-                              break;
-                        }
-                        else
-                        {
-                           int cc;
-                           for(cc = 0; cc < nb; cc++)
-                              word[count++] = string[c + cc];
+   void ProcessWordListString(char * string, StringSearchIndexingMethod method, Id id)
+   {
+      int c;
+      unichar ch;
+      unichar lastCh = 0;
+      int count = 0;
+      int numChars = 0;
+      int nb;
+      char word[1024];
+      char asciiWord[1024];
+
+      for(c = 0; ; c += nb)
+      {
+         ch = UTF8GetChar(string + c, &nb);
 
-                           asciiWord[numChars++] = ToASCII(ch);
-                        }
-                        lastCh = ch;
-                     }
-                  }
+         if(!ch || CharMatchCategories(ch, separators) ||
+            (count && CharMatchCategories(ch, letters|numbers|marks|connector) != CharMatchCategories(lastCh, letters|numbers|marks|connector)))
+         {
+            if(count)
+            {
+               word[count] = 0;
+               asciiWord[numChars] = 0;
+               strlwr(word);
+               strlwr(asciiWord);
+
+               AddWord(word, count, method == allSubstrings, id);
+               if(count > numChars)
+                  AddWord(asciiWord, strlen(asciiWord), method == allSubstrings, id);
+               count = 0;
+               numChars = 0;
+            }
+            if(!CharMatchCategories(ch, separators))
+            {
+               int cc;
+               for(cc = 0; cc < nb; cc++)
+                  word[count++] = string[c + cc];
 
-                  delete string;
-               }
+               asciiWord[numChars++] = ToASCII(ch);
             }
+            if(!ch)
+               break;
          }
-
-         f = filePath ? FileOpen(filePath, write) : null;
-         if(f)
+         else
          {
-            f.Put(wordTree);
-            delete f;
+            int cc;
+            for(cc = 0; cc < nb; cc++)
+               word[count++] = string[c + cc];
+
+            asciiWord[numChars++] = ToASCII(ch);
          }
+         lastCh = ch;
       }
-      delete r;
    }
 
    /*static */WordEntryBinaryTree wordTree
@@ -751,101 +1885,113 @@ private:
       FreeKey = BinaryTree::FreeString;
    };
 
-   WordEntry letters[26];
-   WordEntry doubleLetters[26][26];
-
-   void AddWord(char * word, int count, Id id)
+   void AddWord(char * word, int count, bool addAllSubstrings, Id id)
    {
-      int s;
-      WordEntry mainEntry = null;
-      WordEntry sEntry = null;
-
-      for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
+      //DebugLn("TableEditor::AddWord");
+#ifdef FULL_STRING_SEARCH
+   {
+      if(addAllSubstrings)
       {
-         int l;
-         char subWord[1024];
-         char ch1;
-         WordEntry lEntry = null;
-         memcpy(subWord, word + s, count-s);
-         subWord[count-s] = 0;   // THIS IS REQUIRED!! THE WHILE LOOP BELOW CHECKED count-s FIRST!!
-         ch1 = subWord[0];
-                              
-         for(l = count-s; l>0; l--)
+         int s;
+         WordEntry mainEntry = null;
+         WordEntry sEntry = null;
+
+         for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s]))
          {
-            uint wid;
-            WordEntry start = null, wordEntry;
+            int l;
+            char subWord[1024];
+            char ch1;
+            WordEntry lEntry = null;
+            memcpy(subWord, word + s, count-s);
+            subWord[count-s] = 0;   // THIS IS REQUIRED!! THE WHILE LOOP BELOW CHECKED count-s FIRST!!
+            ch1 = subWord[0];
+
+            for(l = count-s; l>0; l--)
+            {
+               uint wid;
+               WordEntry start = null, wordEntry;
 
-            while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
-            if(!l) break;
+               while(l > 0 && !UTF8_IS_FIRST(subWord[l])) l--;
+               if(!l) break;
 
-            subWord[l] = 0;
+               subWord[l] = 0;
 
-            if(ch1 >= 'a' && ch1 <= 'z')
-            {
-               char ch2 = subWord[1];
-               if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
+               if(ch1 >= 'a' && ch1 <= 'z')
                {
                   char ch2 = subWord[1];
-                  start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
+                  if(count - s > 1 && ch2 >= 'a' && ch2 <= 'z')
+                  {
+                     char ch2 = subWord[1];
+                     start = doubleLetters[ch1 - 'a'][ch2 - 'a'];
+                  }
+                  else
+                  {
+                     start = letters[ch1 - 'a'];
+                  }
+               }
+
+               if(start)
+               {
+                  WordEntry max;
+                  while(start && (max = (WordEntry)((BTNode)start).maximum))
+                  {
+                     if(strcmp(max.string, subWord) >= 0)
+                        break;
+                     start = start.parent;
+                  }
+               }
+
+               if(!start)
+                  start = (WordEntry)wordTree.root;
+
+               if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
+               {
+
                }
                else
                {
-                  start = letters[ch1 - 'a'];
+                  wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
                }
-            }
-            
-            if(start)
-            {
-               WordEntry max;
-               while(start && (max = (WordEntry)((BTNode)start).maximum))
+               if(!mainEntry)
                {
-                  if(strcmp(max.string, subWord) >= 0)
-                     break;
-                  start = start.parent;
+                  mainEntry = wordEntry;
+                  sEntry = wordEntry;
+                  lEntry = wordEntry;
                }
+               else if(!sEntry)
+               {
+                  sEntry = wordEntry;
+                  lEntry = wordEntry;
+                  if(!wordEntry.words) wordEntry.words = IdList { };
+                  wordEntry.words.Add((Id)mainEntry);
+               }
+               else if(!lEntry)
+               {
+                  lEntry = wordEntry;
+                  if(!wordEntry.words) wordEntry.words = IdList { };
+                  wordEntry.words.Add((Id)sEntry);
+               }
+               else
+               {
+                  if(!wordEntry.words) wordEntry.words = IdList { };
+                  wordEntry.words.Add((Id)lEntry);
+               }
+               if(!wordEntry.items) wordEntry.items = IdList { };
+               wordEntry.items.Add(id);
             }
-            
-            if(!start)
-               start = (WordEntry)wordTree.root;
-
-            if((wordEntry = (WordEntry)((BTNode)start).FindString(subWord)))
-            {
-
-            }
-            else
-            {
-               wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(subWord) }));
-            }
-            if(!mainEntry)
-            {
-               mainEntry = wordEntry;
-               sEntry = wordEntry;
-               lEntry = wordEntry;
-            }
-            else if(!sEntry)
-            {
-               sEntry = wordEntry;
-               lEntry = wordEntry;
-               if(!wordEntry.words) wordEntry.words = IdList { };
-               wordEntry.words.Add((Id)mainEntry);
-            }
-            else if(!lEntry)
-            {
-               lEntry = wordEntry;
-               if(!wordEntry.words) wordEntry.words = IdList { };
-               wordEntry.words.Add((Id)sEntry);
-            }
-            else
-            {
-               if(!wordEntry.words) wordEntry.words = IdList { };
-               wordEntry.words.Add((Id)lEntry);
-            }
-            if(!wordEntry.items) wordEntry.items = IdList { };
-            wordEntry.items.Add(id);
-         }                        
+         }
+      }
+      else
+      {
+         WordEntry wordEntry;
+         if(!(wordEntry = (WordEntry)(wordTree.root).FindString(word)))
+            wordTree.Add((BTNode)(wordEntry = WordEntry { string = CopyString(word) }));
+         if(!wordEntry.items) wordEntry.items = IdList { };
+         wordEntry.items.Add(id);
       }
    }
-
+#endif
+   }
 }
 
 public class ListField : struct
@@ -853,6 +1999,93 @@ public class ListField : struct
 public:
    Field field;
    DataField dataField;
+   Field lookupFindField;
+   Field lookupValueField;
+   Table lookupFindIndex;
+   String (*CustomLookup)(Id);
+private:
+
+   ~ListField()
+   {
+      delete dataField;
+   }
+}
+
+public class Lookup
+{
+public:
+   Field valueField;
+   Field findField;
+   Table findIndex;
+
+private:
+   Row row;
+
+   ~Lookup()
+   {
+      delete row;
+   }
+}
+
+public class LookupEditor : struct
+{
+public:
+   subclass(TableEditor) editorClass;
+   Window parentWindow;
+   Field lookupValueField;
+   Field lookupFindField;
+   Field lookupIdField;
+   Table lookupFindIndex;
+}
+
+// all methods currently perform ascii conversion and all that jazz on every string added to the index
+public enum StringSearchIndexingMethod { fullString, allSubstrings };
+
+public class StringSearchField
+{
+public:
+   Field field;
+   StringSearchIndexingMethod method;
+
+   Field lookupFindField;
+   Field lookupValueField;
+
+   //String (*CustomRead)(Id);
+};
+
+public class StringSearchTable
+{
+public:
+   Table table;
+   Field idField;
+   Array<StringSearchField> searchFields;
+
+private:
+   ~StringSearchTable()
+   {
+      delete searchFields;
+   }
+}
+
+public class SQLiteSearchField
+{
+public:
+   Field field;
+   //StringSearchIndexingMethod method;
+};
+
+public class SQLiteSearchTable
+{
+public:
+   Table table;
+   Field idField;
+   Array<SQLiteSearchField> searchFields;
+
+private:
+   ~SQLiteSearchTable()
+   {
+      delete searchFields;
+   }
 }
 
 static WordEntry * btnodes;
@@ -866,6 +2099,7 @@ struct WordEntryBinaryTree : BinaryTree
       WordEntry node;
       uint id;
       uint count = this.count;
+      DebugLn("WordEntryBinaryTree::OnSerialize");
       for(id = 1, node = (WordEntry)root; node;)
       {
          node.id = id++;
@@ -903,6 +2137,7 @@ struct WordEntryBinaryTree : BinaryTree
    {
       WordEntry root, node;
       uint count;
+      DebugLn("WordEntryBinaryTree::OnUnserialize");
       channel.Unserialize(count);
       entries = new WordEntry[count];      
       btnodes = entries;
@@ -947,45 +2182,6 @@ struct WordEntryBinaryTree : BinaryTree
    }
 };
 
-static char ToASCII(unichar ch)
-{
-   char asciiCH = 0;
-   if(ch > 127)
-   {
-      if(ch == 'À' || ch == 'Á' || ch == 'Â' || ch == 'Ã' || ch == 'Ä' || ch == 'Å')
-         asciiCH = 'A';
-      else if(ch == 'Ç')
-         asciiCH = 'C';
-      else if(ch == 'È' || ch == 'É' || ch == 'Ê' || ch == 'Ë')
-         asciiCH = 'E';
-      else if(ch == 'Ì' || ch == 'Í' || ch == 'Î' || ch == 'Ï')
-         asciiCH = 'I';
-      else if(ch == 'Ñ')
-         asciiCH = 'N';
-      else if(ch == 'Ò' || ch == 'Ó' || ch == 'Ô' || ch == 'Õ' || ch == 'Ö')
-         asciiCH = 'O';
-      else if(ch == 'Ù' || ch == 'Ú' || ch == 'Û' || ch == 'Ü')
-         asciiCH = 'U';
-      else if(ch == 'à' || ch == 'á' || ch == 'â' || ch == 'ã' || ch == 'ä' || ch == 'å')
-         asciiCH = 'a';
-      else if(ch == 'ç')
-         asciiCH = 'c';
-      else if(ch == 'è' || ch == 'é' || ch == 'ê' || ch == 'ë')
-         asciiCH = 'e';
-      else if(ch == 'ì' || ch == 'í' || ch == 'î' || ch == 'ï')
-         asciiCH = 'i';
-      else if(ch == 'ñ')
-         asciiCH = 'n';
-      else if(ch == 'ò' || ch == 'ó' || ch == 'ô' || ch == 'õ' || ch == 'ö')
-         asciiCH = 'o';
-      else if(ch == 'ù' || ch == 'ú' || ch == 'û' || ch == 'ü')
-         asciiCH = 'u';
-   }
-   else
-      asciiCH = (char)ch;
-   return asciiCH;
-}
-
 class WordEntry : struct
 {
    String string;
@@ -1005,6 +2201,7 @@ class WordEntry : struct
 
    void OnSerialize(IOChannel channel)
    {
+#ifdef FULL_STRING_SEARCH
       if(this)
       {
          channel.Serialize(id);
@@ -1036,10 +2233,12 @@ class WordEntry : struct
          uint nothing = 0;
          channel.Serialize(nothing);
       }
+#endif
    }
 
    void OnUnserialize(IOChannel channel)
    {
+#ifdef FULL_STRING_SEARCH
       uint id;
       channel.Unserialize(id);
       if(id)
@@ -1064,5 +2263,188 @@ class WordEntry : struct
       }
       else
          this = null;
+#endif
+   }
+}
+
+
+class ListEnumerationTimer : Timer
+{
+   bool hasCompleted;
+   int matchesIndex;
+   bool sqliteSearch;
+   int tablesIndex;
+   Array<Id> matches;
+   Row row;
+   Map<Id, int> uniques;
+}
+
+class WordListPrepTimer : Timer
+{
+   bool hasCompleted;
+   int tablesIndex;
+   Row row;
+}
+
+#if 0
+class EnumerateThread : Thread
+{
+public:
+   bool active;
+   TableEditor editor;
+   //Table table;
+   //Row r;
+   Array<Id> matches;
+
+   void Abort()
+   {
+      /*if(abort)
+         abortNow = true;
+      else*/
+      if(active)
+         abort = true;
+   }
+
+private:
+   bool abort, abortNow;
+
+   unsigned int Main()
+   {
+      app.Wait();
+      app.Lock();
+
+      //if(app.ProcessInput(true))
+         //app.Wait();
+      {
+         Row r { editor.table };
+         if(matches)
+         {
+            int c;
+            if(editor.listFields && editor.idField)
+            {
+               /*for(c=0; c<matches.count && !abort; c++)
+               {
+                  if(r.Find(editor.idField, middle, nil, matches[c]))
+                  {
+                     Id id = 0;
+                     DataRow row;
+                     GuiLock();
+                     row = editor.list.AddRow();
+                     r.GetData(editor.idField, id);
+                     row.tag = id;
+                     editor.SetListRowFields(r, row, true);
+                     GuiUnlock();
+                  }
+                  else
+                     DebugLn($"WordList match cannot be found in database.");
+               }*/
+            }
+            else if(editor.idField && editor.stringField)
+            {
+               /*for(c=0; c<matches.count && !abort; c++)
+               {
+                  if(r.Find(editor.idField, middle, nil, matches[c]))
+                  {
+                     Id id = 0;
+                     String s = null;
+                     r.GetData(editor.idField, id);
+                     r.GetData(editor.stringField, s);
+                     GuiLock();
+                     editor.list.AddString(s).tag = id;
+                     GuiUnlock();
+                     delete s;
+                  }
+                  else
+                     DebugLn($"WordList match cannot be found in database.");
+               }*/
+            }
+            else
+               ;//app.Unlock();
+         }
+         else if(!editor.disabledFullListing)
+         {
+            if(editor.listFields && editor.idField)
+            {
+               app.Unlock();
+               while(r.Next() && !abort)
+               {
+                  Id id = 0;
+                  DataRow row;
+               app.Unlock();
+                  r.GetData(editor.idField, id);
+                  //if(app.ProcessInput(true))
+                     //app.Wait();
+                  //app.Wait();
+                  app.Lock();
+                     row = editor.list.AddRow();
+                     row.tag = id;
+                     editor.SetListRowFields(r, row, true);
+                  //app.Unlock();
+               }
+               //app.Unlock();
+            }
+            else if(editor.idField && editor.stringField)
+            {
+               /*while(r.Next() && !abort)
+               {
+                  Id id = 0;
+                  String s = null;
+                  GuiLock();
+                  r.GetData(editor.idField, id);
+                  r.GetData(editor.stringField, s);
+                  editor.list.AddString(s).tag = id;
+                  GuiUnlock();
+                  delete s;
+               }*/
+            }
+            else
+               ;//app.Unlock();
+         }
+         else
+            ;//app.Unlock();
+
+         //app.Lock();
+            editor.list.Sort(editor.listSortField, editor.listSortOrder);
+         //app.Unlock();
+      }
+      active = false;
+      abort = false;
+
+      app.Unlock();
+      return 0;
    }
+
+   /*void GuiLock()
+   {
+      app.Wait();
+      app.Lock();
+   }*/
+
+   /*void GuiUnlock()
+   {
+      app.Unlock();
+      editor.list.Update(null);
+      //app.Wait(); // Sleep(0.2f);
+      //if(app.ProcessInput(true))
+         //app.Wait();
+         // Update(null);
+         //app.UpdateDisplay();
+      //app.Wait();
+      // app.Lock();
+   }*/
+}
+#endif
+
+static define app = ((GuiApplication)__thisModule);
+
+static inline void DebugLn(typed_object object, ...)
+{
+#if defined(_DEBUG_LINE)
+   va_list args;
+   char buffer[4096];
+   va_start(args, object);
+   PrintStdArgsToBuffer(buffer, sizeof(buffer), object, args);
+   va_end(args);
+   puts(buffer);
+#endif
 }