Avalondock new tab positioning and order - wpf

I have a WPF application using Avalon Dock 2.0 as docking manager. I'm facing a problem concerning the standard positioning of new opened tabs Avalon Dock is performing.
As long as all tabs fit into the tab bar a new tab is appended on the rightmost position of the tab bar. As soon as the new tab does not fit into the bar the new tab is added at the leftmost position making the former rightmost tab dissappear.
I know this is Visual Studio standard behaviour but in my application the order has a meaning. This means a new tab should either always be added on the leftmost OR the rightmost position. The switch is very confusing for the user.
Is there a way to make Avalon Dock add a new tab always on either the leftmost or the rightmost position?

I figured it out. I changed the code in the ArrangeOverride(Size finalSize) method from DocumentPaneTabPanel to prevent the repositioning of the current tab to the first position.
It now hides tabs on the left of the current one not fitting the panel - hiding the most left ones first. The tabs on the right of the current one are hidden as well - here the most right ones are hidden first. If there is an overflow the current tab is always the rightmost on the panel. This is a bit dirty at the moment but I think implementing this in a nicer way should not be that hard.
Here is the code:
protected override Size ArrangeOverride(Size finalSize)
{
double offset = 0.0;
var children = Children.Cast<UIElement>().Where(ch => ch.Visibility != System.Windows.Visibility.Collapsed).ToList();
if (children.Count > 0)
{
//find currently selected tab
int i = 0;
for (i = 0; i < children.Count(); i++)
{
TabItem doc = (TabItem)children[i];
var layoutContent = doc.Content as LayoutContent;
if (layoutContent.IsSelected)
break;
}
//calculate how many tabs left from the currently selected would fit in the panel
int cur_ind = i;
TabItem current_item = (TabItem)children[cur_ind];
List<TabItem> t_to_display = new List<TabItem>();
while (cur_ind >= 0 && offset + current_item.DesiredSize.Width <= finalSize.Width)
{
current_item = (TabItem)children[cur_ind];
current_item.Visibility = System.Windows.Visibility.Visible;
offset += current_item.DesiredSize.Width + current_item.Margin.Left + current_item.Margin.Right;
t_to_display.Add(current_item);
--cur_ind;
}
//arrange the fitting tabs on the left
double cur_offset = offset;
foreach (TabItem t in t_to_display)
{
cur_offset -= t.DesiredSize.Width + t.Margin.Left + t.Margin.Right;
t.Arrange(new Rect(cur_offset, 0.0, t.DesiredSize.Width, finalSize.Height));
}
//arrange the tabs on the right
cur_ind = i + 1;
while (cur_ind < children.Count && offset + current_item.DesiredSize.Width <= finalSize.Width)
{
current_item = (TabItem)children[cur_ind];
current_item.Visibility = System.Windows.Visibility.Visible;
current_item.Arrange(new Rect(offset, 0.0, current_item.DesiredSize.Width, finalSize.Height));
offset += current_item.DesiredSize.Width + current_item.Margin.Left + current_item.Margin.Right;
cur_ind++;
}
while(cur_ind < children.Count)
{
current_item = (TabItem)children[cur_ind];
current_item.Visibility = System.Windows.Visibility.Hidden;
cur_ind++;
}
}
return finalSize;
}

Related

How to get exact cursor position in RichTextbox to display Popup

