Example #1
0
        private ITsString GetBest(int ws)
        {
            var bestWs = WritingSystemServices.ActualWs(m_object.Cache, ws, m_object.Hvo, m_flid);

            return((bestWs == 0 ? null : get_String(bestWs)) ??
                   m_object.Cache.TsStrFactory.MakeString(Strings.ksStars, m_object.Cache.WritingSystemFactory.UserWs));
        }
        private bool AddNewText(Command command)
        {
            // Get the default writing system for the new text.  See LT-6692.
            m_wsPrevText = Cache.DefaultVernWs;
            if (CurrentObject != null && Cache.ServiceLocator.WritingSystems.VernacularWritingSystems.Count > 1)
            {
                m_wsPrevText = WritingSystemServices.ActualWs(Cache, WritingSystemServices.kwsVernInParagraph,
                                                              CurrentObject.Hvo, StTextTags.kflidParagraphs);
            }
            if (m_list.Filter != null)
            {
                // Tell the user we're turning off the filter, and then do it.
                MessageBox.Show(ITextStrings.ksTurningOffFilter, ITextStrings.ksNote, MessageBoxButtons.OK);
                m_mediator.SendMessage("RemoveFilters", this);
                m_activeMenuBarFilter = null;
            }
            SaveOnChangeRecord();             // commit any changes before we create a new text.
            RecordList.ICreateAndInsert <IStText> createAndInsertMethodObj;
            if (command != null)
            {
                createAndInsertMethodObj = new UndoableCreateAndInsertStText(Cache, command, this);
            }
            else
            {
                createAndInsertMethodObj = new NonUndoableCreateAndInsertStText(Cache, this);
            }
            var newText = m_list.DoCreateAndInsert(createAndInsertMethodObj);

            // Check to if a genre was assigned to this text
            // (when selected from the text list: ie a genre w/o a text was sellected)
            string property  = GetCorrespondingPropertyName("DelayedGenreAssignment");
            var    genreList = m_mediator.PropertyTable.GetValue(property, null) as List <TreeNode>;
            var    ownerText = newText.Owner as FDO.IText;

            if (genreList != null && genreList.Count > 0 && ownerText != null)
            {
                foreach (var node in genreList)
                {
                    ownerText.GenresRC.Add((ICmPossibility)node.Tag);
                }
                m_mediator.PropertyTable.RemoveProperty(property);
            }

            if (CurrentObject == null || CurrentObject.Hvo == 0)
            {
                return(false);
            }
            if (!InDesiredTool("interlinearEdit"))
            {
                m_mediator.SendMessage("FollowLink", new FwLinkArgs("interlinearEdit", CurrentObject.Guid));
            }
            // This is a workable alternative (where link is the one created above), but means this code has to know about the FwXApp class.
            //(FwXApp.App as FwXApp).OnIncomingLink(link);
            // This alternative does NOT work; it produces a deadlock...I think the remote code is waiting for the target app
            // to return to its message loop, but it never does, because it is the same app that is trying to send the link, so it is busy
            // waiting for 'Activate' to return!
            //link.Activate();
            return(true);
        }
