diff --git a/Build/mkall.targets b/Build/mkall.targets index b197e4357a..4c88587882 100644 --- a/Build/mkall.targets +++ b/Build/mkall.targets @@ -234,7 +234,7 @@ 6.0.0-beta0063 17.0.0-beta0089 9.4.0.1-beta - 11.0.0-beta0147 + 11.0.0-beta0148 70.1.123 3.7.4 1.1.1-beta0001 diff --git a/Build/nuget-common/packages.config b/Build/nuget-common/packages.config index 92e198129a..a8d76c37f4 100644 --- a/Build/nuget-common/packages.config +++ b/Build/nuget-common/packages.config @@ -52,15 +52,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/Src/FdoUi/InflectionFeatureEditor.cs b/Src/FdoUi/InflectionFeatureEditor.cs index 71d9b99aab..aae08a4529 100644 --- a/Src/FdoUi/InflectionFeatureEditor.cs +++ b/Src/FdoUi/InflectionFeatureEditor.cs @@ -17,6 +17,7 @@ using SIL.LCModel.Core.Text; using SIL.LCModel.Core.KernelInterfaces; using SIL.Utils; +using SIL.LCModel.Infrastructure; namespace SIL.FieldWorks.FdoUi { @@ -326,84 +327,136 @@ public void DoIt(IEnumerable itemsToChange, ProgressState state) HashSet possiblePOS = GetPossiblePartsOfSpeech(); // Make a Dictionary from HVO of entry to list of modified senses. var sensesByEntry = new Dictionary>(); - int i = 0; - // Report progress 50 times or every 100 items, whichever is more (but no more than once per item!) - int interval = Math.Min(100, Math.Max(itemsToChange.Count() / 50, 1)); - foreach(int hvoSense in itemsToChange) - { - i++; - if (i % interval == 0) - { - state.PercentDone = i * 20 / itemsToChange.Count(); - state.Breath(); - } - if (!IsItemEligible(m_cache.DomainDataByFlid, hvoSense, possiblePOS)) - continue; - var ls = m_cache.ServiceLocator.GetInstance().GetObject(hvoSense); - var msa = ls.MorphoSyntaxAnalysisRA; - int hvoEntry = ls.EntryID; - if (!sensesByEntry.ContainsKey(hvoEntry)) - sensesByEntry[hvoEntry] = new HashSet(); - sensesByEntry[hvoEntry].Add(ls); - } - //REVIEW: Should these really be the same Undo/Redo strings as for InflectionClassEditor.cs? - m_cache.DomainDataByFlid.BeginUndoTask(FdoUiStrings.ksUndoBEInflClass, FdoUiStrings.ksRedoBEInflClass); - i = 0; - interval = Math.Min(100, Math.Max(sensesByEntry.Count / 50, 1)); IFsFeatStruc fsTarget = null; if (m_selectedHvo != 0) fsTarget = Cache.ServiceLocator.GetInstance().GetObject(m_selectedHvo); - foreach (var kvp in sensesByEntry) + int i = 0; + //REVIEW: Should these really be the same Undo/Redo strings as for InflectionClassEditor.cs? + UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW(FdoUiStrings.ksUndoBEInflClass, FdoUiStrings.ksRedoBEInflClass, + m_cache.ActionHandlerAccessor, () => { - i++; - if (i % interval == 0) + // Report progress 50 times or every 100 items, whichever is more (but no more than once per item!) + int interval = Math.Min(100, Math.Max(itemsToChange.Count() / 50, 1)); + foreach (int hvoSense in itemsToChange) { - state.PercentDone = i * 80 / sensesByEntry.Count + 20; - state.Breath(); + i++; + if (i % interval == 0) + { + state.PercentDone = i * 20 / itemsToChange.Count(); + state.Breath(); + } + var ls = m_cache.ServiceLocator.GetInstance().GetObject(hvoSense); + IFsFeatStruc newFsTarget = fsTarget; + if (fsTarget != null && fsTarget.ContainsBlank()) + { + // Create a new fsTarget by filling in fsTarget's blanks using the lex sense's feature structure. + newFsTarget = FillInBlanks(fsTarget, ls); + } + if (!IsItemEligible(m_cache.DomainDataByFlid, hvoSense, possiblePOS, newFsTarget)) + continue; + int hvoEntry = ls.EntryID; + if (!sensesByEntry.ContainsKey(hvoEntry)) + sensesByEntry[hvoEntry] = new HashSet(); + sensesByEntry[hvoEntry].Add(ls); } - var entry = m_cache.ServiceLocator.GetInstance().GetObject(kvp.Key); - var sensesToChange = kvp.Value; - IMoStemMsa msmTarget = entry.MorphoSyntaxAnalysesOC.OfType() - .FirstOrDefault(msm => MsaMatchesTarget(msm, fsTarget)); - - if (msmTarget == null) + i = 0; + interval = Math.Min(100, Math.Max(sensesByEntry.Count / 50, 1)); + foreach (var kvp in sensesByEntry) { - // See if we can reuse an existing MoStemMsa by changing it. - // This is possible if it is used only by senses in the list, or not used at all. - var otherSenses = new HashSet(); - var senses = new HashSet(entry.AllSenses.ToArray()); - if (senses.Count != sensesToChange.Count) + i++; + if (i % interval == 0) { - otherSenses = new HashSet(senses.Where(ls => !sensesToChange.Contains(ls))); + state.PercentDone = i * 80 / sensesByEntry.Count + 20; + state.Breath(); } - - var msm = entry.MorphoSyntaxAnalysesOC - .OfType() // filter only IMoStemMsa - .FirstOrDefault(msa => !otherSenses.Any(ls => ls.MorphoSyntaxAnalysisRA == msa)); - - if (msm != null) + var entry = m_cache.ServiceLocator.GetInstance().GetObject(kvp.Key); + var sensesToChange = kvp.Value; + foreach (var ls in sensesToChange) { - // Can reuse this one! Nothing we don't want to change uses it. - // Adjust its POS as well as its inflection feature, just to be sure. - // Ensure that we don't change the POS! See LT-6835. - msmTarget = msm; - InitMsa(msmTarget, msm.PartOfSpeechRA.Hvo); + IFsFeatStruc newFsTarget = fsTarget; + if (fsTarget != null && fsTarget.ContainsBlank()) + { + newFsTarget = FillInBlanks(fsTarget, ls); + } + IMoStemMsa msmTarget = GetMsmTarget(newFsTarget, entry, sensesToChange, pos); + ls.MorphoSyntaxAnalysisRA = msmTarget; } } - if (msmTarget == null && pos != null) + }); + } + + IFsFeatStruc FillInBlanks(IFsFeatStruc pattern, ILexSense ls) + { + IFsFeatStruc copy = Cache.ServiceLocator.GetInstance().Create(); + IPartOfSpeech pos = pattern.Owner as IPartOfSpeech; + pos.ReferenceFormsOC.Add(copy); + pattern.SetCloneProperties(copy); + IMoMorphSynAnalysis msa = ls.MorphoSyntaxAnalysisRA; + IFsFeatStruc values = null; + if (msa is IMoStemMsa moStemMsa) + { + values = moStemMsa.MsFeaturesOA; + } + if (msa is IMoInflAffMsa moInflAffMsa) + { + values = moInflAffMsa.InflFeatsOA; + } + var newCopy = copy.FillInBlanks(values); + if (newCopy == null) + { + // Only had empty blanks. + pos.ReferenceFormsOC.Remove(copy); + return null; + } + foreach (var fs in pos.ReferenceFormsOC) + { + if (fs != copy && fs.IsEquivalent(copy)) + { + // Use existing fs instead of new copy. + pos.ReferenceFormsOC.Remove(copy); + return fs; + } + } + return copy; + } + + private IMoStemMsa GetMsmTarget(IFsFeatStruc fsTarget, ILexEntry entry, HashSet sensesToChange, IPartOfSpeech pos) + { + IMoStemMsa msmTarget = entry.MorphoSyntaxAnalysesOC.OfType() + .FirstOrDefault(msm => MsaMatchesTarget(msm, fsTarget)); + + if (msmTarget == null) + { + // See if we can reuse an existing MoStemMsa by changing it. + // This is possible if it is used only by senses in the list, or not used at all. + var otherSenses = new HashSet(); + var senses = new HashSet(entry.AllSenses.ToArray()); + if (senses.Count != sensesToChange.Count) { - // Nothing we can reuse...make a new one. - msmTarget = m_cache.ServiceLocator.GetInstance().Create(); - entry.MorphoSyntaxAnalysesOC.Add(msmTarget); - InitMsa(msmTarget, pos.Hvo); + otherSenses = new HashSet(senses.Where(ls => !sensesToChange.Contains(ls))); } - // Finally! Make the senses we want to change use it. - foreach (var ls in sensesToChange) + + var msm = entry.MorphoSyntaxAnalysesOC + .OfType() // filter only IMoStemMsa + .FirstOrDefault(msa => !otherSenses.Any(ls => ls.MorphoSyntaxAnalysisRA == msa)); + + if (msm != null) { - ls.MorphoSyntaxAnalysisRA = msmTarget; + // Can reuse this one! Nothing we don't want to change uses it. + // Adjust its POS as well as its inflection feature, just to be sure. + // Ensure that we don't change the POS! See LT-6835. + msmTarget = msm; + InitMsa(msmTarget, msm.PartOfSpeechRA.Hvo, fsTarget); } } - m_cache.DomainDataByFlid.EndUndoTask(); + if (msmTarget == null && pos != null) + { + // Nothing we can reuse...make a new one. + msmTarget = m_cache.ServiceLocator.GetInstance().Create(); + entry.MorphoSyntaxAnalysesOC.Add(msmTarget); + InitMsa(msmTarget, pos.Hvo, fsTarget); + } + return msmTarget; } /// @@ -428,10 +481,9 @@ public void SetClearField() throw new NotImplementedException(); } - private void InitMsa(IMoStemMsa msmTarget, int hvoPos) + private void InitMsa(IMoStemMsa msmTarget, int hvoPos, IFsFeatStruc newFeatures) { msmTarget.PartOfSpeechRA = m_cache.ServiceLocator.GetObject(hvoPos) as IPartOfSpeech;//var newFeatures = (IFsFeatStruc)m_cache.ServiceLocator.GetObject(m_selectedHvo); - var newFeatures = m_selectedHvo == 0 ? null : (IFsFeatStruc)m_cache.ServiceLocator.GetObject(m_selectedHvo); if (newFeatures == null) { msmTarget.MsFeaturesOA = null; @@ -480,25 +532,44 @@ public void FakeDoit(IEnumerable itemsToChange, int tagFakeFlid, int tagEna { CheckDisposed(); - ITsString tss = TsStringUtils.MakeString(m_selectedLabel, m_cache.DefaultAnalWs); + IFsFeatStruc fsTarget = null; + if (m_selectedHvo != 0) + fsTarget = Cache.ServiceLocator.GetInstance().GetObject(m_selectedHvo); // Build a Set of parts of speech that can take this class. HashSet possiblePOS = GetPossiblePartsOfSpeech(); int i = 0; // Report progress 50 times or every 100 items, whichever is more (but no more than once per item!) int interval = Math.Min(100, Math.Max(itemsToChange.Count() / 50, 1)); - foreach (int hvo in itemsToChange) + // FillInBlanks can add feature structures to IPartOfSpeech.ReferenceFormsOC. + // These feature structures don't hurt anything, so we make the work non-undoable. + NonUndoableUnitOfWorkHelper.Do(m_cache.ServiceLocator.GetInstance(), () => { - i++; - if (i % interval == 0) + foreach (int hvo in itemsToChange) { - state.PercentDone = i * 100 / itemsToChange.Count(); - state.Breath(); + i++; + if (i % interval == 0) + { + state.PercentDone = i * 100 / itemsToChange.Count(); + state.Breath(); + } + ITsString tss = TsStringUtils.MakeString(m_selectedLabel, m_cache.DefaultAnalWs); + IFsFeatStruc newFsTarget = fsTarget; + if (fsTarget != null && fsTarget.ContainsBlank()) + { + // Create a new fsTarget by filling in fsTarget's blanks using the lex sense's feature structure. + ILexSense ls = Cache.ServiceLocator.GetInstance().GetObject(hvo); + newFsTarget = FillInBlanks(fsTarget, ls); + string fsLabel = newFsTarget == null ? "" : newFsTarget.ShortName; + tss = TsStringUtils.MakeString(fsLabel, m_cache.DefaultAnalWs); + } + bool fEnable = IsItemEligible(m_sda, hvo, possiblePOS, newFsTarget); + if (fEnable) + { + m_sda.SetString(hvo, tagFakeFlid, tss); + } + m_sda.SetInt(hvo, tagEnable, (fEnable ? 1 : 0)); } - bool fEnable = IsItemEligible(m_sda, hvo, possiblePOS); - if (fEnable) - m_sda.SetString(hvo, tagFakeFlid, tss); - m_sda.SetInt(hvo, tagEnable, (fEnable ? 1 : 0)); - } + }); } /// @@ -519,7 +590,7 @@ public List FieldPath } } - private bool IsItemEligible(ISilDataAccess sda, int hvo, HashSet possiblePOS) + private bool IsItemEligible(ISilDataAccess sda, int hvo, HashSet possiblePOS, IFsFeatStruc fsTarget) { bool fEnable = false; int hvoMsa = sda.get_ObjectProp(hvo, LexSenseTags.kflidMorphoSyntaxAnalysis); @@ -528,14 +599,15 @@ private bool IsItemEligible(ISilDataAccess sda, int hvo, HashSet possiblePO if (hvoMsa != 0) { int clsid = m_cache.ServiceLocator.GetInstance().GetObject(hvoMsa).ClassID; + if (clsid == MoStemMsaTags.kClassId) { int pos = sda.get_ObjectProp(hvoMsa, MoStemMsaTags.kflidPartOfSpeech); if (m_notSure || (pos != 0 && possiblePOS.Contains(pos))) { // Only show it as a change if it is different - int hvoFeature = sda.get_ObjectProp(hvoMsa, MoStemMsaTags.kflidMsFeatures); - fEnable = hvoFeature != m_selectedHvo; + IMoStemMsa msa = m_cache.ServiceLocator.GetInstance().GetObject(hvoMsa); + fEnable = !EquivalentFs(fsTarget, msa?.MsFeaturesOA); } } if (clsid == MoInflAffMsaTags.kClassId) @@ -544,14 +616,23 @@ private bool IsItemEligible(ISilDataAccess sda, int hvo, HashSet possiblePO if (m_notSure || (pos != 0 && possiblePOS.Contains(pos))) { // Only show it as a change if it is different - int hvoFeature = sda.get_ObjectProp(hvoMsa, MoInflAffMsaTags.kflidInflFeats); - fEnable = hvoFeature != m_selectedHvo; + IMoInflAffMsa msa = m_cache.ServiceLocator.GetInstance().GetObject(hvoMsa); + fEnable = !EquivalentFs(fsTarget, msa?.InflFeatsOA); } } } return fEnable; } + private bool EquivalentFs(IFsFeatStruc fs1, IFsFeatStruc fs2) + { + if (fs1 == null) + return fs1 == fs2; + if (fs2 == null) + return false; + return fs1.IsEquivalent(fs2); + } + private IPartOfSpeech GetPOS() { ISilDataAccess sda = m_cache.DomainDataByFlid; diff --git a/Src/LexText/LexTextControls/FeatureStructureTreeView.cs b/Src/LexText/LexTextControls/FeatureStructureTreeView.cs index b793507122..b9524ebd10 100644 --- a/Src/LexText/LexTextControls/FeatureStructureTreeView.cs +++ b/Src/LexText/LexTextControls/FeatureStructureTreeView.cs @@ -139,6 +139,10 @@ private void AddNode(IFsFeatDefn defn, FeatureTreeNode parentNode) { AddNode(val, newNode); } + FeatureTreeNode unknownNode = new FeatureTreeNode(LexTextControls.ksPreserveExistingValues, + (int)ImageKind.radio, (int)ImageKind.radio, 0, FeatureTreeNodeInfo.NodeKind.SymFeatValue); + InsertNode(unknownNode, newNode); + HandleCheckBoxNodes(null, unknownNode); } } var complex = defn as IFsComplexFeature; @@ -327,7 +331,8 @@ private void HandleCheckBoxNodes(TreeView tv, FeatureTreeNode tn) sibling = (FeatureTreeNode)sibling.NextNode; } } - tv.Invalidate(); + if (tv != null) + tv.Invalidate(); } // m_lastSelectedTreeNode = tn; } diff --git a/Src/LexText/LexTextControls/LexTextControls.Designer.cs b/Src/LexText/LexTextControls/LexTextControls.Designer.cs index 2d01b3686c..8611971f6b 100644 --- a/Src/LexText/LexTextControls/LexTextControls.Designer.cs +++ b/Src/LexText/LexTextControls/LexTextControls.Designer.cs @@ -2361,6 +2361,15 @@ internal static string ksPossibleInvalidFile { } } + /// + /// Looks up a localized string similar to Preserve existing values. + /// + internal static string ksPreserveExistingValues { + get { + return ResourceManager.GetString("ksPreserveExistingValues", resourceCulture); + } + } + /// /// Looks up a localized string similar to Preview Import Summary. /// diff --git a/Src/LexText/LexTextControls/LexTextControls.resx b/Src/LexText/LexTextControls/LexTextControls.resx index 4d8b9e1e28..8f771e65a0 100644 --- a/Src/LexText/LexTextControls/LexTextControls.resx +++ b/Src/LexText/LexTextControls/LexTextControls.resx @@ -435,6 +435,9 @@ None of the above + + Preserve existing values + No senses in entry diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/MsaInflectionFeatureListDlgTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/MsaInflectionFeatureListDlgTests.cs index 07b1f34138..1b1b62c180 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/MsaInflectionFeatureListDlgTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/MsaInflectionFeatureListDlgTests.cs @@ -124,7 +124,7 @@ public void PopulateTreeFromFeatureSystem() Assert.AreEqual(2, tv.Nodes.Count, "Count of top level nodes in tree view"); TreeNodeCollection col = tv.Nodes[0].Nodes; - Assert.AreEqual(3, col.Count, "Count of first level nodes in tree view"); + Assert.AreEqual(4, col.Count, "Count of first level nodes in tree view"); } } @@ -179,7 +179,7 @@ private void LoadFeatureValuesIntoTreeview(FeatureStructureTreeView tv, IFsFeatS { TreeNodeCollection col2 = node.Nodes; if (node.Text == "gender") - Assert.AreEqual(2, col2.Count, "Count of second level nodes in tree view"); + Assert.AreEqual(3, col2.Count, "Count of second level nodes in tree view"); if (node.Text == "person") Assert.AreEqual(1, col2.Count, "Count of second level nodes in tree view"); } @@ -198,7 +198,7 @@ private FeatureStructureTreeView SetUpSampleData(out IFsFeatStruc featStruct) foreach (TreeNode node in col) { TreeNodeCollection col2 = node.Nodes; - Assert.AreEqual(2, col2.Count, "Count of second level nodes in tree view"); + Assert.AreEqual(3, col2.Count, "Count of second level nodes in tree view"); if (node.PrevNode == null) node.Checked = true; } diff --git a/Src/LexText/LexTextControls/MsaInflectionFeatureListDlg.cs b/Src/LexText/LexTextControls/MsaInflectionFeatureListDlg.cs index 628a1aed00..d4f2d15b50 100644 --- a/Src/LexText/LexTextControls/MsaInflectionFeatureListDlg.cs +++ b/Src/LexText/LexTextControls/MsaInflectionFeatureListDlg.cs @@ -650,7 +650,7 @@ private bool CheckFeatureStructure(TreeNodeCollection col) if (CheckFeatureStructure(tn.Nodes)) return true; } - else if (tn.Chosen && (0 != tn.Hvo)) + else if (tn.Chosen && (0 != tn.Hvo || tn.Kind == FeatureTreeNodeInfo.NodeKind.SymFeatValue)) { return true; } @@ -671,7 +671,7 @@ public void UpdateFeatureStructure(TreeNodeCollection col) { if (tn.Nodes.Count > 0) UpdateFeatureStructure(tn.Nodes); - else if (tn.Chosen && (0 != tn.Hvo)) + else if (tn.Chosen && (0 != tn.Hvo || tn.Kind == FeatureTreeNodeInfo.NodeKind.SymFeatValue)) { var fs = m_fs; IFsFeatureSpecification val = null; @@ -726,7 +726,7 @@ private void BuildFeatureStructure(FeatureTreeNode node, ref IFsFeatStruc fs, re break; case FeatureTreeNodeInfo.NodeKind.SymFeatValue: var closed = val as IFsClosedValue; - if (closed != null) + if (closed != null && node.Hvo != 0) closed.ValueRA = m_cache.ServiceLocator.GetInstance().GetObject(node.Hvo); break; } diff --git a/Src/LexText/LexTextControls/PopupTreeManager.cs b/Src/LexText/LexTextControls/PopupTreeManager.cs index d16e7544f4..d02bfa4bdc 100644 --- a/Src/LexText/LexTextControls/PopupTreeManager.cs +++ b/Src/LexText/LexTextControls/PopupTreeManager.cs @@ -520,16 +520,16 @@ protected void SelectChosenItem(TreeNode item, PopupTree popupTree) { CheckDisposed(); - if (item != null) - { - // We do NOT want to simulate a mouse click because that will cause the - // text box in the combo to be focused. We may be updating this from a PropChanged - // that should not set focus. - popupTree.SelectByAction = TreeViewAction.Unknown; - popupTree.SelectedNode = item; - if (m_treeCombo != null) - m_treeCombo.SetComboText(item); - } + if (item == null) + item = m_kEmptyNode; + + // We do NOT want to simulate a mouse click because that will cause the + // text box in the combo to be focused. We may be updating this from a PropChanged + // that should not set focus. + popupTree.SelectByAction = TreeViewAction.Unknown; + popupTree.SelectedNode = item; + if (m_treeCombo != null) + m_treeCombo.SetComboText(item); } ///