Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 17 additions & 34 deletions Src/Common/Controls/DetailControls/PhoneEnvReferenceView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using SIL.LCModel.Core.Cellar;
using SIL.LCModel.Core.Phonology;
using SIL.LCModel.Core.Text;
using SIL.FieldWorks.Common.DetailRules;
using SIL.FieldWorks.Common.ViewsInterfaces;
using SIL.FieldWorks.Common.Framework.DetailControls.Resources;
using SIL.LCModel.Core.KernelInterfaces;
Expand Down Expand Up @@ -350,9 +351,7 @@ internal bool OnRedo(object args)
public void ResetValidator()
{
CheckDisposed();
m_validator = new PhonEnvRecognizer(
m_cache.LangProject.PhonologicalDataOA.AllPhonemes().ToArray(),
m_cache.LangProject.PhonologicalDataOA.AllNaturalClassAbbrs().ToArray());
m_validator = EnvironmentErrors.CreateRecognizer(m_cache);
}

/// <summary>
Expand Down Expand Up @@ -953,16 +952,13 @@ internal void ShowEnvironmentError()
{
CheckDisposed();
string s;
if (CanGetEnvironmentStringRep(out s))
if (!CanGetEnvironmentStringRep(out s))
return;
var message = EnvironmentErrors.Describe(m_validator, s);
if (message != null)
{
if (!m_validator.Recognize(s))
{
string sMsg;
int pos;
StringServices.CreateErrorMessageFromXml(s, m_validator.ErrorMessage, out pos, out sMsg);
MessageBox.Show(sMsg, DetailControlsStrings.ksBadEnv,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
MessageBox.Show(message, DetailControlsStrings.ksBadEnv,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}

Expand All @@ -971,10 +967,7 @@ internal bool CanShowEnvironmentError()
{
CheckDisposed();
string s;
if (CanGetEnvironmentStringRep(out s))
return (!m_validator.Recognize(s));
else
return false;
return CanGetEnvironmentStringRep(out s) && EnvironmentErrors.Describe(m_validator, s) != null;
}

private bool CanGetEnvironmentStringRep(out string s)
Expand All @@ -995,6 +988,9 @@ private bool CanGetEnvironmentStringRep(out string s)
return true;
}

// The insert rules live in EnvironmentInsertRules, shared with the Avalonia row; this
// view supplies the selected environment's text and selection.

internal bool CanInsertSlash()
{
CheckDisposed();
Expand All @@ -1005,12 +1001,10 @@ internal bool CanInsertSlash()
IVwSelection vwsel;
if (!GetSelectedStringRep(out tss, out vwsel, out hvoDummyObj, out ichAnchor, out ichEnd))
return false;
// A selection off every environment (the empty line at the end) starts a new one.
if (tss == null || hvoDummyObj == 0)
return true;
string s = tss.Text;
if (s == null || s == String.Empty)
return true;
return s.IndexOf('/') < 0;
return EnvironmentInsertRules.CanInsertSlash(tss.Text);
}

internal bool CanInsertEnvBar()
Expand All @@ -1025,12 +1019,7 @@ internal bool CanInsertEnvBar()
return false;
if (tss == null || hvoDummyObj == 0)
return false;
string s = tss.Text;
if (s == null || s == String.Empty)
return false;
int ichSlash = s.IndexOf('/');
return (ichSlash >= 0) && (ichEnd > ichSlash) && (ichAnchor > ichSlash) &&
(s.IndexOf('_') < 0);
return EnvironmentInsertRules.CanInsertBar(tss.Text, ichAnchor, ichEnd);
}

internal bool CanInsertItem()
Expand All @@ -1045,10 +1034,7 @@ internal bool CanInsertItem()
return false;
if (tss == null || hvoDummyObj == 0)
return false;
string s = tss.Text;
if (s == null || s == String.Empty)
return false;
return PhonEnvRecognizer.CanInsertItem(s, ichEnd, ichAnchor);
return EnvironmentInsertRules.CanInsertItem(tss.Text, ichAnchor, ichEnd);
}

internal bool CanInsertHashMark()
Expand All @@ -1063,10 +1049,7 @@ internal bool CanInsertHashMark()
return false;
if (tss == null || hvoDummyObj == 0)
return false;
string s = tss.Text;
if (s == null || s == String.Empty)
return false;
return PhonEnvRecognizer.CanInsertHashMark(s, ichEnd, ichAnchor);
return EnvironmentInsertRules.CanInsertHashMark(tss.Text, ichAnchor, ichEnd);
}
#endregion

