diff --git a/MahApps.Metro/Controls/MetroPopover.cs b/MahApps.Metro/Controls/MetroPopover.cs new file mode 100644 index 0000000000..644909ec31 --- /dev/null +++ b/MahApps.Metro/Controls/MetroPopover.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace MahApps.Metro.Controls +{ + + [ContentProperty("Content")] + public class MetroPopover : FrameworkElement + { + #region Disposable event handler + + static class Disposable + { + public static IDisposable Subscription(Action subscribe, Action unsubscribe, TEventHandler handler) + { + subscribe(handler); + return new DisposableSubscription(unsubscribe, handler); + } + } + + /// + /// Unsubscribes the given event handler on disposal. + /// + class DisposableSubscription : IDisposable + { + readonly Action description; + readonly TEventHandler handler; + + public DisposableSubscription(Action unsubscribe, TEventHandler handler) + { + this.description = unsubscribe; + this.handler = handler; + } + + public void Dispose() + { + description(handler); + } + } + + #endregion + + #region Popover adorner + + // see tech.pro/tutorial/856/wpf-tutorial-using-a-visual-collection by Michael Kuehl + + class PopoverAdorner : Adorner + { + readonly VisualCollection _visuals; + readonly MetroPopover _popover; + readonly MetroPopoverWindow _popoverWindow; + + public PopoverAdorner(UIElement adornedElement, MetroPopover popover) + : base(adornedElement) + { + _popover = popover; + _visuals = new VisualCollection(this); + _popoverWindow = new MetroPopoverWindow(popover) { + Content = popover.Content + }; + + // bind key popover window properties to the popovers + _popoverWindow.SetBinding(MetroPopoverWindow.HorizontalAlignmentProperty, new Binding("HorizontalAlignment") { Source = popover, Mode = BindingMode.OneWay }); + //_popoverWindow.SetBinding(MetroPopoverWindow.WidthProperty, new Binding("Width") { Source = popover, Mode = BindingMode.OneWay }); + //_popoverWindow.SetBinding(MetroPopoverWindow.MinWidthProperty, new Binding("MinWidth") { Source = popover, Mode = BindingMode.OneWay }); + //_popoverWindow.SetBinding(MetroPopoverWindow.MaxWidthProperty, new Binding("MaxWidth") { Source = popover, Mode = BindingMode.OneWay }); + _visuals.Add(_popoverWindow); + } + + public MetroPopover Popover + { + get { return _popover; } + } + + public MetroPopoverWindow PopoverWindow + { + get { return _popoverWindow; } + } + + public void UpdateContent(object content) + { + _popoverWindow.Content = content; + } + + public void ShowWindow() + { + Attach(); + _popoverWindow.Show(); + } + + public void HideWindow() + { + _popoverWindow.Hide(); + Detach(); + } + + public bool IsWindowOpen() + { + return _popoverWindow.IsLoaded && _popoverWindow.Opacity > 0; + } + + public void Detach() + { + var adornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement); + if (adornerLayer != null) { + adornerLayer.Remove(this); + } + } + + public void Attach() + { + var adornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement); + if (adornerLayer != null && !IsAttachedTo(adornerLayer)) { + adornerLayer.Add(this); + } + } + + bool IsAttachedTo(AdornerLayer adornerLayer) + { + if (adornerLayer != null) { + var adorners = adornerLayer.GetAdorners(AdornedElement); + return adorners != null && adorners.Contains(this); + } else { + return false; + } + } + + bool IsAdornedElementLoaded() + { + if (AdornedElement is FrameworkElement) { + return ((FrameworkElement)AdornedElement).IsLoaded; + } else { + return true; + } + } + + protected override Size MeasureOverride(Size constraint) + { + _popoverWindow.Measure(constraint); + return _popoverWindow.DesiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var targetSize = AdornedElement.RenderSize; + + double offsetX; + if (Popover.HorizontalAlignment == System.Windows.HorizontalAlignment.Left) { + offsetX = 0; + } else if (Popover.HorizontalAlignment == System.Windows.HorizontalAlignment.Right) { + offsetX = targetSize.Width - finalSize.Width; + } else if (Popover.HorizontalAlignment == System.Windows.HorizontalAlignment.Center || Popover.HorizontalAlignment == System.Windows.HorizontalAlignment.Stretch) { + offsetX = (targetSize.Width - finalSize.Width) / 2.0; + } else { + offsetX = 0; + } + + var offsetY = AdornedElement.RenderSize.Height; + if (AdornedElement is Control) { + offsetY -= ((Control)AdornedElement).Margin.Bottom; + } + + _popoverWindow.Arrange(new Rect(offsetX, offsetY, finalSize.Width, finalSize.Height)); + return _popoverWindow.RenderSize; + } + + protected override Visual GetVisualChild(int index) + { + return _visuals[index]; + } + + protected override int VisualChildrenCount + { + get { return _visuals.Count; } + } + } + + #endregion + + PopoverAdorner _adorner; + IDisposable _previewOwningWindowClickSubscription; + + static MetroPopover() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(MetroPopover), new FrameworkPropertyMetadata(typeof(MetroPopover))); + + // Ensure visibility is always collapsed + UIElement.VisibilityProperty.OverrideMetadata(typeof(MetroPopover), new FrameworkPropertyMetadata(Visibility.Collapsed, null, new CoerceValueCallback(MetroPopover.CoerceCollapsedVisibility))); + } + + private static object CoerceCollapsedVisibility(DependencyObject d, object baseValue) + { + return Visibility.Collapsed; + } + + public MetroPopover() + { + this.Loaded += OnLoaded; + this.Unloaded += OnUnloaded; + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(MetroPopover), new PropertyMetadata(null, OnContentChanged)); + public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register("IsOpen", typeof(bool), typeof(MetroPopover), new PropertyMetadata(false, OnIsOpenChanged)); + public static readonly DependencyProperty TargetProperty = DependencyProperty.Register("Target", typeof(UIElement), typeof(MetroPopover), new PropertyMetadata(null, OnTargetChanged)); + public static readonly DependencyProperty AutoCloseProperty = DependencyProperty.Register("AutoClose", typeof(bool), typeof(MetroPopover), new PropertyMetadata(true)); + + /// + /// The element to place the popover under. Note horizontal placement is controlled via the property. + /// + public UIElement Target + { + get { return (UIElement)GetValue(TargetProperty); } + set { SetValue(TargetProperty, value); } + } + + /// + /// Gets or sets the content to be displayed in the popover. + /// + public object Content + { + get { return (object)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + /// + /// Gets or sets a value indicating if the popover open. + /// + public bool IsOpen + { + get { return (bool)GetValue(IsOpenProperty); } + set { SetValue(IsOpenProperty, value); } + } + + /// + /// Gets or sets a value that indicates if the popover should be automatically closed when the user clicks else where in the owning window. + /// + public bool AutoClose + { + get { return (bool)GetValue(AutoCloseProperty); } + set { SetValue(AutoCloseProperty, value); } + } + + /// + /// Opens the popover. + /// + public void Open() + { + IsOpen = true; + } + + /// + /// Closes the popover. + /// + public void Close() + { + IsOpen = false; + } + + /// + /// A last chance virtual method for stopping an popover from closing. + /// + /// + protected virtual bool OnRequestClose() + { + return true; //allow the dialog to close. + } + + private static void OnTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var popover = (MetroPopover)d; + popover.SetupAdorner(); + } + + private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var popover = (MetroPopover)d; + var oldChild = e.OldValue; + var newChild = e.NewValue; + + // rebuild + if (popover._adorner != null) { + popover._adorner.UpdateContent(newChild); + } + } + + private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var popover = (MetroPopover)d; + var isOpen = (bool)e.NewValue; + + if (popover._adorner != null) { + if (isOpen) { + popover._adorner.ShowWindow(); + } else { + popover._adorner.HideWindow(); + } + } + } + + // Attach to parent window. + + void OnLoaded(object sender, RoutedEventArgs e) + { + Window owner = this.TryFindParent(); + if (owner != null && _previewOwningWindowClickSubscription == null) { + _previewOwningWindowClickSubscription = Disposable.Subscription(handler => owner.PreviewMouseDown += handler, handler => owner.PreviewMouseDown -= handler, OnPreviewOwningWindowMouseDown); + } + } + + void OnUnloaded(object sender, RoutedEventArgs e) + { + if (_previewOwningWindowClickSubscription != null) { + _previewOwningWindowClickSubscription.Dispose(); + _previewOwningWindowClickSubscription = null; + } + } + + private void OnPreviewOwningWindowMouseDown(object sender, MouseButtonEventArgs e) + { + var originalSource = e.OriginalSource as DependencyObject; + if (AutoClose && IsPopoverOpen() && + originalSource != null && // didn't click on target or popover (adorner) + !IsDescendant(Target, originalSource) && + !IsDescendant(_adorner.PopoverWindow, originalSource)) { + Close(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + // The MetroPopover control itself isn't ever rendered on screen so set the measure to 0. + return default(Size); + } + + /// + /// Programically determines if the popover is actually open & visible. + /// + /// + bool IsPopoverOpen() + { + return _adorner != null && _adorner.IsWindowOpen(); + } + + private void SetupAdorner() + { + if (_adorner == null || _adorner.AdornedElement != Target) { + if (_adorner != null) { + _adorner.Detach(); + this.RemoveLogicalChild(_adorner.PopoverWindow); + + _adorner = null; + } + + if (Target != null) { + _adorner = new PopoverAdorner(Target, this); + this.AddLogicalChild(_adorner.PopoverWindow); + } + } + } + + + static bool IsDescendant(DependencyObject reference, DependencyObject node) + { + bool result = false; + DependencyObject dependencyObject = node; + while (dependencyObject != null) { + if (dependencyObject == reference) { + result = true; + break; + } + + dependencyObject = dependencyObject.GetParentObject(); + } + return result; + } + } +} diff --git a/MahApps.Metro/Controls/MetroPopoverWindow.cs b/MahApps.Metro/Controls/MetroPopoverWindow.cs new file mode 100644 index 0000000000..ca4f1c362a --- /dev/null +++ b/MahApps.Metro/Controls/MetroPopoverWindow.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media.Animation; + +namespace MahApps.Metro.Controls +{ + public class MetroPopoverWindow : ContentControl + { + readonly MetroPopover _popover; + + static MetroPopoverWindow() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(MetroPopoverWindow), new FrameworkPropertyMetadata(typeof(MetroPopoverWindow))); + } + + public MetroPopoverWindow(MetroPopover popover) + { + _popover = popover; + } + + + /// + /// Waits for the popover to become ready for interaction. + /// + /// A task that represents the operation and it's status. + public void Show() + { + Dispatcher.VerifyAccess(); + + + this.Opacity = 1.0; + this.Visibility = System.Windows.Visibility.Visible; + } + + public void Hide() + { + Dispatcher.VerifyAccess(); + + this.Opacity = 0.0; + this.Visibility = System.Windows.Visibility.Collapsed; + } + + } + +} diff --git a/MahApps.Metro/Controls/MetroWindow.cs b/MahApps.Metro/Controls/MetroWindow.cs index 1817854ecd..7e139187a3 100644 --- a/MahApps.Metro/Controls/MetroWindow.cs +++ b/MahApps.Metro/Controls/MetroWindow.cs @@ -500,7 +500,7 @@ public string WindowTitle { get { return TitleCaps ? Title.ToUpper() : Title; } } - + /// /// Begins to show the MetroWindow's overlay effect. /// @@ -541,6 +541,7 @@ public System.Threading.Tasks.Task ShowOverlayAsync() return tcs.Task; } + /// /// Begins to hide the MetroWindow's overlay effect. /// diff --git a/MahApps.Metro/MahApps.Metro.NET45.csproj b/MahApps.Metro/MahApps.Metro.NET45.csproj index aefbd61611..dc2ca05239 100644 --- a/MahApps.Metro/MahApps.Metro.NET45.csproj +++ b/MahApps.Metro/MahApps.Metro.NET45.csproj @@ -114,6 +114,8 @@ + + @@ -524,6 +526,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/MahApps.Metro/MahApps.Metro.csproj b/MahApps.Metro/MahApps.Metro.csproj index a2d77860e7..46704ed92e 100644 --- a/MahApps.Metro/MahApps.Metro.csproj +++ b/MahApps.Metro/MahApps.Metro.csproj @@ -117,6 +117,7 @@ + @@ -127,6 +128,7 @@ + @@ -461,6 +463,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/MahApps.Metro/Themes/Generic.xaml b/MahApps.Metro/Themes/Generic.xaml index 2377b24c10..65f4fd7eed 100644 --- a/MahApps.Metro/Themes/Generic.xaml +++ b/MahApps.Metro/Themes/Generic.xaml @@ -24,6 +24,7 @@ + diff --git a/MahApps.Metro/Themes/MetroPopover.xaml b/MahApps.Metro/Themes/MetroPopover.xaml new file mode 100644 index 0000000000..ff9296c3ab --- /dev/null +++ b/MahApps.Metro/Themes/MetroPopover.xaml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MetroDemo/ExampleViews/OtherExamples.xaml b/samples/MetroDemo/ExampleViews/OtherExamples.xaml index 7f233676ff..31fa6088f7 100644 --- a/samples/MetroDemo/ExampleViews/OtherExamples.xaml +++ b/samples/MetroDemo/ExampleViews/OtherExamples.xaml @@ -18,179 +18,228 @@ - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + Show Popover + + This popover is aligned to the bottom left. + + Show Popover + + This popover is aligned to the bottom center. + + Show Popover + + + + + + + + + + This popover is aligned to the bottom right with form controls. + + + + Item 1 + Item 2 + Item 3 + Item 4 + + - - - - - - - - - - - - - - + + + + + diff --git a/samples/MetroDemo/ExampleViews/OtherExamples.xaml.cs b/samples/MetroDemo/ExampleViews/OtherExamples.xaml.cs index 14604a2c12..f38aebcbbf 100644 --- a/samples/MetroDemo/ExampleViews/OtherExamples.xaml.cs +++ b/samples/MetroDemo/ExampleViews/OtherExamples.xaml.cs @@ -27,7 +27,7 @@ public OtherExamples() var t = new DispatcherTimer(TimeSpan.FromSeconds(2), DispatcherPriority.Normal, Tick, this.Dispatcher); } - + void Tick(object sender, EventArgs e) { var dateTime = DateTime.Now;