I Want to display POPUP specific position of RichTextBox in WPF . i have understood that there is a way to get the same in winforms RichTextBox with following lines of code.
Point point = richTextBox1.GetPositionFromCharIndex(richTextBox1.SelectionStart);
I suppose it depends on when and what you are popping up. An example from MSDN shows how to position a ContextMenu with the location of the selected text within a RichTextBox control.
How to: Position a Custom Context Menu in a RichTextBox
The interesting bit would be the code below:
TextPointer position = rtb.Selection.End;
if (position == null) return;
Rect positionRect = position.GetCharacterRect(LogicalDirection.Forward);
contextMenu.HorizontalOffset = positionRect.X;
contextMenu.VerticalOffset = positionRect.Y;
This gets the relative position of the selection. If you are popping up a form you will need to translate that into a Window position.
This is a bit of code I used to test loading a popup window over the selected text in a RichTextBox. This also takes into account multiple monitors.
TextPointer tp = txtEditor.Selection.End;
if (tp == null) return;
Rect charRect = tp.GetCharacterRect(LogicalDirection.Forward);
Point winPoint = txtEditor.PointToScreen(charRect.TopRight);
Popup p = new Popup();
p.Left = winPoint.X;
p.Top = winPoint.Y;
p.Show();
UPDATE:
I did some additional research and found a MSDN Popup Placement Behavior article that is likely what you are looking for as far as Popup behavior. You can use the code I provided above with the selection or caret position of the RichTextBox to then determine the ultimate positioning of the Popup. I hope that helps.
static void tb_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.KeyStates == ((e.KeyStates ^ System.Windows.Input.KeyStates.Down)^System.Windows.Input.KeyStates.Down))
{
if (e.Key == System.Windows.Input.Key.OemPeriod)
{
TextBox tb = (TextBox)sender;
Rect r = tb.GetRectFromCharacterIndex(tb.CaretIndex, true);
Point p = tb.TransformToAncestor(tb).Transform(new Point(r.X, r.Y + 10));
p = tb.PointToScreen(p);
Rect rect = new Rect(p.X, p.Y, 0, 0);
Grid g = (Grid)Application.Current.MainWindow.Content;
System.Windows.Controls.Primitives.Popup popup = new System.Windows.Controls.Primitives.Popup();
popup.SetValue(System.Windows.Controls.Primitives.Popup.PlacementRectangleProperty, rect);
popup.IsOpen = true;
g.Children.Add(popup);}}}

Custom panel layout doesn't work as expected when animating (WPF)