Expand Down
43 changes: 27 additions & 16 deletions Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,30 @@ public void SetHelpTopic(string helpTopic)
/// <returns></returns>
public static bool ChooseNaturalClass(IVwRootBox rootb, LcmCache cache,
IPersistenceProvider persistenceProvider, Mediator mediator, PropertyTable propertyTable)
{
var pnc = ChooseNaturalClass(cache, persistenceProvider, mediator, propertyTable);
if (pnc != null)
{
ITsString tss = pnc.Abbreviation.BestAnalysisVernacularAlternative;
string sIns = String.Format("[{0}]", tss.Text);
int wsPending = cache.DefaultVernWs;
IVwRootSite site = rootb.Site;
IVwGraphics vg = null;
if (site != null)
vg = site.get_ScreenGraphics(rootb);
rootb.OnTyping(vg, sIns, VwShiftStatus.kfssNone, ref wsPending);
}
return true;
}

/// <summary>
/// Bring up a chooser for selecting a natural class. The Avalonia environment menus
/// insert the chosen class into their own editor.
/// </summary>
/// <returns>The chosen class; null when the user cancelled or took the chooser's jump
/// link.</returns>
public static IPhNaturalClass ChooseNaturalClass(LcmCache cache,
IPersistenceProvider persistenceProvider, Mediator mediator, PropertyTable propertyTable)
{
IEnumerable<ObjectLabel> labels = ObjectLabel.CreateObjectLabels(cache,
cache.LanguageProject.PhonologicalDataOA.NaturalClassesOS, "",
Expand Down Expand Up @@ -2627,24 +2651,11 @@ public static bool ChooseNaturalClass(IVwRootBox rootb, LcmCache cache,

DialogResult res = chooser.ShowDialog();
if (DialogResult.Cancel == res)
return true;
return null;
if (chooser.HandleAnyJump())
return true;
if (chooser.ChosenOne != null)
{
var pnc = (IPhNaturalClass) chooser.ChosenOne.Object;
ITsString tss = pnc.Abbreviation.BestAnalysisVernacularAlternative;
string sName = tss.Text;
string sIns = String.Format("[{0}]", sName);
int wsPending = cache.DefaultVernWs;
IVwRootSite site = rootb.Site;
IVwGraphics vg = null;
if (site != null)
vg = site.get_ScreenGraphics(rootb);
rootb.OnTyping(vg, sIns, VwShiftStatus.kfssNone, ref wsPending);
}
return null;
return chooser.ChosenOne?.Object as IPhNaturalClass;
}
return true;
}

/// <summary>
Expand Down
65 changes: 58 additions & 7 deletions Src/Common/FwAvalonia/Detail/DataTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ private FieldContent AddField(int row, DetailField field)
// The field's slice menu opens from the label cell's right-click or the
// gutter "..." button; the editor's current item rides each request it raises.
var labelCell = WrapWithFieldMenu(labelBlock, field, automationId, out var labelKebab,
editor as IDetailItemSelection);
editor as IDetailItemSelection, editor);

