I cannot give you any concrete help but in terms of architecture you need to change your layout from this
To this
Everything else is a hack. Your unit/glyph must become a word-chord-pair.
Edit: I have been fooling around with a templated ItemsControl and it even works out to some degree, so it might be of interest.
<ItemsControl Grid.IsSharedSizeScope="True" ItemsSource="{Binding SheetData}"
Name="_chordEditor">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition SharedSizeGroup="A" Height="Auto"/>
<RowDefinition SharedSizeGroup="B" Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Children>
<TextBox Name="chordTB" Grid.Row="0" Text="{Binding Chord}"/>
<TextBox Name="wordTB" Grid.Row="1" Text="{Binding Word}"
PreviewKeyDown="Glyph_Word_KeyDown" TextChanged="Glyph_Word_TextChanged"/>
</Grid.Children>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
private readonly ObservableCollection<ChordWordPair> _sheetData = new ObservableCollection<ChordWordPair>();
public ObservableCollection<ChordWordPair> SheetData
{
get { return _sheetData; }
}
public class ChordWordPair: INotifyPropertyChanged
{
private string _chord = String.Empty;
public string Chord
{
get { return _chord; }
set
{
if (_chord != value)
{
_chord = value;
// This uses some reflection extension method,
// a normal event raising method would do just fine.
PropertyChanged.Notify(() => this.Chord);
}
}
}
private string _word = String.Empty;
public string Word
{
get { return _word; }
set
{
if (_word != value)
{
_word = value;
PropertyChanged.Notify(() => this.Word);
}
}
}
public ChordWordPair() { }
public ChordWordPair(string word, string chord)
{
Word = word;
Chord = chord;
}
public event PropertyChangedEventHandler PropertyChanged;
}
private void AddNewGlyph(string text, int index)
{
var glyph = new ChordWordPair(text, String.Empty);
SheetData.Insert(index, glyph);
FocusGlyphTextBox(glyph, false);
}
private void FocusGlyphTextBox(ChordWordPair glyph, bool moveCaretToEnd)
{
var cp = _chordEditor.ItemContainerGenerator.ContainerFromItem(glyph) as ContentPresenter;
Action focusAction = () =>
{
var grid = VisualTreeHelper.GetChild(cp, 0) as Grid;
var wordTB = grid.Children[1] as TextBox;
Keyboard.Focus(wordTB);
if (moveCaretToEnd)
{
wordTB.CaretIndex = int.MaxValue;
}
};
if (!cp.IsLoaded)
{
cp.Loaded += (s, e) => focusAction.Invoke();
}
else
{
focusAction.Invoke();
}
}
private void Glyph_Word_TextChanged(object sender, TextChangedEventArgs e)
{
var glyph = (sender as FrameworkElement).DataContext as ChordWordPair;
var tb = sender as TextBox;
string[] glyphs = tb.Text.Split(' ');
if (glyphs.Length > 1)
{
glyph.Word = glyphs[0];
for (int i = 1; i < glyphs.Length; i++)
{
AddNewGlyph(glyphs[i], SheetData.IndexOf(glyph) + i);
}
}
}
private void Glyph_Word_KeyDown(object sender, KeyEventArgs e)
{
var tb = sender as TextBox;
var glyph = (sender as FrameworkElement).DataContext as ChordWordPair;
if (e.Key == Key.Left && tb.CaretIndex == 0 || e.Key == Key.Back && tb.Text == String.Empty)
{
int i = SheetData.IndexOf(glyph);
if (i > 0)
{
var leftGlyph = SheetData[i - 1];
FocusGlyphTextBox(leftGlyph, true);
e.Handled = true;
if (e.Key == Key.Back) SheetData.Remove(glyph);
}
}
if (e.Key == Key.Right && tb.CaretIndex == tb.Text.Length)
{
int i = SheetData.IndexOf(glyph);
if (i < SheetData.Count - 1)
{
var rightGlyph = SheetData[i + 1];
FocusGlyphTextBox(rightGlyph, false);
e.Handled = true;
}
}
}
Initially some glyph should be added to the collection, otherwise there will be no input field (this can be avoided with further templating, e.g. by using a datatrigger that shows a field if the collection is empty).
Perfecting this would require a lot of additional work like styling the TextBoxes, adding written line breaks (right now it only breaks when the wrap panel makes it), supporting selection accross multiple textboxes, etc.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…