Example #3
0
        public override void Display(IVwEnv vwenv, int hvo, int frag)
        {
            switch (frag)
            {
            case kfragContext:
                var  ctxtOrVar       = m_cache.ServiceLocator.GetInstance <IPhContextOrVarRepository>().GetObject(hvo);
                bool isOuterIterCtxt = false;
                // are we inside an iteration context? this is important since we only open a context pile if we are not
                // in an iteration context, since an iteration context does it for us
                if (vwenv.EmbeddingLevel > 0)
                {
                    int outerHvo, outerTag, outerIndex;
                    vwenv.GetOuterObject(vwenv.EmbeddingLevel - 1, out outerHvo, out outerTag, out outerIndex);
                    var outerObj = m_cache.ServiceLocator.GetObject(outerHvo);
                    isOuterIterCtxt = outerObj.ClassID == PhIterationContextTags.kClassId;
                }

                switch (ctxtOrVar.ClassID)
                {
                case PhSequenceContextTags.kClassId:
                    var seqCtxt = (IPhSequenceContext)ctxtOrVar;
                    if (seqCtxt.MembersRS.Count > 0)
                    {
                        vwenv.AddObjVecItems(PhSequenceContextTags.kflidMembers, this, kfragContext);
                    }
                    else
                    {
                        OpenSingleLinePile(vwenv, GetMaxNumLines(), false);
                        vwenv.Props = m_bracketProps;
                        vwenv.AddProp(PhSequenceContextTags.kflidMembers, this, kfragEmpty);
                        CloseSingleLinePile(vwenv, false);
                    }
                    break;

                case PhSimpleContextNCTags.kClassId:
                    var ncCtxt = (IPhSimpleContextNC)ctxtOrVar;
                    if (ncCtxt.FeatureStructureRA != null && ncCtxt.FeatureStructureRA.ClassID == PhNCFeaturesTags.kClassId)
                    {
                        // Natural class simple context with a feature-based natural class
                        var natClass = (IPhNCFeatures)ncCtxt.FeatureStructureRA;

                        int numLines = GetNumLines(ncCtxt);
                        if (numLines == 0)
                        {
                            if (!isOuterIterCtxt)
                            {
                                OpenSingleLinePile(vwenv, GetMaxNumLines());
                            }

                            vwenv.AddProp(ktagInnerNonBoundary, this, kfragLeftBracket);
                            vwenv.AddProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragQuestions);
                            vwenv.AddProp(ktagInnerNonBoundary, this, kfragRightBracket);

                            if (!isOuterIterCtxt)
                            {
                                CloseSingleLinePile(vwenv);
                            }
                        }
                        else if (numLines == 1)
                        {
                            if (!isOuterIterCtxt)
                            {
                                OpenSingleLinePile(vwenv, GetMaxNumLines());
                            }

                            // use normal brackets for a single line context
                            vwenv.AddProp(ktagInnerNonBoundary, this, kfragLeftBracket);

                            // special consonant and vowel natural classes only display the abbreviation
                            if (natClass.Abbreviation.AnalysisDefaultWritingSystem.Text == "C" ||
                                natClass.Abbreviation.AnalysisDefaultWritingSystem.Text == "V")
                            {
                                vwenv.AddObjProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragNC);
                            }
                            else
                            {
                                if (natClass.FeaturesOA != null && natClass.FeaturesOA.FeatureSpecsOC.Count > 0)
                                {
                                    vwenv.AddObjProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragFeatNC);
                                }
                                else if (ncCtxt.PlusConstrRS.Count > 0)
                                {
                                    vwenv.AddObjVecItems(PhSimpleContextNCTags.kflidPlusConstr, this, kfragPlusVariable);
                                }
                                else
                                {
                                    vwenv.AddObjVecItems(PhSimpleContextNCTags.kflidMinusConstr, this, kfragMinusVariable);
                                }
                            }
                            vwenv.AddProp(ktagInnerNonBoundary, this, kfragRightBracket);

                            if (!isOuterIterCtxt)
                            {
                                CloseSingleLinePile(vwenv);
                            }
                        }
                        else
                        {
                            // multiline context

                            // left bracket pile
                            int maxNumLines = GetMaxNumLines();
                            vwenv.Props = m_bracketProps;
                            vwenv.set_IntProperty((int)FwTextPropType.ktptMarginLeading, (int)FwTextPropVar.ktpvMilliPoint, PileMargin);
                            vwenv.OpenInnerPile();
                            AddExtraLines(maxNumLines - numLines, ktagLeftNonBoundary, vwenv);
                            vwenv.AddProp(ktagLeftNonBoundary, this, kfragLeftBracketUpHook);
                            for (int i = 1; i < numLines - 1; i++)
                            {
                                vwenv.AddProp(ktagLeftNonBoundary, this, kfragLeftBracketExt);
                            }
                            vwenv.AddProp(ktagLeftBoundary, this, kfragLeftBracketLowHook);
                            vwenv.CloseInnerPile();

                            // feature and variable pile
                            vwenv.set_IntProperty((int)FwTextPropType.ktptAlign, (int)FwTextPropVar.ktpvEnum, (int)FwTextAlign.ktalLeft);
                            vwenv.OpenInnerPile();
                            AddExtraLines(maxNumLines - numLines, vwenv);
                            vwenv.AddObjProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragFeatNC);
                            vwenv.AddObjVecItems(PhSimpleContextNCTags.kflidPlusConstr, this, kfragPlusVariable);
                            vwenv.AddObjVecItems(PhSimpleContextNCTags.kflidMinusConstr, this, kfragMinusVariable);
                            vwenv.CloseInnerPile();

                            // right bracket pile
                            vwenv.Props = m_bracketProps;
                            if (!isOuterIterCtxt)
                            {
                                vwenv.set_IntProperty((int)FwTextPropType.ktptMarginTrailing, (int)FwTextPropVar.ktpvMilliPoint, PileMargin);
                            }
                            vwenv.OpenInnerPile();
                            AddExtraLines(maxNumLines - numLines, ktagRightNonBoundary, vwenv);
                            vwenv.AddProp(ktagRightNonBoundary, this, kfragRightBracketUpHook);
                            for (int i = 1; i < numLines - 1; i++)
                            {
                                vwenv.AddProp(ktagRightNonBoundary, this, kfragRightBracketExt);
                            }
                            vwenv.AddProp(ktagRightBoundary, this, kfragRightBracketLowHook);
                            vwenv.CloseInnerPile();
                        }
                    }
                    else
                    {
                        // natural class context with segment-based natural class
                        if (!isOuterIterCtxt)
                        {
                            OpenSingleLinePile(vwenv, GetMaxNumLines());
                        }

                        vwenv.AddProp(ktagInnerNonBoundary, this, kfragLeftBracket);
                        if (ncCtxt.FeatureStructureRA != null)
                        {
                            vwenv.AddObjProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragNC);
                        }
                        else
                        {
                            vwenv.AddProp(PhSimpleContextNCTags.kflidFeatureStructure, this, kfragQuestions);
                        }
                        vwenv.AddProp(ktagInnerNonBoundary, this, kfragRightBracket);

                        if (!isOuterIterCtxt)
                        {
                            CloseSingleLinePile(vwenv);
                        }
                    }
                    break;

                case PhIterationContextTags.kClassId:
                    var iterCtxt = (IPhIterationContext)ctxtOrVar;
                    if (iterCtxt.MemberRA != null)
                    {
                        int numLines = GetNumLines(iterCtxt.MemberRA as IPhSimpleContext);
                        if (numLines > 1)
                        {
                            vwenv.AddObjProp(PhIterationContextTags.kflidMember, this, kfragContext);
                            DisplayIterCtxt(numLines, vwenv);
                        }
                        else
                        {
                            OpenSingleLinePile(vwenv, GetMaxNumLines());
                            if (iterCtxt.MemberRA.ClassID == PhSimpleContextNCTags.kClassId)
                            {
                                vwenv.AddObjProp(PhIterationContextTags.kflidMember, this, kfragContext);
                            }
                            else
                            {
                                vwenv.AddProp(ktagInnerNonBoundary, this, kfragLeftParen);
                                vwenv.AddObjProp(PhIterationContextTags.kflidMember, this, kfragContext);
                                vwenv.AddProp(ktagInnerNonBoundary, this, kfragRightParen);
                            }
                            DisplayIterCtxt(1, vwenv);
                            // Views doesn't handle selection properly when we have an inner pile with strings on either side,
                            // so we don't add a zero-width space at the end
                            CloseSingleLinePile(vwenv, false);
                        }
                    }
                    else
                    {
                        OpenSingleLinePile(vwenv, GetMaxNumLines());
                        vwenv.AddProp(PhIterationContextTags.kflidMember, this, kfragQuestions);
                        CloseSingleLinePile(vwenv);
                    }
                    break;

                case PhSimpleContextSegTags.kClassId:
                    if (!isOuterIterCtxt)
                    {
                        OpenSingleLinePile(vwenv, GetMaxNumLines());
                    }

                    var segCtxt = (IPhSimpleContextSeg)ctxtOrVar;
                    if (segCtxt.FeatureStructureRA != null)
                    {
                        vwenv.AddObjProp(PhSimpleContextSegTags.kflidFeatureStructure, this, kfragTerminalUnit);
                    }
                    else
                    {
                        vwenv.AddProp(PhSimpleContextSegTags.kflidFeatureStructure, this, kfragQuestions);
                    }

                    if (!isOuterIterCtxt)
                    {
                        CloseSingleLinePile(vwenv);
                    }
                    break;

                case PhSimpleContextBdryTags.kClassId:
                    if (!isOuterIterCtxt)
                    {
                        OpenSingleLinePile(vwenv, GetMaxNumLines());
                    }

                    var bdryCtxt = (IPhSimpleContextBdry)ctxtOrVar;
                    if (bdryCtxt.FeatureStructureRA != null)
                    {
                        vwenv.AddObjProp(PhSimpleContextBdryTags.kflidFeatureStructure, this, kfragTerminalUnit);
                    }
                    else
                    {
                        vwenv.AddProp(PhSimpleContextBdryTags.kflidFeatureStructure, this, kfragQuestions);
                    }

                    if (!isOuterIterCtxt)
                    {
                        CloseSingleLinePile(vwenv);
                    }
                    break;

                case PhVariableTags.kClassId:
                    OpenSingleLinePile(vwenv, GetMaxNumLines());
                    vwenv.AddProp(ktagXVariable, this, kfragXVariable);
                    CloseSingleLinePile(vwenv);
                    break;
                }
                break;

            case kfragNC:
                int ncWs = WritingSystemServices.ActualWs(m_cache, WritingSystemServices.kwsFirstAnal, hvo,
                                                          PhNaturalClassTags.kflidAbbreviation);
                if (ncWs != 0)
                {
                    vwenv.AddStringAltMember(PhNaturalClassTags.kflidAbbreviation, ncWs, this);
                }
                else
                {
                    ncWs = WritingSystemServices.ActualWs(m_cache, WritingSystemServices.kwsFirstAnal, hvo,
                                                          PhNaturalClassTags.kflidName);
                    if (ncWs != 0)
                    {
                        vwenv.AddStringAltMember(PhNaturalClassTags.kflidName, ncWs, this);
                    }
                    else
                    {
                        vwenv.AddProp(PhNaturalClassTags.kflidAbbreviation, this, kfragQuestions);
                    }
                }
                break;

            case kfragTerminalUnit:
                int tuWs = WritingSystemServices.ActualWs(m_cache, WritingSystemServices.kwsFirstVern,
                                                          hvo, PhTerminalUnitTags.kflidName);
                if (tuWs != 0)
                {
                    vwenv.AddStringAltMember(PhTerminalUnitTags.kflidName, tuWs, this);
                }
                else
                {
                    vwenv.AddProp(PhTerminalUnitTags.kflidName, this, kfragQuestions);
                }
                break;

            case kfragFeatNC:
                vwenv.AddObjProp(PhNCFeaturesTags.kflidFeatures, this, kfragFeats);
                break;

            case kfragFeats:
                vwenv.AddObjVecItems(FsFeatStrucTags.kflidFeatureSpecs, this, kfragFeature);
                break;

            case kfragFeature:
                vwenv.AddProp(ktagFeature, this, kfragFeatureLine);
                break;

            case kfragPlusVariable:
                vwenv.AddProp(ktagVariable, this, kfragPlusVariableLine);
                break;

            case kfragMinusVariable:
                vwenv.AddProp(ktagVariable, this, kfragMinusVariableLine);
                break;
            }
        }
        /// <summary>
        /// Set custom field data for one field (specified by owner HVO and field ID).
        /// </summary>
        /// <returns><c>true</c>, if custom field data was set, <c>false</c> otherwise (e.g., if value hadn't changed,
        /// or value was null, or field type was one not implemented in LCM, such as CellarPropertyType.Float).</returns>
        /// <param name="hvo">HVO of object whose field we're setting.</param>
        /// <param name="flid">Field ID of custom field to set.</param>
        /// <param name="value">Field's new value (as returned by GetCustomFieldData).</param>
        /// <param name="guidOrGuids">GUID or guids associated with new value (as returned by GetCustomFieldData).
        /// May be null or BsonNull.Value if no GUIDs associated with this value.</param>
        public bool SetCustomFieldData(int hvo, int flid, BsonValue value, BsonValue guidOrGuids)
        {
            if ((value == null) || (value == BsonNull.Value) ||
                ((value.BsonType == BsonType.Array) && (value.AsBsonArray.Count == 0)))
            {
                return(false);
            }
            List <Guid> fieldGuids = new List <Guid>();

            if (guidOrGuids == null || guidOrGuids == BsonNull.Value)
            {
                // Leave fieldGuids as an empty list
            }
            else
            {
                if (guidOrGuids is BsonArray)
                {
                    fieldGuids.AddRange(guidOrGuids.AsBsonArray.Select(bsonValue => ParseGuidOrDefault(bsonValue.AsString)));
                }
                else
                {
                    fieldGuids.Add(ParseGuidOrDefault(guidOrGuids.AsString));
                }
            }
            ISilDataAccessManaged data      = (ISilDataAccessManaged)cache.DomainDataByFlid;
            CellarPropertyType    fieldType = (CellarPropertyType)lcmMetaData.GetFieldType(flid);
            string fieldName = lcmMetaData.GetFieldNameOrNull(flid);

//			logger.Debug("Custom field named {0} has type {1}", fieldName, fieldType.ToString());
            if (fieldName == null)
            {
                return(false);
            }

            // Valid field types in LCM are GenDate, Integer, String, OwningAtomic, ReferenceAtomic, and ReferenceCollection, so that's all we implement.
            switch (fieldType)
            {
            case CellarPropertyType.GenDate:
            {
                var    valueAsMultiText = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument);
                string valueAsString    = valueAsMultiText.BestString(new string[] { MagicStrings.LanguageCodeForGenDateFields });
                if (string.IsNullOrEmpty(valueAsString))
                {
                    return(false);
                }
                GenDate valueAsGenDate;
                if (GenDate.TryParse(valueAsString, out valueAsGenDate))
                {
                    GenDate oldValue = data.get_GenDateProp(hvo, flid);
                    if (oldValue == valueAsGenDate)
                    {
                        return(false);
                    }
                    else
                    {
                        data.SetGenDate(hvo, flid, valueAsGenDate);
                        return(true);
                    }
                }
                return(false);
            }

            case CellarPropertyType.Integer:
            {
                var    valueAsMultiText = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument);
                string valueAsString    = valueAsMultiText.BestString(new string[] { MagicStrings.LanguageCodeForIntFields });
                if (string.IsNullOrEmpty(valueAsString))
                {
                    return(false);
                }
                int valueAsInt;
                if (int.TryParse(valueAsString, out valueAsInt))
                {
                    int oldValue = data.get_IntProp(hvo, flid);
                    if (oldValue == valueAsInt)
                    {
                        return(false);
                    }
                    else
                    {
                        data.SetInt(hvo, flid, valueAsInt);
                        return(true);
                    }
                }
                return(false);
            }

            case CellarPropertyType.OwningAtomic:
            {
                // Custom field is a MultiparagraphText, which is an IStText object in LCM
                IStTextRepository textRepo = servLoc.GetInstance <IStTextRepository>();
                Guid    fieldGuid          = fieldGuids.FirstOrDefault();
                IStText text;
                if (!textRepo.TryGetObject(fieldGuid, out text))
                {
                    int currentFieldContentsHvo = data.get_ObjectProp(hvo, flid);
                    if (currentFieldContentsHvo != LcmCache.kNullHvo)
                    {
                        text = (IStText)cache.GetAtomicPropObject(currentFieldContentsHvo);
                    }
                    else
                    {
                        // NOTE: I don't like the "magic" -2 number below, but LCM doesn't seem to have an enum for this. 2015-11 RM
                        int newStTextHvo = data.MakeNewObject(cache.GetDestinationClass(flid), hvo, flid, -2);
                        text = (IStText)cache.GetAtomicPropObject(newStTextHvo);
                    }
                }
                // Shortcut: if text contents haven't changed, we don't want to change anything at all
                BsonValue currentLcmTextContents = ConvertUtilities.GetCustomStTextValues(text, flid,
                                                                                          servLoc.WritingSystemManager, lcmMetaData, cache.DefaultUserWs);
                if ((currentLcmTextContents == BsonNull.Value || currentLcmTextContents == null) &&
                    (value == BsonNull.Value || value == null))
                {
                    return(false);
                }
                if (currentLcmTextContents != null && currentLcmTextContents.Equals(value))
                {
                    // No changes needed.
                    return(false);
                }
                // BsonDocument passed in contains "paragraphs". ParseCustomStTextValuesFromBson wants only a "value" element
                // inside the doc, so we'll need to construct a new doc for the StTextValues.
                BsonDocument     doc       = value.AsBsonDocument;
                LfMultiParagraph multiPara = BsonSerializer.Deserialize <LfMultiParagraph>(doc);
                // Now we have another way to check for "old value and new value were the same": if the LCM multiparagraph was empty,
                // GetCustomStTextValues will have returned null -- so if this multiPara has no paragraphs, that's also an unchanged situation
                if ((multiPara.Paragraphs == null || multiPara.Paragraphs.Count <= 0) &&
                    (currentLcmTextContents == BsonNull.Value || currentLcmTextContents == null))
                {
                    return(false);
                }
                int wsId;
                if (multiPara.InputSystem == null)
                {
                    wsId = lcmMetaData.GetFieldWs(flid);
                }
                else
                {
                    wsId = servLoc.WritingSystemFactory.GetWsFromStr(multiPara.InputSystem);
                }
                ConvertUtilities.SetCustomStTextValues(text, multiPara.Paragraphs, wsId);

                return(true);
            }

            case CellarPropertyType.ReferenceAtomic:
                if (fieldGuids.FirstOrDefault() != Guid.Empty)
                {
                    int referencedHvo = data.get_ObjFromGuid(fieldGuids.FirstOrDefault());
                    int oldHvo        = data.get_ObjectProp(hvo, flid);
                    if (referencedHvo == oldHvo)
                    {
                        return(false);
                    }
                    else
                    {
                        data.SetObjProp(hvo, flid, referencedHvo);
                        // TODO: What if the value of the referenced object has changed in LanguageForge? (E.g., change that possibility's text from "foo" to "bar")
                        // Need to implement that scenario.
                        return(true);
                    }
                }
                else
                {
                    // It's a reference to an ICmPossibility instance: create a new entry in appropriate PossibilityList
                    LfStringField valueAsLfStringField = BsonSerializer.Deserialize <LfStringField>(value.AsBsonDocument);
                    string        nameHierarchy        = valueAsLfStringField.Value;
                    if (nameHierarchy == null)
                    {
                        return(false);
                    }
                    int fieldWs = lcmMetaData.GetFieldWs(flid);
                    // Oddly, this can return 0 for some custom fields. TODO: Find out why: that seems like it would be an error.
                    if (fieldWs == 0)
                    {
                        fieldWs = cache.DefaultUserWs;                         // TODO: Investigate, because this should probably be wsEn instead so that we can create correct keys.
                    }
                    if (fieldWs < 0)
                    {
                        // FindOrCreatePossibility has a bug where it doesn't handle "magic" writing systems (e.g., -1 for default analysis, etc) and
                        // throws an exception instead. So we need to get a real ws here.
                        fieldWs = WritingSystemServices.ActualWs(cache, fieldWs, hvo, flid);
                    }
                    ICmPossibilityList parentList = GetParentListForField(flid);
                    ICmPossibility     newPoss    = parentList.FindOrCreatePossibility(nameHierarchy, fieldWs);

                    int oldHvo = data.get_ObjectProp(hvo, flid);
                    if (newPoss.Hvo == oldHvo)
                    {
                        return(false);
                    }
                    else
                    {
                        data.SetObjProp(hvo, flid, newPoss.Hvo);
                        return(true);
                    }
                }

            case CellarPropertyType.ReferenceCollection:
            case CellarPropertyType.ReferenceSequence:
            {
                if (value == null || value == BsonNull.Value)
                {
                    // Can't write null to a collection or sequence in LCM; it's forbidden. So data.SetObjProp(hvo, flid, LcmCache.kNullHvo) will not work.
                    // Instead, we delete all items from the existing collection or sequence, and thus store an empty coll/seq in LCM.
                    int oldSize = data.get_VecSize(hvo, flid);
                    if (oldSize == 0)
                    {
                        // It was already empty, so leave it unchanged so we don't cause unnecessary changes in the .fwdata XML (and unnecessary Mercurial commits).
                        return(false);
                    }
                    else
                    {
                        data.Replace(hvo, flid, 0, oldSize, null, 0);                                 // This is how you set an empty array
                        return(true);
                    }
                }
                int fieldWs = lcmMetaData.GetFieldWs(flid);
                // TODO: Investigate why this is sometimes coming back as 0 instead of as a real writing system ID
                if (fieldWs == 0)
                {
                    fieldWs = cache.DefaultUserWs;
                }
                ICmPossibilityList parentList = GetParentListForField(flid);

                LfStringArrayField valueAsStringArray = BsonSerializer.Deserialize <LfStringArrayField>(value.AsBsonDocument);

                // Step 1: Get ICmPossibility instances from the string keys that LF gave us

                // First go through all the GUIDs we have and match them up to the keys. If they match up,
                // then remove the keys from the list. Any remaining keys get looked up with FindOrCreatePossibility(), so now we
                // have a complete set of GUIDs (or ICmPossibility objects, which works out to the same thing).

                // TODO: This is all kind of ugly, and WAY too long for one screen. I could put it in its own function,
                // but there's really no real gain from that, as it simply moves the logic even further away from where
                // it needs to be. There's not really a *good* way to achieve simplicity with this code design, unfortunately.
                // The only thing that would be close to simple would be to call some functions from the LcmToMongo option list
                // converters, and that's pulling in code from the "wrong" direction, which has its own ugliness. Ugh.

                HashSet <string> keysFromLF = new HashSet <string>(valueAsStringArray.Values);
                var fieldObjs = new List <ICmPossibility>();
                foreach (Guid guid in fieldGuids)
                {
                    ICmPossibility poss;
                    string         key = "";
                    if (guid != default(Guid))
                    {
                        poss = servLoc.GetInstance <ICmPossibilityRepository>().GetObject(guid);
                        if (poss == null)
                        {
                            // TODO: Decide what to do with possibilities deleted from LCM
                            key = "";
                        }
                        else
                        {
                            if (poss.Abbreviation == null)
                            {
                                key = "";
                            }
                            else
                            {
                                ITsString keyTss = poss.Abbreviation.get_String(wsEn);
                                key = keyTss == null ? "" : keyTss.Text ?? "";
                            }
                            fieldObjs.Add(poss);
                        }
                    }
                    keysFromLF.Remove(key);
                    // Ignoring return value (HashSet.Remove returns false if the key wasn't present), because false could mean one of two things:
                    // 1. The CmPossibility had its English abbreviation changed in LCM, but LF doesn't know this yet.
                    //    If this is the case, the LF key won't match, but the GUID will still match. So we might end up creating
                    //    duplicate entries below with the FindOrCreatePossibility. TODO: Need to verify that LCM->LF possibility lists
                    //    get updated correctly if renames happen! (... Or use the OptionList converters, that's what they were for.)
                    // 2. The CmPossibility was just created in LF and isn't in LCM yet. In which case we should have been using the
                    //    OptionList converters, which would hopefully have handled creating the ICmPossibility instane in LCM.
                    // Either way, we can't really use that fact later, since we can't be certain if the possibility was renamed or created.
                }
                // Any remaining keysFromLF strings did not have corresponding GUIDs in Mongo.
                // This is most likely because they were added by LF, which doesn't write to the customFieldGuids field.
                // So we assume they exist in FW, and just look them up.
                foreach (string key in keysFromLF)
                {
                    ICmPossibility poss = parentList.FindOrCreatePossibility(key, wsEn);
                    // TODO: If this is a new possibility, then we need to populate it with ALL the corresponding data from LF,
                    // which we don't necessarily have at this point. Need to make that a separate step in the Send/Receive: converting option lists first.
                    fieldObjs.Add(poss);
                }
                // logger.Debug("Custom field {0} for CmObject {1}: BSON list was [{2}] and customFieldGuids was [{3}]. This was translated to keysFromLF = [{4}] and fieldObjs = [{5}]",
                //  fieldName,
                //  hvo,
                //  String.Join(", ", valueAsStringArray.Values),
                //  String.Join(", ", fieldGuids.Select(g => g.ToString())),
                //  String.Join(", ", keysFromLF.AsEnumerable()),
                //  String.Join(", ", fieldObjs.Select(poss => poss.AbbrAndName))
                // );
                // Step 2: Remove any objects from the "old" list that weren't in the "new" list
                // We have to look them up by HVO because that's the only public API available in LCM
                // Following logic inspired by XmlImportData.CopyCustomFieldData in FieldWorks source
                int[] oldHvosArray = data.VecProp(hvo, flid);
                int[] newHvosArray = fieldObjs.Select(poss => poss.Hvo).ToArray();
                // Shortcut check
                if (oldHvosArray.SequenceEqual(newHvosArray))
                {
                    // Nothing to do, so return now so that we don't cause unnecessary changes and commits in Mercurial
                    return(false);
                }
                HashSet <int> newHvos      = new HashSet <int>(newHvosArray);
                HashSet <int> combinedHvos = new HashSet <int>();
                // Loop backwards so deleting items won't mess up indices of subsequent deletions
                for (int idx = oldHvosArray.Length - 1; idx >= 0; idx--)
                {
                    int oldHvo = oldHvosArray[idx];
                    if (newHvos.Contains(oldHvo))
                    {
                        combinedHvos.Add(oldHvo);
                    }
                    else
                    {
                        data.Replace(hvo, flid, idx, idx + 1, null, 0);                                 // Important to pass *both* null *and* 0 here to remove items
                    }
                }

                // Step 3: Add any objects from the "new" list that weren't in the "old" list
                foreach (int newHvo in newHvosArray)
                {
                    if (combinedHvos.Contains(newHvo))
                    {
                        continue;
                    }
                    // This item was added in the new list
                    data.Replace(hvo, flid, combinedHvos.Count, combinedHvos.Count, new int[] { newHvo }, 1);
                    combinedHvos.Add(newHvo);
                }
                return(true);
            }

            case CellarPropertyType.String:
            {
                var    valueAsMultiText          = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument);
                int    wsIdForField              = lcmMetaData.GetFieldWs(flid);
                string wsStrForField             = servLoc.WritingSystemFactory.GetStrFromWs(wsIdForField);
                KeyValuePair <string, string> kv = valueAsMultiText.BestStringAndWs(new string[] { wsStrForField });
                string foundWs   = kv.Key ?? string.Empty;
                string foundData = kv.Value ?? string.Empty;
                int    foundWsId = servLoc.WritingSystemFactory.GetWsFromStr(foundWs);
                if (foundWsId == 0)
                {
                    return(false);                            // Skip any unidentified writing systems
                }
                ITsString oldValue = data.get_StringProp(hvo, flid);
                ITsString newValue = ConvertMongoToLcmTsStrings.SpanStrToTsString(foundData, foundWsId, servLoc.WritingSystemFactory);
                if (oldValue != null && TsStringUtils.GetDiffsInTsStrings(oldValue, newValue) == null)                         // GetDiffsInTsStrings() returns null when there are no changes
                {
                    return(false);
                }
                else
                {
                    data.SetString(hvo, flid, newValue);
                    return(true);
                }
            }

            default:
                return(false);
                // TODO: Maybe issue a proper warning (or error) log message for "field type not recognized"?
            }
        }