Skip to content

Commit a31e54a

Browse files
mark-silclaude
andauthored
Stop NavBarAdapter's own selections from re-setting properties (#1209)
NavBarAdapter now reflects an areaChoice or ToolForAreaNamed_ change in the side pane without the TabClicked and ItemClicked events that the selection raises running the tab's or item's choice. Until now those events, raised by SidePane.SelectTab and SelectItem exactly as a user click raises them, re-ran ChoiceGroup's property sets from inside the OnPropertyChanged handler, including a set for the first item of a tab on its first visit, which the remembered tool's set then overwrote. The sets the re-run made, which no longer happen: * areaChoiceParameters * areaChoice * currentContentControlParameters * currentContentControl Every value was the one already in place, except areaChoiceParameters, which the first programmatic SelectTab of a session created with no listener alive. A user click on a tab or item still runs its choice. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 380cc90 commit a31e54a

2 files changed

Lines changed: 218 additions & 5 deletions

File tree

‎Src/XCore/FlexUIAdapter/NavBarAdapter.cs‎

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class SidebarAdapter : AdapterBase, IxCoreColleague, IDisposable
2626
private ChoiceGroupCollection m_choiceGroupCollectionCache; // cache of what was received by CreateUIForChoiceGroupCollection()
2727
private List<ChoiceGroup> m_choiceGroupCache = new List<ChoiceGroup>(); // cache of what was received by CreateUIForChoiceGroup()
2828
private string areasLabel; // label of the special case ChoiceGroup for Areas
29+
private bool m_selectingInSidePane; // true while the adapter's own SelectTab/SelectItem runs
2930

3031
#region Disposable stuff
3132
#if DEBUG
@@ -141,27 +142,50 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu
141142
}
142143

143144
/// <summary>
144-
/// Handle when an item on the sidepane is clicked.
145+
/// Handle when an item on the sidepane is clicked. A selection the adapter itself makes
146+
/// while reflecting a property change raises the same event and is ignored here.
145147
/// </summary>
146148
private void SidePaneItemClickedHandler(Item itemClicked)
147149
{
150+
if (m_selectingInSidePane)
151+
return;
148152
var choice = itemClicked.Tag as ChoiceBase;
149153
Debug.Assert(choice != null, "SidePaneItemClickedHandler received an Item with an invalid Tag.");
150154
if (choice != null)
151155
choice.OnClick(this, null);
152156
}
153157

154158
/// <summary>
155-
/// Handle when a tab on the sidepane is clicked.
159+
/// Handle when a tab on the sidepane is clicked. A selection the adapter itself makes
160+
/// while reflecting a property change raises the same event and is ignored here.
156161
/// </summary>
157162
private void SidePaneTabClickedHandler(Tab tabClicked)
158163
{
164+
if (m_selectingInSidePane)
165+
return;
159166
var choice = tabClicked.Tag as ChoiceBase;
160167
Debug.Assert(choice != null, "SidePaneTabClickedHandler received a Tab with an invalid Tag.");
161168
if (choice != null)
162169
choice.OnClick(this, null);
163170
}
164171

