#include 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; })) 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 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 : 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 { bool initialized; public: property Table table { set { DebugLn("TableEditor::table|set"); table = value; } } Table table; property Table index { set { DebugLn("TableEditor::index|set"); index = value; filterRow.tbl = index; } } Table 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; FieldIndex indexedFields[1]; if(!idField) fldId = table.FindField(defaultIdField); if(!fldName) fldName = table.FindField(defaultNameField); if(!fldActive) fldActive = table.FindField(defaultActiveField); indexedFields[0] = { fldId }; table.Index(1, indexedFields); editRow.tbl = table; 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; } // List property ListBox list { set { DebugLn("TableEditor::list|set"); list = value; //ResetListFields(); } } Field idField; Field stringField; Field indexFilterField; ListBox list; property Array listFields { set { DebugLn("TableEditor::listFields|set"); listFields = value; //ResetListFields(); } } Array listFields; int listSortOrder; DataField listSortField; bool disabledFullListing; property Array searchFields { set { StringSearchTable searchTable { table, idField, value }; DebugLn("TableEditor::searchFields|set"); // The searchTables property will incref... property::searchTables = { [ searchTable ] }; } } property Array 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 searchTables; property Array sqliteSearchTables { set { incref value; for(t : value) { incref t; } DebugLn("TableEditor::searchTables|set"); if(sqliteSearchTables) sqliteSearchTables.Free(); delete sqliteSearchTables; sqliteSearchTables = value; } } Array sqliteSearchTables; property String searchString { set { bool modified = modifiedDocument; DebugLn("TableEditor::searchString|set"); switch(modified ? OnLeavingModifiedDocument() : no) { case cancel: break; case yes: EditSave(); case no: if(modified) EditLoad(); delete searchString; if(value && value[0]) searchString = CopyString(value); Enumerate(); break; } } } String searchString; Map lookups; Array dynamicLookupEditors; property Array dynamicLookupEditors { set { DebugLn("TableEditor::dynamicLookupEditors|set"); dynamicLookupEditors = value; } } // Fields Editor property Id selectedId { get { return selectedId; } } Array fieldsBoxes { }; Array tableEditors { }; Array dynamicLookupTableEditors { }; bool readOnly; public virtual void OnInitialize(); public virtual void OnLoad(); public virtual void OnStateChanged(); bool internalModifications; public void NotifyModifiedDocument() { DebugLn("TableEditor::NotifyModifiedDocument"); if(!internalModifications) OnStateChanged(); } //public virtual bool Window::NotifyNew(AltListSection listSection, Row r); //virtual void Window::NotifyInitFields(AltEditSection editSection); public virtual DialogResult OnLeavingModifiedDocument() { 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() { 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); public bool NotifyClosing() { bool result = true; DebugLn("TableEditor::NotifyClosing"); if(modifiedDocument) { switch(OnLeavingModifiedDocument()) { case cancel: result = false; break; case yes: EditSave(); case no: EditLoad(); break; } } if(result) { StopWordListPrepTimer(); StopListEnumerationTimer(); } return result; } //public void List() // this gets called out of nowhere by some constructor thing... public void Enumerate() { DebugLn("TableEditor::Enumerate"); if(list) { StopListEnumerationTimer(); list.Clear(); EditClear(); } if(list || OnList != TableEditor::OnList) { Row r { table }; Array 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 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) { listEnumerationTimer.matches = { }; // TOFIX: This (void *) cast here should NOT be required... Fix this Container warning: // warning: incompatible expression matches (ecere::com::Array); expected ecere::com::Container listEnumerationTimer.matches.Copy((void *)matches); } else listEnumerationTimer.matches = null; listEnumerationTimer.Start(); } else DebugLn("TableEditor::OnList -- timer state error"); } virtual void OnCreateDynamicLookupEditors() { DebugLn("TableEditor::OnCreateLookupEditors"); if(dynamicLookupEditors && dynamicLookupEditors.count) { for(f : dynamicLookupEditors) { if(f.editorClass && f.parentWindow && f.lookupFindField) { 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()) { // 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); } delete row; } } } } TableEditor masterEditor; public property TableEditor masterEditor { set { if(value != masterEditor) { if(masterEditor) masterEditor.RemoveTableEditor(this); masterEditor = value; if(value) value.AddTableEditor(this); } } } 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; }; void CreateRow() { DebugLn("TableEditor::CreateRow"); //list.NotifySelect(this, list, null, 0); 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? // 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); id++; } 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; 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) { 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? { 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;*/ } } } } 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, newText); if(list) { row = list.AddString(newText); row.tag = id; } } //delete r; delete newText; } if(list) { list.Sort(listSortField, listSortOrder); if(row) SelectListRow(row); } OnStateChanged(); } } void Remove() { DebugLn("TableEditor::Remove"); if(editRow.sysID) //list && list.currentRow) { if(OnRemovalRequest()) { editRow.Delete(); if(list) list.DeleteRow(list.currentRow); EditClear(); //NotifyDeleted(master, this); if(list) SelectListRow(list.currentRow); OnStateChanged(); } } } void Load() { DebugLn("TableEditor::Load"); EditLoad(); } void Write() { DebugLn("TableEditor::Write"); EditSave(); } bool ListSelect(DataRow row) { bool result = true; DebugLn("TableEditor::ListSelect"); if(/*-row && -*/row != lastRow) { uint id; if(modifiedDocument) { if(row) list.currentRow = lastRow; result = false; switch(OnLeavingModifiedDocument()) { case cancel: break; case yes: EditSave(); case no: EditClear(); list.currentRow = row; break; } } if(list.currentRow == row) SelectListRow(row); } return result; } bool Select(Id id) { bool result; 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)) { selectedId = editRow.sysID; EditLoad(); result = true; } else result = false; return result; } bool Filter(Id id) { bool result; DebugLn("TableEditor::Filter"); if(selectedId && index && indexFilterField) { 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; return result; } bool SelectNext(bool loopAround) { bool result = NotifyClosing(); bool wasNil = !editRow.sysID; DebugLn("TableEditor::SelectNext"); if(result) { 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; } return result; } bool SelectPrevious(bool loopAround) { bool result = NotifyClosing(); bool wasNil = !editRow.sysID; DebugLn("TableEditor::SelectPrevious"); if(result) { 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; } void SelectListRow(DataRow row) { // Time startTime = GetTime(); DebugLn("TableEditor::SelectListRow"); if(row) { // 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)) { listRow = row; //NotifySelectListRow(master, this, selectedId); EditLoad(); } } // Logf("SelectListRow took %f seconds\n", GetTime() - startTime); } private: Row editRow { }; DataRow listRow; DataRow lastRow; Id selectedId; Row filterRow { }; bool filtered; Array searchCI { }; ListEnumerationTimer listEnumerationTimer { userData = this, delay = 0.1f; bool DelayExpired() { static Time startTime; bool next = false; int c; Row row = listEnumerationTimer.row; Array 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 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 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) { // 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 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() { DebugLn("TableEditor::InitFieldsBoxes"); if(readOnly) { for(fb : fieldsBoxes) { fb.readOnly = true; fb.Init(); } } else { for(fb : fieldsBoxes) fb.Init(); } //NotifyInitFields(master, this); } void EditNew() { DebugLn("TableEditor::EditNew"); modifiedDocument = false; } void EditSave() { DebugLn("TableEditor::EditSave"); internalModifications = true; for(fb : fieldsBoxes) fb.Save(); if(idField && list && listFields && listFields.count) { DataRow listRow = list.currentRow; // ADDED THIS HERE FOR SQLITE TO REFRESH editRow.Find(idField, middle, nil, listRow.tag); SetListRowFields(editRow, listRow, false); list.Sort(listSortField, listSortOrder); } internalModifications = false; 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; 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(); for(e : dynamicLookupTableEditors) e.Destroy(0); for(e : tableEditors) e.Destroy(0); tableEditors.Free(); dynamicLookupTableEditors.Free(); //dynamicLookupTableEditors.size = 0; internalModifications = false; DebugLn(" TODO: remove all sub table editors"); modifiedDocument = false; OnStateChanged(); } void SetListRowFields(Row dbRow, DataRow listRow, bool restoreSelection) { // DebugLn("TableEditor::SetListRowFields"); for(lf : listFields) { if(lf.dataField && lf.field) { if(eClass_IsDerived(lf.field.type, class(char*))) { String s = null; dbRow.GetData(lf.field, s); listRow.SetData(lf.dataField, 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 SearchWordList() { DebugLn("TableEditor::SearchWordList"); #ifdef FULL_STRING_SEARCH { int c; int numTokens = 0; int len[256]; char * words[256]; WordEntry entries[256]; Array results = null; if(searchTables && searchTables.count && searchString && searchString[0]) { char * searchCopy = CopyString(searchString); numTokens = TokenizeWith(searchCopy, sizeof(words) / sizeof(char *), words, " ',/-;[]{}", false); for(c = 0; c 1) { // AND int i; Map matches { }; Map uniques { }; MapNode mn; results = { }; for(c = 0; c 1) results.Add(mn.key); } matches.Free(); delete matches; delete uniques; } else if(numTokens == 1) { results = { }; if(entries[0] && entries[0].items && entries[0].items.count) { for(c = 0; c 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]; asciiWord[numChars++] = ToASCII(ch); } if(!ch) break; } else { int cc; for(cc = 0; cc < nb; cc++) word[count++] = string[c + cc]; asciiWord[numChars++] = ToASCII(ch); } lastCh = ch; } } /*static */WordEntryBinaryTree wordTree { CompareKey = (void *)BinaryTree::CompareString; FreeKey = BinaryTree::FreeString; }; WordEntry letters[26]; WordEntry doubleLetters[26][26]; void AddWord(char * word, int count, bool addAllSubstrings, Id id) { //DebugLn("TableEditor::AddWord"); #ifdef FULL_STRING_SEARCH { if(addAllSubstrings) { int s; WordEntry mainEntry = null; WordEntry sEntry = null; for(s = 0; s < count; s += UTF8_NUM_BYTES(word[s])) { 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; subWord[l] = 0; if(ch1 >= 'a' && ch1 <= 'z') { char ch2 = subWord[1]; 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 { 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 { 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 searchFields; private: ~StringSearchTable() { delete searchFields; } } public class SQLiteSearchField { public: Field field; //StringSearchIndexingMethod method; }; public class SQLiteSearchTable { public: Table table; Field idField; Array searchFields; private: ~SQLiteSearchTable() { delete searchFields; } } static WordEntry * btnodes; struct WordEntryBinaryTree : BinaryTree { WordEntry * entries; void OnSerialize(IOChannel channel) { WordEntry node; uint id; uint count = this.count; DebugLn("WordEntryBinaryTree::OnSerialize"); for(id = 1, node = (WordEntry)root; node;) { node.id = id++; if(node.left) node = node.left; else if(node.right) node = node.right; else if(node.parent) { bool isLeft = node == node.parent.left; node = node.parent; while(node) { if(isLeft && node.right) { node = node.right; break; } if(node.parent) isLeft = node == node.parent.left; node = node.parent; } } else node = null; } id--; channel.Serialize(id); channel.Serialize((WordEntry)root); } void OnUnserialize(IOChannel channel) { WordEntry root, node; uint count; DebugLn("WordEntryBinaryTree::OnUnserialize"); channel.Unserialize(count); entries = new WordEntry[count]; btnodes = entries; channel.Unserialize(root); this.root = (BTNode)root; // count = root ? this.root.count : 0; this.count = count; for(node = (WordEntry)root; node;) { if(node.words) { int c; for(c = 0; c matches; Row row; Map 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 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