// Hover-reveal: the WHOLE row (label cell + editor) is the hover/focus
// surface for the field-options "..." and any editor affordance (chooser
Expand Down Expand Up @@ -738,7 +738,7 @@ private void OnVectorSelectionChanged(object sender, EventArgs e)
// The label cell answers context-menu requests with the row's slice menu; the
// kebab opens its own menu or hotlinks. With no host bridge, content is unwrapped.
private Control WrapWithFieldMenu(Control inner, DetailField field, string automationId,
out Control kebab, IDetailItemSelection selection = null)
out Control kebab, IDetailItemSelection selection = null, Control editor = null)
{
kebab = null;
if (_menuRequested == null)
Expand All @@ -765,14 +765,36 @@ private Control WrapWithFieldMenu(Control inner, DetailField field, string autom
AutomationProperties.SetName(button, FwAvaloniaStrings.FieldOptionsMenu);
ToolTip.SetTip(button, FwAvaloniaStrings.FieldOptionsMenu);
var kind = hasMenu ? DetailMenuKind.SliceMenu : DetailMenuKind.Hotlinks;
// Button.Click fires for both a mouse click and keyboard activation (Enter/Space), so the
// affordance is fully keyboard-operable once Tab focus reveals it.
// Click fires for keyboard activation (Enter/Space) once Tab focus reveals the
// button; a pointer click is answered by the rail handlers below instead.
button.Click += (s, e) =>
{
// No pointer position is available here, so the menu drops from the
// icon rather than from wherever the mouse sits.
_menuRequested(DetailMenuRequest.FromAnchor(button, field, kind, selection));
};
// The press is swallowed before the button can take focus from the row's
// editor (as under SliceTreeNode's icon); the release raises the menu.
var pressedOnButton = false;
rail.AddHandler(Avalonia.Input.InputElement.PointerPressedEvent, (s, e) =>
{
if (!IsOn(button, e.Source) || !e.GetCurrentPoint(button).Properties.IsLeftButtonPressed)
return;
pressedOnButton = true;
e.Handled = true;
TakeRowFocus(rail, editor, button);
}, Avalonia.Interactivity.RoutingStrategies.Tunnel);
rail.AddHandler(Avalonia.Input.InputElement.PointerReleasedEvent, (s, e) =>
{
if (!pressedOnButton)
return;
pressedOnButton = false;
e.Handled = true;
_menuRequested(DetailMenuRequest.FromAnchor(button, field, kind, selection));
}, Avalonia.Interactivity.RoutingStrategies.Tunnel);
// A press whose release never arrives (capture lost mid-press) opens nothing.
rail.AddHandler(Avalonia.Input.InputElement.PointerCaptureLostEvent,
(s, e) => pressedOnButton = false, Avalonia.Interactivity.RoutingStrategies.Tunnel);
rail.Child = button;
kebab = button;
}
Expand All @@ -781,19 +803,48 @@ private Control WrapWithFieldMenu(Control inner, DetailField field, string autom
DockPanel.SetDock(rail, Dock.Left);
wrapper.Children.Add(rail);
wrapper.Children.Add(inner); // fills the width remaining after the gutter
WireLabelContextMenu(wrapper, field, selection);
WireLabelContextMenu(wrapper, field, selection, editor, kebab);
return wrapper;
}

// Whether an event source is the control or something drawn inside it.
private static bool IsOn(Control control, object source)
=> source is Visual visual
&& (ReferenceEquals(visual, control)
|| Avalonia.VisualTree.VisualExtensions.IsVisualAncestorOf(control, visual));

// A label gesture focuses its row (Slice.TakeFocus), so the menu closes back onto it;
// a vector row takes it on its button, since focusing an item would make it current.
private static void TakeRowFocus(Control cell, Control editor, Control kebab)
{
if (editor == null)
return;
var focused = TopLevel.GetTopLevel(cell)?.FocusManager?.GetFocusedElement();
if (IsOn(cell, focused) || IsOn(editor, focused))
return;
Control target;
if (editor is IDetailItemSelection)
target = kebab;
else if (editor.Focusable)
target = editor;
else
{
target = Avalonia.VisualTree.VisualExtensions.GetVisualDescendants(editor).OfType<Control>()
.FirstOrDefault(c => c.Focusable && c.IsEffectivelyEnabled && c.IsEffectivelyVisible);
}
target?.Focus();
}

/// <summary>
/// Wires up the Label context menu for a slice; the request carries the row editor's
/// current item when it keeps one.
/// current item when it keeps one, and the row takes focus first.
/// </summary>
private void WireLabelContextMenu(Control cell, DetailField field,
IDetailItemSelection selection)
IDetailItemSelection selection, Control editor, Control kebab)
{
cell.AddHandler(Control.ContextRequestedEvent, (s, e) =>
{
TakeRowFocus(cell, editor, kebab);
_menuRequested(DetailMenuRequest.FromContextRequested(cell, e, field,
DetailMenuKind.SliceMenu, selection));
e.Handled = true;
Expand Down
4 changes: 4 additions & 0 deletions Src/Common/FwAvalonia/Detail/DetailMenuFlyout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ private DetailMenuItem()

public static DetailMenuItem Separator() => new DetailMenuItem();

/// <summary>A leaf that is shown but cannot run.</summary>
public static DetailMenuItem Disabled(string label)
=> new DetailMenuItem(label, isEnabled: false, isChecked: false, children: null, execute: null);

public string Label { get; }
public bool IsEnabled { get; }
public bool IsChecked { get; }
Expand Down
Loading
Loading