172+
/// <summary>
173+
/// Runs one of the side pane's selection calls with the click handlers silenced, so
174+
/// that showing a property's value does not set that property again.
175+
/// </summary>
176+
private bool SelectInSidePane(Func<bool> select)
177+
{
178+
m_selectingInSidePane = true;
179+
try
180+
{
181+
return select();
182+
}
183+
finally
184+
{
185+
m_selectingInSidePane = false;
186+
}
187+
}
188+
165189
public override void PersistLayout()
166190
{
167191
// use MyPanel
@@ -555,12 +579,12 @@ public void OnPropertyChanged(string propertyName)
555579
// overwriting the property table value. See FWR-2004.
556580
string propToolForArea = "ToolForAreaNamed_" + areaName;
557581
string toolForArea = m_propertyTable.GetStringProperty(propToolForArea, null);
558-
m_sidepane.SelectTab(tab);
582+
SelectInSidePane(() => m_sidepane.SelectTab(tab));
559583
// FWR-2895 Deleting a Custom list could result in needing to
560584
// update the PropertyTable.
561585
if (!String.IsNullOrEmpty(toolForArea) && !toolForArea.StartsWith("?"))
562586
{
563-
var fsuccess = m_sidepane.SelectItem(tab, toolForArea);
587+
var fsuccess = SelectInSidePane(() => m_sidepane.SelectItem(tab, toolForArea));
564588
if (!fsuccess)
565589
{
566590
m_propertyTable.SetProperty(propToolForArea, null, true);
@@ -605,7 +629,7 @@ private void SetToolForCurrentArea(string tool)
605629
// calling the SelectItem if it is already the CurrentItem.
606630
if(m_sidepane.CurrentItem != item)
607631
{
608-
m_sidepane.SelectItem(tab, item.Name);
632+
SelectInSidePane(() => m_sidepane.SelectItem(tab, item.Name));
609633
}
610634
break;
611635
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) 2026 SIL International
2+
// This software is licensed under the LGPL, version 2.1 or later
3+
// (http://www.gnu.org/licenses/lgpl-2.1.html)
4+
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Windows.Forms;
9+
using System.Xml;
10+
using NUnit.Framework;
11+
using SIL.SilSidePane;
12+
13+
namespace XCore
14+
{
15+
/// <summary>
16+
/// How <see cref="SidebarAdapter"/> reflects areaChoice and ToolForAreaNamed_ changes in
17+
/// the side pane without setting those properties again.
18+
/// </summary>
19+
[TestFixture]
20+
[Apartment(ApartmentState.STA)]
21+
public class SidebarAdapterSelectionTests
22+
{
23+
/// <summary>
24+
/// Two areas with two tools each, in the shape of the sidebar section of Main.xml.
25+
/// </summary>
26+
private const string SidebarXml =
27+
"<root>"
28+
+ "<lists>"
29+
+ "<list id=\"AreasList\">"
30+
+ "<item label=\"Lexicon\" value=\"lexicon\"><parameters><panels>"
31+
+ "<listPanel label=\"Lexical Tools\" listId=\"LexicalToolsList\"/></panels></parameters></item>"
32+
+ "<item label=\"Grammar\" value=\"grammar\"><parameters><panels>"
33+
+ "<listPanel label=\"Grammar Tools\" listId=\"GrammarToolsList\"/></panels></parameters></item>"
34+
+ "</list>"
35+
+ "<list id=\"LexicalToolsList\">"
36+
+ "<item label=\"Lexicon Edit\" value=\"lexiconEdit\"><control/></item>"
37+
+ "<item label=\"Browse\" value=\"lexiconBrowse\"><control/></item>"
38+
+ "</list>"
39+
+ "<list id=\"GrammarToolsList\">"
40+
+ "<item label=\"Category Edit\" value=\"posEdit\"><control/></item>"
41+
+ "<item label=\"Category Browse\" value=\"posBrowse\"><control/></item>"
42+
+ "</list>"
43+
+ "</lists>"
44+
+ "<sidebar>"
45+
+ "<tab label=\"Areas\" list=\"AreasList\" behavior=\"singlePropertyAtomicValue\" property=\"areaChoice\"/>"
46+
+ "<tab label=\"Lexical Tools\" list=\"LexicalToolsList\" behavior=\"singlePropertyAtomicValue\""
47+
+ " property=\"currentContentControl\"/>"
48+
+ "<tab label=\"Grammar Tools\" list=\"GrammarToolsList\" behavior=\"singlePropertyAtomicValue\""
49+
+ " property=\"currentContentControl\"/>"
50+
+ "</sidebar>"
51+
+ "</root>";
52+
53+
private Form m_window;
54+
private Mediator m_mediator;
55+
private PropertyTable m_propertyTable;
56+
private ImageCollection m_smallImages;
57+
private ImageCollection m_largeImages;
58+
private SidebarAdapter m_adapter;
59+
private Control m_panel;
60+
private ChoiceGroupCollection m_groups;
61+
62+
[SetUp]
63+
public void SetUp()
64+
{
65+
m_window = new Form();
66+
m_mediator = new Mediator();
67+
m_propertyTable = new PropertyTable(m_mediator);
68+
m_smallImages = new ImageCollection(false);
69+
m_largeImages = new ImageCollection(true);
70+
m_adapter = new SidebarAdapter();
71+
m_panel = m_adapter.Init(m_window, m_smallImages, m_largeImages, m_mediator, m_propertyTable);
72+
m_window.Controls.Add(m_panel);
73+
74+
var doc = new XmlDocument();
75+
doc.LoadXml(SidebarXml);
76+
m_groups = new ChoiceGroupCollection(m_mediator, m_propertyTable, m_adapter,
77+
doc.DocumentElement.SelectSingleNode("sidebar"));
78+
m_groups.Init();
79+
m_adapter.FinishInit();
80+
CreateHandles(m_window);
81+
}
82+
83+
[TearDown]
84+
public void TearDown()
85+
{
86+
m_adapter.Dispose();
87+
m_window.Dispose();
88+
m_smallImages.Dispose();
89+
m_largeImages.Dispose();
90+
m_propertyTable.Dispose();
91+
m_mediator.Dispose();
92+
}
93+
94+
/// <summary>
95+
/// The side pane's list views raise their selection events only once they have a
96+
/// window handle, which a hidden child of an unshown form never gets on its own.
97+
/// </summary>
98+
private static void CreateHandles(Control control)
99+
{
100+
var unused = control.Handle;
101+
foreach (Control child in control.Controls)
102+
CreateHandles(child);
103+
}
104+
105+
private static IEnumerable<Control> AllControls(Control root)
106+
{
107+
yield return root;
108+
foreach (Control child in root.Controls)
109+
foreach (var descendant in AllControls(child))
110+
yield return descendant;
111+
}
112+
113+
/// <summary>
114+
/// Every side-pane item widget, across all the tabs' item lists. The form is never
115+
/// shown, so a list's Visible flag cannot tell which tab is current.
116+
/// </summary>
117+
private IEnumerable<ListViewItem> AllItemWidgets()
118+
{
119+
return AllControls(m_panel).OfType<ListView>().SelectMany(list => list.Items.Cast<ListViewItem>());
120+
}
121+
122+
/// <summary>
123+
/// The tool selected in the item list that holds the named tool, or null when none is.
124+
/// Each tab's list keeps its own selection, so the list is named by one of its tools.
125+
/// </summary>
126+
private string SelectedToolInTheListHolding(string tool)
127+
{
128+
var list = AllItemWidgets().First(widget => ((Item)widget.Tag).Name == tool).ListView;
129+
if (list.SelectedItems.Count == 0)
130+
return null;
131+
return ((ListPropertyChoice)((Item)list.SelectedItems[0].Tag).Tag).Value;
132+
}
133+
134+
private void ShowArea(string area, string rememberedTool)
135+
{
136+
m_propertyTable.SetProperty("areaChoice", area, false);
137+
m_propertyTable.SetProperty("ToolForAreaNamed_" + area, rememberedTool, false);
138+
m_adapter.OnPropertyChanged("areaChoice");
139+
}
140+
141+
[Test]
142+
public void AreaChoiceChange_SelectsTheRememberedToolWithoutSettingAnyProperty()
143+
{
144+
ShowArea("grammar", "posBrowse");
145+
146+
Assert.That(SelectedToolInTheListHolding("posBrowse"), Is.EqualTo("posBrowse"));
147+
Assert.That(m_mediator.JobItems, Is.EqualTo(0), "the selection must not set a property");
148+
Assert.That(m_propertyTable.PropertyExists("currentContentControl"), Is.False);
149+
Assert.That(m_propertyTable.PropertyExists("areaChoiceParameters"), Is.False);
150+
}
151+
152+
[Test]
153+
public void ReturnToAnArea_SelectsTheToolRememberedSinceTheLastVisit()
154+
{
155+
ShowArea("lexicon", "lexiconEdit");
156+
ShowArea("grammar", "posEdit");
157+
158+
ShowArea("lexicon", "lexiconBrowse");
159+
160+
Assert.That(SelectedToolInTheListHolding("lexiconBrowse"), Is.EqualTo("lexiconBrowse"));
161+
Assert.That(m_mediator.JobItems, Is.EqualTo(0), "the selection must not set a property");
162+
Assert.That(m_propertyTable.PropertyExists("currentContentControl"), Is.False);
163+
}
164+
165+
[Test]
166+
public void ToolForAreaChange_SelectsThatToolWithoutSettingAnyProperty()
167+
{
168+
ShowArea("lexicon", "lexiconEdit");
169+
m_propertyTable.SetProperty("ToolForAreaNamed_lexicon", "lexiconBrowse", false);
170+
171+
m_adapter.OnPropertyChanged("ToolForAreaNamed_lexicon");
172+
173+
Assert.That(SelectedToolInTheListHolding("lexiconBrowse"), Is.EqualTo("lexiconBrowse"));
174+
Assert.That(m_mediator.JobItems, Is.EqualTo(0), "the selection must not set a property");
175+
Assert.That(m_propertyTable.PropertyExists("currentContentControl"), Is.False);
176+
}
177+
178+
[Test]
179+
public void ItemSelectedOutsideTheAdapter_StillSetsTheProperty()
180+
{
181+
ShowArea("lexicon", "lexiconEdit");
182+
183+
AllItemWidgets().First(widget => ((Item)widget.Tag).Name == "lexiconBrowse").Selected = true;
184+
185+
Assert.That(m_propertyTable.GetStringProperty("currentContentControl", null), Is.EqualTo("lexiconBrowse"));
186+
Assert.That(m_mediator.JobItems, Is.GreaterThan(0), "a selection the adapter did not make is a click");
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)