I've got a custom (and getting complex) TabControl. It's a gathering of many sources, plus my own wanted features. In it is a custom Panel to show the headers of the TabControl. Its features are to compress the size of the TabItems until they reached their minimum, and then activates scrolling features (in the Panel, again). There is also another custom panel to hold a single button, that renders on the right of the TabItems (it's a "new tab" button).
It all works great, until I try to animate the scrolling.
Here are some relevant snippets :
In the CustomTabPanel (C#, overriding Panel and implementing IScrollInfo):
private readonly TranslateTransform _translateTransform = new TranslateTransform();
public void LineLeft()
{
FirstVisibleIndex++;
var offset = HorizontalOffset + _childRects[0].Width;
if (offset < 0 || _viewPort.Width >= _extent.Width)
offset = 0;
else
{
if (offset + _viewPort.Width > _extent.Width)
offset = _extent.Width - _viewPort.Width;
}
_offset.X = offset;
if (_scrollOwner != null)
_scrollOwner.InvalidateScrollInfo();
//Animate the new offset
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset,
new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd) { AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
aScrollAnimation.Completed += ScrollAnimationCompleted;
_translateTransform.BeginAnimation(TranslateTransform.XProperty, aScrollAnimation , HandoffBehavior.SnapshotAndReplace);
//End of animation
// These lines are the only ones needed if we remove the animation
//_translateTransform.X = -offset;
//InvalidateMeasure();
}
void ScrollAnimationCompleted(object sender, EventArgs e)
{
InvalidateMeasure();
}
the _translateTransform is initialized in the constructor :
base.RenderTransform = _translateTransform;
Again, everything is fine if I remove the animation part and just replace it with the commented out lines at the end.
I must also point out that the problem is NOT with the animation itself. That part works out well. The problem is about when I remove some tab items : all the layout then screws up. The TranslateTransformation seems to hold on some wrong value, or something.
Thanks in advance.
Well. As it's often the case, I kept working on the thing, and... answered myself.
Could still be useful for other people, so here was the catch. In the line :
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset, new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd)
{ AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
the FillBehavior should have been FillBehavior.Stop.
As easy as that!

PreferredSize property doesn't match GetPreferredSize

I am trying to write a menu item control that will AutoSize based on the length of text it contains (like the Label control).
To do this, I overrided the GetPreferredSize method to calculate the length of the text:
public override Size GetPreferredSize(Size proposedSize)
{
Size size = TextRenderer.MeasureText(this.Text, this.Font);
int w = size.Width + this.Padding.Left + this.Padding.Right;
int h = size.Height + this.Padding.Top + this.Padding.Bottom;
return new Size(w, h);
}
I then add a bunch of these controls to a containing menu control and try to position them based on the size above:
if (item.AutoSize)
{
item.Size = item.PreferredSize;
}
item.Left = _Left;
item.Top = _Top;
if (this.MenuOrientation == Orientation.Vertical)
{
_Top += item.Size.Height;
}
else
{
_Left += item.Size.Width;
}
this.Controls.Add(item);
However, the sizes returned by PreferredSize and GetPreferredSize aren't the same. For one string, GetPreferredSize returns {Width=147, Height=27}, but PreferredSize returns {Width=105, Height=21}. Because of this the controls overlap instead of appearing beside one another.
I tried overriding MinimumSize instead of GetPreferredSize, but that also got scaled down from what I calculated.
So my question is, what is the correct way to do this? I'd also like to understand the way that AutoSize, PreferredSize, MinimumSize, and MaximumSize are meant to interact. MSDN is little help on this.

ScrollViewer with Canvas, auto-resize canvas to content including negative space

I have a canvas in a scrollviewer.
To be able to use the scrollviewer, I've overridden the Canvas's MeasureOverride method to return the size of all children.
This works fine, except that the canvas can only contain items in positive space for the scrollviewer to work correctly.
I'd like the user to be able to drag elements around without the restriction of them having to be on the positive side of the canvas's origin.
So basicly I want to be able to position the elements anywhere, like at (-200, 10) or (500, -20) , and be able to scroll from the most left element (-200) to the most right(500), and from top to bottom.
To complicate matters the canvas can be scaled using the LayoutTransform, and I'd like to include the view-area in the scrollbars, so not only the min/max bounds of the children are taken into account by the scrollbars, but also the min/max of the current view-area.
Does anybody know how to make this work ?
Today I worked on this problem only :)
Happy to share the code which works:
public void RepositionAllObjects(Canvas canvas)
{
adjustNodesHorizontally(canvas);
adjustNodesVertically(canvas);
}
private void adjustNodesVertically(Canvas canvas)
{
double minLeft = Canvas.GetLeft(canvas.Children[0]);
foreach (UIElement child in canvas.Children)
{
double left = Canvas.GetLeft(child);
if (left < minLeft)
minLeft = left;
}
if (minLeft < 0)
{
minLeft = -minLeft;
foreach (UIElement child in canvas.Children)
Canvas.SetLeft(child, Canvas.GetLeft(child) + minLeft);
}
}
private void adjustNodesHorizontally(Canvas canvas)
{
double minTop = Canvas.GetTop(canvas.Children[0]);
foreach (UIElement child in canvas.Children)
{
double top = Canvas.GetTop(child);
if (top < minTop)
minTop = top;
}
if (minTop < 0)
{
minTop = -minTop;
foreach (UIElement child in canvas.Children)
Canvas.SetTop(child, Canvas.GetTop(child) + minTop);
}
}
Now call RepositionAllObjects method to realign the objects as and when required.
Of course, you need to have your own canvas derived from Canvas. And this method is required (you can tweak it if you like):
public static Rect GetDimension(UIElement element)
{
Rect box = new Rect();
box.X = Canvas.GetLeft(element);
box.Y = Canvas.GetTop(element);
box.Width = element.DesiredSize.Width;
box.Height = element.DesiredSize.Height;
return box;
}
protected override Size MeasureOverride(Size constraint)
{
Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
double minX = 900000; //some dummy high number
double minY = 900000; //some dummy high number
double maxX = 0;
double maxY = 0;
foreach (UIElement element in this.Children)
{
element.Measure(availableSize);
Rect box = GetDimension(element);
if (minX > box.X) minX = box.X;
if (minY > box.Y) minY = box.Y;
if (maxX < box.X + box.Width) maxX = box.X + box.Width;
if (maxY < box.Y + box.Height) maxY = box.Y + box.Height;
}
if (minX == 900000) minX = 0;
if (minY == 900000) minY = 0;
return new Size { Width = maxX - minX, Height = maxY - minY };
}
Now, all you need to do is wrap this canvas inside a scrollviewer.
Hope it helps!
I think the ScrollViewer assumes that the origin will be at (0, 0) -- after all, there's no way to tell it otherwise; Canvas is the only panel that knows about specified-and-nonzero origins.
A simple option might be to forget about a ScrollViewer, and implement your own panning functionality -- perhaps with dragging, or perhaps with a big "scroll left" button on the left side of the view, a "scroll right" button on the right, etc. -- and implement it in terms of a transform on the content.
But if you need to stick with the classic scrollbar metaphor, I think you could make your own panel (or override ArrangeOverride too, which amounts to the same thing) and offset all the positions -- make them zero-based. So if you have an element at (20, -20) and another at (0, 5), you would need to offset everything downward by 20 to make it fit in a zero-based space; and when you lay out your children, the first would go at (20, 0) and the second at (0, 25).
But now scrolling is weird. If the user is scrolled all the way to the bottom, and drags something off the bottom edge, the view stays put; same with the right. But if they're scrolled all the way to the top, and drag something off the top edge, suddenly everything jumps downward, because the ScrollViewer's VerticalOffset was zero before and is still zero, but zero means something different because of your layout offset.
You might be able to get around the scrolling weirdness by binding the HorizontalOffset and VerticalOffset. If your ViewModel automatically fixed up those properties (and fired PropertyChanged) whenever a control moves "negative", then you might be able to keep the view scrolled to the same logical content, even though you're now telling WPF's layout engine that everything is somewhere else. I.e., your ViewModel would track the logical scroll position (zero, before you drag the element negative), and would report the zero-based scroll position to the ScrollViewer (so after the drag, you would tell the ScrollViewer that its VerticalOffset is now 20).

How do you determine the width of the text in a WPF TreeViewItem at run time?

How do you determine the width of the text in a WPF TreeViewItem at run time?
I need to calculate an offset so I can draw a line from one leaf to the leaf of a different TreeView. All the 'width' properties return a size that is way bigger than the space taken up by the actual text of the node. It must be possible because the Select feature doesn't highlight the entire row. I'm writing the client in WPF and Silverlight.
You weren't very specific on the text or the tags, so I'm assuming you're taking about the .Net Framework's TreeViewItem.
There might be easier ways, but one possibility is to use the Graphics.MeasureString method. It gives you the size in pixels of a text when drawn using a specific font.
#mrphil: Sweet aborted fetus, that's scary
myTreeViewItem.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
Size s = myTreeViewItem.DesiredSize;
return s.Width;
I have two solutions:
A) Uses the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
double expanderWidth = 0;
Grid grid = (Grid)VisualTreeHelper.GetChild(selected, 0);
ToggleButton toggleButton = (ToggleButton)VisualTreeHelper.GetChild(grid, 0);
expanderWidth = toggleButton.ActualWidth;
Border bd = (Border)VisualTreeHelper.GetChild(grid, 1);
textWidth = bd.ActualWidth;
B) If you don't want to use the visual tree
TreeViewItem selected = (TreeViewItem)dataSourceTreeView.SelectedItem;
double textWidth = 0;
Typeface typeface = new Typeface(selected.FontFamily,
selected.FontStyle, selected.FontWeight, selected.FontStretch);
GlyphTypeface glyphTypeface;
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
throw new InvalidOperationException("No glyphtypeface found");
string headerText = (string)selected.Header;
double size = selected.FontSize;
ushort[] glyphIndexes = new ushort[headerText.Length];
double[] advanceWidths = new double[headerText.Length];
for (int n = 0; n < headerText.Length; n++)
{
ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[headerText[n]];
glyphIndexes[n] = glyphIndex;
double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
advanceWidths[n] = width;
textWidth += width;
}

Resources