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);
}
///