diff --git a/Changelog.md b/Changelog.md index df1979099..9838646e8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,8 +5,15 @@ - ### Bug fixes - [#821](../../issues/821) - Different visual of menu item and submenu item + - [#823](../../issues/823) - Gallery item content hidden when mouse pressed + - [#825](../../issues/825) - CLS compliance - [#830](../../issues/830) - When a window is set to automatically resize to its content, when its title is set in code, it disappears - [#834](../../issues/834) - InRibbonGallery resizing issue when changing `Visibility` + - [#837](../../issues/837) - InRibbonGallery Property MinItemsInDropDownRow not considered + - [#838](../../issues/838) - InRibbonGallery changes into DropDownButton after DropDown is opened and closed + - [#840](../../issues/840) - Ribbon does not scroll anymore + - [#848](../../issues/848) - Colorful-Theme and Fullscreen Issue + - [#849](../../issues/849) - QuickAccessToolBar not editable anymore (thanks @chrfin) ## 8.0.0 diff --git a/Directory.build.props b/Directory.build.props index 9f3911d39..42efcccd5 100644 --- a/Directory.build.props +++ b/Directory.build.props @@ -10,7 +10,7 @@ netcoreapp3.0;net462;net452 - latest + latestmajor true false @@ -48,13 +48,13 @@ - + - + - - + + \ No newline at end of file diff --git a/Fluent.Ribbon.Showcase/TestContent.xaml b/Fluent.Ribbon.Showcase/TestContent.xaml index 4551ac132..e180b4317 100644 --- a/Fluent.Ribbon.Showcase/TestContent.xaml +++ b/Fluent.Ribbon.Showcase/TestContent.xaml @@ -1201,11 +1201,11 @@ Pellentesque nec dolor sed lacus tristique rutrum sed vitae urna. Sed eu pharetr KeyTip="S" GroupBy="Tag" ResizeMode="Both" - MaxItemsInRow="6" + MaxItemsInRow="2" MinItemsInRow="2" ItemWidth="40" ItemHeight="55" - Width="300" + Width="100" MinItemsInDropDownRow="5"> + + + + + + + + + + + + + Group="{Binding ElementName=tabGroup1, Mode=OneWay}" + BorderBrush="{DynamicResource Fluent.Ribbon.Brushes.AccentBaseColorBrush}"> + /// Test-Content + /// + public partial class TestContent { private readonly MainViewModel viewModel; private string windowTitle; @@ -52,6 +54,7 @@ public TestContent() public string WindowTitle => this.windowTitle ?? (this.windowTitle = GetVersionText(Window.GetWindow(this).GetType().BaseType)); + /// Identifies the dependency property. public static readonly DependencyProperty BrushesProperty = DependencyProperty.Register(nameof(Brushes), typeof(List>), typeof(TestContent), new PropertyMetadata(default(List>))); public List> Brushes @@ -88,15 +91,11 @@ private static string GetVersionText(Type type) { var version = type.Assembly.GetName().Version; - var assemblyProductAttribute = (type.Assembly - .GetCustomAttributes(typeof(AssemblyProductAttribute), false) as AssemblyProductAttribute[]) - .FirstOrDefault(); + var assemblyProductAttribute = type.Assembly.GetCustomAttribute(); - var assemblyInformationalVersionAttribute = (type.Assembly - .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) as AssemblyInformationalVersionAttribute[]) - .FirstOrDefault(); + var assemblyInformationalVersionAttribute = type.Assembly.GetCustomAttribute(); - return $"{assemblyProductAttribute.Product} {version} ({assemblyInformationalVersionAttribute.InformationalVersion})"; + return $"{assemblyProductAttribute?.Product} {version} ({assemblyInformationalVersionAttribute?.InformationalVersion})"; } private string selectedMenu = "Backstage"; @@ -594,7 +593,10 @@ private void StartSnoop_OnClick(object sender, RoutedEventArgs e) snoopPath = alternativeSnoopPath; } - var startInfo = new ProcessStartInfo(snoopPath, $"inspect --targetPID {Process.GetCurrentProcess().Id}"); + var startInfo = new ProcessStartInfo(snoopPath, $"inspect --targetPID {Process.GetCurrentProcess().Id}") + { + UseShellExecute = true + }; try { using var p = Process.Start(startInfo); diff --git a/Fluent.Ribbon.Tests/Controls/RibbonGroupBoxTests.cs b/Fluent.Ribbon.Tests/Controls/RibbonGroupBoxTests.cs index 7a19293b0..c2a0c6dc8 100644 --- a/Fluent.Ribbon.Tests/Controls/RibbonGroupBoxTests.cs +++ b/Fluent.Ribbon.Tests/Controls/RibbonGroupBoxTests.cs @@ -28,22 +28,28 @@ public void Size_Should_Change_On_Group_State_Change_When_Items_Are_Bound() using (new TestRibbonWindow(ribbonGroupBox)) { { - ribbonGroupBox.State = RibbonGroupBoxState.Small; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Small; + UIHelper.DoEvents(); + } Assert.That(items.First().ControlSize, Is.EqualTo(RibbonControlSize.Small)); } { - ribbonGroupBox.State = RibbonGroupBoxState.Middle; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Middle; + UIHelper.DoEvents(); + } Assert.That(items.First().ControlSize, Is.EqualTo(RibbonControlSize.Middle)); } { - ribbonGroupBox.State = RibbonGroupBoxState.Large; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Large; + UIHelper.DoEvents(); + } Assert.That(items.First().ControlSize, Is.EqualTo(RibbonControlSize.Large)); } @@ -60,22 +66,28 @@ public void Size_Should_Change_On_Group_State_Change_When_Items_Are_Ribbon_Contr using (new TestRibbonWindow(ribbonGroupBox)) { { - ribbonGroupBox.State = RibbonGroupBoxState.Small; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Small; + UIHelper.DoEvents(); + } Assert.That(ribbonGroupBox.Items.OfType().First().Size, Is.EqualTo(RibbonControlSize.Small)); } { - ribbonGroupBox.State = RibbonGroupBoxState.Middle; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Middle; + UIHelper.DoEvents(); + } Assert.That(ribbonGroupBox.Items.OfType().First().Size, Is.EqualTo(RibbonControlSize.Middle)); } { - ribbonGroupBox.State = RibbonGroupBoxState.Large; - UIHelper.DoEvents(); + { + ribbonGroupBox.State = RibbonGroupBoxState.Large; + UIHelper.DoEvents(); + } Assert.That(ribbonGroupBox.Items.OfType().First().Size, Is.EqualTo(RibbonControlSize.Large)); } diff --git a/Fluent.Ribbon.Tests/Internal/ScopeGuardTests.cs b/Fluent.Ribbon.Tests/Internal/ScopeGuardTests.cs index 2bd3b3bc1..8ef4a5d46 100644 --- a/Fluent.Ribbon.Tests/Internal/ScopeGuardTests.cs +++ b/Fluent.Ribbon.Tests/Internal/ScopeGuardTests.cs @@ -11,6 +11,10 @@ public void IsActive_Marker_Should_Change_On_Dispose() { var guard = new ScopeGuard(); + Assert.That(guard.IsActive, Is.False); + + guard.Start(); + Assert.That(guard.IsActive, Is.True); guard.Dispose(); @@ -31,7 +35,7 @@ public void Actions_Should_Be_Called() var disposeActionCallCount = 0; void DisposeAction() => ++disposeActionCallCount; - var guard = new ScopeGuard(EntryAction, DisposeAction); + var guard = new ScopeGuard(EntryAction, DisposeAction).Start(); Assert.That(entryActionCallCount, Is.EqualTo(1)); Assert.That(disposeActionCallCount, Is.EqualTo(0)); diff --git a/Fluent.Ribbon.Tests/Properties/AssemblyInfo.cs b/Fluent.Ribbon.Tests/Properties/AssemblyInfo.cs index 287ea673c..a2a942b12 100644 --- a/Fluent.Ribbon.Tests/Properties/AssemblyInfo.cs +++ b/Fluent.Ribbon.Tests/Properties/AssemblyInfo.cs @@ -1,5 +1,5 @@ -using System.Reflection; +using System; -[assembly: AssemblyDescription("")] +[assembly: CLSCompliant(false)] [assembly: NUnit.Framework.Apartment(System.Threading.ApartmentState.STA)] \ No newline at end of file diff --git a/Fluent.Ribbon.sln b/Fluent.Ribbon.sln index 3a608a6c9..bb1e99669 100644 --- a/Fluent.Ribbon.sln +++ b/Fluent.Ribbon.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CDBFA905 Directory.build.targets = Directory.build.targets GitVersion.yml = GitVersion.yml global.json = global.json + Directory.packages.props = Directory.packages.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C6ADD536-3DB2-4418-AED6-98AB991D1B77}" diff --git a/Fluent.Ribbon.sln.DotSettings b/Fluent.Ribbon.sln.DotSettings index 06d25f279..e273504dd 100644 --- a/Fluent.Ribbon.sln.DotSettings +++ b/Fluent.Ribbon.sln.DotSettings @@ -1,5 +1,5 @@ - - CSharp72 + + CSharp8 Inherit False DO_NOT_SHOW @@ -52,6 +52,9 @@ QAT <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + PackageReference + Highest + MsBuild True True True diff --git a/Fluent.Ribbon/Controls/ColorGallery.cs b/Fluent.Ribbon/Controls/ColorGallery.cs index 0e35021e7..740550d60 100644 --- a/Fluent.Ribbon/Controls/ColorGallery.cs +++ b/Fluent.Ribbon/Controls/ColorGallery.cs @@ -11,8 +11,6 @@ namespace Fluent using System.Windows.Interop; using System.Windows.Markup; using System.Windows.Media; - using ControlzEx.Native; - using ControlzEx.Standard; using Fluent.Extensions; using Fluent.Internal; using Fluent.Internal.KnownBoxes; @@ -852,14 +850,14 @@ private void OnMoreColorsClick(object sender, RoutedEventArgs e) else { #pragma warning disable 618 - var chooseColor = new NativeMethods.CHOOSECOLOR(); + var chooseColor = new CHOOSECOLOR(); var wnd = Window.GetWindow(this); if (wnd != null) { chooseColor.hwndOwner = new WindowInteropHelper(wnd).Handle; } - chooseColor.Flags = Constants.CC_ANYCOLOR; + chooseColor.Flags = CC_ANYCOLOR; if (customColors == IntPtr.Zero) { // Set custom colors) @@ -872,7 +870,7 @@ private void OnMoreColorsClick(object sender, RoutedEventArgs e) } chooseColor.lpCustColors = customColors; - if (NativeMethods.ChooseColor(chooseColor)) + if (ChooseColor(chooseColor)) { var color = ConvertFromWin32Color(chooseColor.rgbResult); if (RecentColors.Contains(color)) @@ -1124,5 +1122,77 @@ private static Color[] GetGradient(Color color, int count) } #endregion + + #region native + +#pragma warning disable SA1307, SA1310, SA1401 + + /// + /// Creates a Color dialog box that enables the user to select a color. + /// + /// A pointer to a CHOOSECOLOR structure that contains information used to initialize the dialog box. When ChooseColor returns, this structure contains information about the user's color selection. + /// If the user clicks the OK button of the dialog box, the return value is nonzero. The rgbResult member of the CHOOSECOLOR structure contains the RGB color value of the color selected by the user.If the user cancels or closes the Color dialog box or an error occurs, the return value is zero. + [DllImport("comdlg32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ChooseColor(CHOOSECOLOR lpcc); + + /// + /// Contains information the ChooseColor function uses to initialize the Color dialog box. After the user closes the dialog box, the system returns information about the user's selection in this structure. + /// + [StructLayout(LayoutKind.Sequential)] + private class CHOOSECOLOR + { + /// + /// The length, in bytes, of the structure. + /// + public int lStructSize = Marshal.SizeOf(typeof(CHOOSECOLOR)); + + /// + /// A handle to the window that owns the dialog box. This member can be any valid window handle, or it can be NULL if the dialog box has no owner. + /// + public IntPtr hwndOwner; + + /// + /// If the CC_ENABLETEMPLATEHANDLE flag is set in the Flags member, hInstance is a handle to a memory object containing a dialog box template. If the CC_ENABLETEMPLATE flag is set, hInstance is a handle to a module that contains a dialog box template named by the lpTemplateName member. If neither CC_ENABLETEMPLATEHANDLE nor CC_ENABLETEMPLATE is set, this member is ignored. + /// + public IntPtr hInstance = IntPtr.Zero; + + /// + /// If the CC_RGBINIT flag is set, rgbResult specifies the color initially selected when the dialog box is created. If the specified color value is not among the available colors, the system selects the nearest solid color available. If rgbResult is zero or CC_RGBINIT is not set, the initially selected color is black. If the user clicks the OK button, rgbResult specifies the user's color selection. To create a COLORREF color value, use the RGB macro. + /// + public int rgbResult; + + /// + /// A pointer to an array of 16 values that contain red, green, blue (RGB) values for the custom color boxes in the dialog box. If the user modifies these colors, the system updates the array with the new RGB values. To preserve new custom colors between calls to the ChooseColor function, you should allocate static memory for the array. To create a COLORREF color value, use the RGB macro. + /// + public IntPtr lpCustColors = IntPtr.Zero; + + /// + /// A set of bit flags that you can use to initialize the Color dialog box. When the dialog box returns, it sets these flags to indicate the user's input. + /// + public int Flags; + + /// + /// Application-defined data that the system passes to the hook procedure identified by the lpfnHook member. When the system sends the WM_INITDIALOG message to the hook procedure, the message's lParam parameter is a pointer to the CHOOSECOLOR structure specified when the dialog was created. The hook procedure can use this pointer to get the lCustData value. + /// + public IntPtr lCustData = IntPtr.Zero; + + /// + /// A pointer to a CCHookProc hook procedure that can process messages intended for the dialog box. This member is ignored unless the CC_ENABLEHOOK flag is set in the Flags member. + /// + public IntPtr lpfnHook = IntPtr.Zero; + + /// + /// The name of the dialog box template resource in the module identified by the hInstance member. This template is substituted for the standard dialog box template. For numbered dialog box resources, lpTemplateName can be a value returned by the MAKEINTRESOURCE macro. This member is ignored unless the CC_ENABLETEMPLATE flag is set in the Flags member. + /// + public IntPtr lpTemplateName = IntPtr.Zero; + } + + /// + /// Causes the dialog box to display all available colors in the set of basic colors. + /// + private const int CC_ANYCOLOR = 256; + + #endregion } } \ No newline at end of file diff --git a/Fluent.Ribbon/Controls/GalleryGroupContainer.cs b/Fluent.Ribbon/Controls/GalleryGroupContainer.cs index 983f250c9..83239562e 100644 --- a/Fluent.Ribbon/Controls/GalleryGroupContainer.cs +++ b/Fluent.Ribbon/Controls/GalleryGroupContainer.cs @@ -3,7 +3,6 @@ namespace Fluent { using System.Windows; using System.Windows.Controls; - using System.Windows.Media; using Fluent.Internal.KnownBoxes; /// diff --git a/Fluent.Ribbon/Controls/GalleryPanel.cs b/Fluent.Ribbon/Controls/GalleryPanel.cs index ebef50808..94d14fab8 100644 --- a/Fluent.Ribbon/Controls/GalleryPanel.cs +++ b/Fluent.Ribbon/Controls/GalleryPanel.cs @@ -370,7 +370,7 @@ private void Refresh() foreach (UIElement item in this.InternalChildren) { - if (item == null) + if (item is null) { continue; } @@ -380,18 +380,18 @@ private void Refresh() if (this.GroupByAdvanced != null) { - propertyValue = this.ItemContainerGenerator == null + propertyValue = this.ItemContainerGenerator is null ? this.GroupByAdvanced(item) : this.GroupByAdvanced(this.ItemContainerGenerator.ItemFromContainerOrContainerContent(item)); } else if (string.IsNullOrEmpty(this.GroupBy) == false) { - propertyValue = this.ItemContainerGenerator == null + propertyValue = this.ItemContainerGenerator is null ? this.GetPropertyValueAsString(item) : this.GetPropertyValueAsString(this.ItemContainerGenerator.ItemFromContainerOrContainerContent(item)); } - if (propertyValue == null) + if (propertyValue is null) { propertyValue = Undefined; } @@ -438,7 +438,7 @@ private void Refresh() dictionary[propertyValue].Items.Add(galleryItemPlaceholder); } - if ((this.IsGrouped == false || (this.GroupBy == null && this.GroupByAdvanced == null)) + if ((this.IsGrouped == false || (this.GroupBy is null && this.GroupByAdvanced is null)) && this.galleryGroupContainers.Count != 0) { // Make it without headers if there is only one group or if we are not supposed to group @@ -520,8 +520,8 @@ protected override Size ArrangeOverride(Size finalSize) private string GetPropertyValueAsString(object item) { - if (item == null - || this.GroupBy == null) + if (item is null + || this.GroupBy is null) { return Undefined; } @@ -529,7 +529,7 @@ private string GetPropertyValueAsString(object item) var property = item.GetType().GetProperty(this.GroupBy, BindingFlags.Public | BindingFlags.Instance); var result = property?.GetValue(item, null); - if (result == null) + if (result is null) { return Undefined; } diff --git a/Fluent.Ribbon/Controls/InRibbonGallery.cs b/Fluent.Ribbon/Controls/InRibbonGallery.cs index 34b0fef0c..0e8e42415 100644 --- a/Fluent.Ribbon/Controls/InRibbonGallery.cs +++ b/Fluent.Ribbon/Controls/InRibbonGallery.cs @@ -969,6 +969,8 @@ private static object CoerceSelectedItem(DependencyObject d, object basevalue) public InRibbonGallery() { ContextMenuService.Coerce(this); + + this.IsVisibleChanged += this.OnIsVisibleChanged; } #endregion @@ -1114,10 +1116,10 @@ public override void OnApplyTemplate() } this.galleryPanel = this.GetTemplateChild("PART_GalleryPanel") as GalleryPanel; - + if (this.galleryPanel.IsNotNull()) { - using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdates)) + using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdates).Start()) { this.galleryPanel.MinItemsInRow = this.MinItemsInRow; this.galleryPanel.MaxItemsInRow = this.MaxItemsInRow; @@ -1142,6 +1144,13 @@ public override void OnApplyTemplate() this.popupResizeBorder = this.GetTemplateChild("PART_PopupResizeBorder") as FrameworkElement; } + private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + var groupBox = UIHelper.GetParent(this); + + groupBox?.TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer(); + } + private void OnPopupPreviewMouseUp(object sender, MouseButtonEventArgs e) { // Ignore mouse up when mouse donw is on expand button @@ -1171,7 +1180,7 @@ private void OnDropDownClosed(object sender, EventArgs e) if (this.galleryPanel.IsNotNull()) { - using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdatesRefresh)) + using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdatesRefresh).Start()) { this.CurrentGalleryPanelState?.Restore(); @@ -1212,7 +1221,7 @@ private void OnDropDownOpened(object sender, EventArgs e) if (this.galleryPanel.IsNotNull()) { - using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdatesRefresh)) + using (new ScopeGuard(this.galleryPanel.SuspendUpdates, this.galleryPanel.ResumeUpdatesRefresh).Start()) { this.CurrentGalleryPanelState?.Save(); @@ -1549,6 +1558,24 @@ public bool CanAddToQuickAccessToolBar #region Implementation of IScalableRibbonControl + /// + public void ResetScale() + { + if (this.IsCollapsed + && RibbonProperties.GetSize(this) == RibbonControlSize.Large) + { + this.IsCollapsed = false; + } + + if (this.galleryPanel.IsNotNull() + && this.galleryPanel.MaxItemsInRow < this.MaxItemsInRow) + { + this.galleryPanel.MaxItemsInRow = this.MaxItemsInRow; + } + + this.InvalidateMeasure(); + } + /// public void Enlarge() { @@ -1685,10 +1712,6 @@ public void Restore(FrameworkElement widthControl, FrameworkElement heightContro { widthControl.SetCurrentValue(WidthProperty, this.Width); } - else if (DoubleUtil.GreaterThan(widthControl.ActualWidth, 0)) - { - widthControl.SetCurrentValue(WidthProperty, widthControl.ActualWidth); - } if (double.IsNaN(this.Height) == false) { diff --git a/Fluent.Ribbon/Controls/QuickAccessToolBar.cs b/Fluent.Ribbon/Controls/QuickAccessToolBar.cs index 66b0ffe89..c68d0c729 100644 --- a/Fluent.Ribbon/Controls/QuickAccessToolBar.cs +++ b/Fluent.Ribbon/Controls/QuickAccessToolBar.cs @@ -187,12 +187,64 @@ public ItemCollectionWithLogicalTreeSupport QuickAccessItem if (this.quickAccessItems == null) { this.quickAccessItems = new ItemCollectionWithLogicalTreeSupport(this); + this.quickAccessItems.CollectionChanged += this.OnQuickAccessItemsCollectionChanged; } return this.quickAccessItems; } } + /// + /// Handles collection of quick access menu items changes + /// + /// Sender + /// The event data + private void OnQuickAccessItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (this.MenuDownButton is null) + { + return; + } + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var item in e.NewItems.OfType()) + { + var index = this.QuickAccessItems.IndexOf(item); + this.MenuDownButton.Items.Insert(index + 1, item); + this.QuickAccessItems[index].InvalidateProperty(QuickAccessMenuItem.TargetProperty); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var item in e.OldItems.OfType()) + { + this.MenuDownButton.Items.Remove(item); + item.InvalidateProperty(QuickAccessMenuItem.TargetProperty); + } + + break; + + case NotifyCollectionChangedAction.Replace: + foreach (var item in e.OldItems.OfType()) + { + this.MenuDownButton.Items.Remove(item); + item.InvalidateProperty(QuickAccessMenuItem.TargetProperty); + } + + foreach (var item in e.NewItems.OfType()) + { + var index = this.QuickAccessItems.IndexOf(item); + this.MenuDownButton.Items.Insert(index + 1, item); + this.QuickAccessItems[index].InvalidateProperty(QuickAccessMenuItem.TargetProperty); + } + + break; + } + } + #endregion #region ShowAboveRibbon diff --git a/Fluent.Ribbon/Controls/Ribbon.cs b/Fluent.Ribbon/Controls/Ribbon.cs index ca53bc6f1..17165e6f7 100644 --- a/Fluent.Ribbon/Controls/Ribbon.cs +++ b/Fluent.Ribbon/Controls/Ribbon.cs @@ -491,10 +491,10 @@ private static void OnContextMenuOpened(object sender, RoutedEventArgs e) private ObservableCollection keyTipKeys; // Collection of contextual tab groups - private ItemCollectionWithLogicalTreeSupport contextualGroups; + private ObservableCollection contextualGroups; // Collection of tabs - private ItemCollectionWithLogicalTreeSupport tabs; + private ObservableCollection tabs; // Collection of toolbar items private ObservableCollection toolBarItems; @@ -824,13 +824,13 @@ public double QuickAccessToolBarHeight /// Gets collection of contextual tab groups /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - public ItemCollectionWithLogicalTreeSupport ContextualGroups + public ObservableCollection ContextualGroups { get { if (this.contextualGroups == null) { - this.contextualGroups = new ItemCollectionWithLogicalTreeSupport(this); + this.contextualGroups = new ObservableCollection(); } return this.contextualGroups; @@ -841,13 +841,13 @@ public ItemCollectionWithLogicalTreeSupport Contextual /// gets collection of ribbon tabs /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - public ItemCollectionWithLogicalTreeSupport Tabs + public ObservableCollection Tabs { get { if (this.tabs == null) { - this.tabs = new ItemCollectionWithLogicalTreeSupport(this); + this.tabs = new ObservableCollection(); } return this.tabs; @@ -1906,16 +1906,6 @@ protected override IEnumerator LogicalChildren yield return this.TabControl.ToolbarPanel; } - foreach (var item in this.Tabs.GetLogicalChildren()) - { - yield return item; - } - - foreach (var item in this.ContextualGroups.GetLogicalChildren()) - { - yield return item; - } - if (this.layoutRoot != null) { yield return this.layoutRoot; diff --git a/Fluent.Ribbon/Controls/RibbonGroupBox.cs b/Fluent.Ribbon/Controls/RibbonGroupBox.cs index 135d3ff0a..68077960a 100644 --- a/Fluent.Ribbon/Controls/RibbonGroupBox.cs +++ b/Fluent.Ribbon/Controls/RibbonGroupBox.cs @@ -4,6 +4,7 @@ namespace Fluent using System; using System.Collections; using System.Collections.Generic; + using System.Collections.Specialized; using System.ComponentModel; using System.Windows; using System.Windows.Automation.Peers; @@ -243,6 +244,22 @@ private void ScaleScaleableItems(ScaleDirection scaleDirection) } } + private void ResetScaleableItems() + { + foreach (var item in this.Items) + { + var scalableRibbonControl = this.ItemContainerGenerator.ContainerOrContainerContentFromItem(item); + + if (scalableRibbonControl is null + || (scalableRibbonControl is UIElement uiElement && uiElement.Visibility != Visibility.Visible)) + { + continue; + } + + scalableRibbonControl.ResetScale(); + } + } + private void OnScalableControlScaled(object sender, EventArgs e) { this.TryClearCache(); @@ -251,7 +268,7 @@ private void OnScalableControlScaled(object sender, EventArgs e) /// /// Gets or sets whether to reset cache when scalable control is scaled /// - internal bool SuppressCacheReseting { get; set; } + internal ScopeGuard CacheResetGuard { get; } private void UpdateScalableControlSubscritions(bool registerEvents) { @@ -607,19 +624,19 @@ static RibbonGroupBox() private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var box = (RibbonGroupBox)d; - box.ClearCache(); + box.TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer(); } private static void OnFontSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var box = (RibbonGroupBox)d; - box.ClearCache(); + box.TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer(); } private static void OnFontFamilyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var box = (RibbonGroupBox)d; - box.ClearCache(); + box.TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer(); } /// @@ -627,6 +644,8 @@ private static void OnFontFamilyChanged(DependencyObject d, DependencyPropertyCh /// public RibbonGroupBox() { + this.CacheResetGuard = new ScopeGuard(() => this.UpdateScalableControlSubscritions(false), () => this.UpdateScalableControlSubscritions(true)); + this.CoerceValue(ContextMenuProperty); this.Focusable = false; @@ -790,39 +809,83 @@ internal Size DesiredSizeIntermediate { var contentHeight = UIHelper.GetParent(this)?.ContentHeight ?? RibbonTabControl.DefaultContentHeight; - this.SuppressCacheReseting = true; - this.UpdateScalableControlSubscritions(true); - - // Get desired size for these values - var backupState = this.State; - var backupScale = this.Scale; - this.State = this.StateIntermediate; - this.Scale = this.ScaleIntermediate; - this.InvalidateLayout(); - this.Measure(new Size(double.PositiveInfinity, contentHeight)); - this.cachedMeasures.Remove(stateScale); - this.cachedMeasures.Add(stateScale, this.DesiredSize); - result = this.DesiredSize; - - // Rollback changes - this.State = backupState; - this.Scale = backupScale; - this.InvalidateLayout(); - this.Measure(new Size(double.PositiveInfinity, contentHeight)); - - this.SuppressCacheReseting = false; + using (this.CacheResetGuard.Start()) + { + // Get desired size for these values + var backupState = this.State; + var backupScale = this.Scale; + this.State = this.StateIntermediate; + this.Scale = this.ScaleIntermediate; + this.InvalidateLayout(); + this.Measure(new Size(double.PositiveInfinity, contentHeight)); + this.cachedMeasures.Remove(stateScale); + this.cachedMeasures.Add(stateScale, this.DesiredSize); + result = this.DesiredSize; + + // Rollback changes + this.State = backupState; + this.Scale = backupScale; + this.InvalidateLayout(); + this.Measure(new Size(double.PositiveInfinity, contentHeight)); + + this.UpdateScalableControlSubscritions(true); + } } return result; } } - private void TryClearCache() + private bool TryClearCache() { - if (this.SuppressCacheReseting == false) + if (this.CacheResetGuard.IsActive == false) { this.ClearCache(); + return true; + } + + return false; + } + + internal bool TryClearCacheAndResetStateAndScale() + { + if (this.CacheResetGuard.IsActive + || this.IsLoaded == false) + { + return false; + } + + this.UpdateScalableControlSubscritions(false); + + this.State = RibbonGroupBoxState.Large; + this.Scale = 0; + this.StateIntermediate = RibbonGroupBoxState.Large; + this.ScaleIntermediate = 0; + this.ClearCache(); + + this.ResetScaleableItems(); + + this.UpdateScalableControlSubscritions(true); + return true; + } + + /// + /// Tries to clear the cache, reset the state and reset the scale. + /// If that succeeds the parent is notified about that. + /// + /// true if the cache was reset. Otherwise false. + public bool TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer() + { + // We should try to clear the entire cache. + // The entire cache should only be cleared if we don't do regular measuring, but only if some event outside our own measuring code caused size changes (such as elements getting visible/invisible or being added/removed). + // For reference https://github.com/fluentribbon/Fluent.Ribbon/issues/834 + if (this.TryClearCacheAndResetStateAndScale()) + { + UIHelper.GetParent(this)?.GroupBoxCacheClearedAndStateAndScaleResetted(this); + return true; } + + return false; } /// @@ -908,6 +971,14 @@ private void OnPopupClosed(object sender, EventArgs e) this.DropDownClosed?.Invoke(this, e); } + /// + protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) + { + base.OnItemsChanged(e); + + this.TryClearCacheAndResetStateAndScaleAndNotifyParentRibbonGroupsContainer(); + } + /// protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { @@ -938,13 +1009,8 @@ protected override void OnChildDesiredSizeChanged(UIElement child) { base.OnChildDesiredSizeChanged(child); - // We must clear the current cached measure. + // We must always clear the current cached measure. this.cachedMeasures.Remove(this.GetCurrentIntermediateStateScale()); - - // We should try to clear the entire cache. - // The entire cache is only cleared when we don't do regular measuring, but only if some event outside our own measuring code caused size changes. - // For reference https://github.com/fluentribbon/Fluent.Ribbon/issues/834 - this.TryClearCache(); } private StateScale GetCurrentIntermediateStateScale() diff --git a/Fluent.Ribbon/Controls/RibbonGroupsContainer.cs b/Fluent.Ribbon/Controls/RibbonGroupsContainer.cs index 4978e6982..dd1aaff25 100644 --- a/Fluent.Ribbon/Controls/RibbonGroupsContainer.cs +++ b/Fluent.Ribbon/Controls/RibbonGroupsContainer.cs @@ -155,12 +155,13 @@ protected override Size MeasureOverride(Size availableSize) if (groupBox.State != groupBox.StateIntermediate || groupBox.Scale != groupBox.ScaleIntermediate) { - groupBox.SuppressCacheReseting = true; - groupBox.State = groupBox.StateIntermediate; - groupBox.Scale = groupBox.ScaleIntermediate; - groupBox.InvalidateLayout(); - groupBox.Measure(new Size(double.PositiveInfinity, availableSize.Height)); - groupBox.SuppressCacheReseting = false; + using (groupBox.CacheResetGuard.Start()) + { + groupBox.State = groupBox.StateIntermediate; + groupBox.Scale = groupBox.ScaleIntermediate; + groupBox.InvalidateLayout(); + groupBox.Measure(new Size(double.PositiveInfinity, availableSize.Height)); + } } // Something wrong with cache? @@ -597,5 +598,30 @@ private static double CoerceOffset(double offset, double extent, double viewport } #endregion + + // We have to reset the reduce order to it's initial value, clear all caches we keep here and invalidate measure/arrange + internal void GroupBoxCacheClearedAndStateAndScaleResetted(RibbonGroupBox ribbonGroupBox) + { + var ribbonPanel = this; + + var newReduceOrderIndex = ribbonPanel.reduceOrder.Length - 1; + ribbonPanel.reduceOrderIndex = newReduceOrderIndex; + + this.measureCache = default; + + foreach (var item in this.InternalChildren) + { + var groupBox = item as RibbonGroupBox; + if (groupBox is null) + { + continue; + } + + groupBox.TryClearCacheAndResetStateAndScale(); + } + + ribbonPanel.InvalidateMeasure(); + ribbonPanel.InvalidateArrange(); + } } } \ No newline at end of file diff --git a/Fluent.Ribbon/Controls/RibbonGroupsContainerScrollViewer.cs b/Fluent.Ribbon/Controls/RibbonGroupsContainerScrollViewer.cs new file mode 100644 index 000000000..1d2508192 --- /dev/null +++ b/Fluent.Ribbon/Controls/RibbonGroupsContainerScrollViewer.cs @@ -0,0 +1,17 @@ +// ReSharper disable once CheckNamespace +namespace Fluent +{ + using System.Windows; + using System.Windows.Controls; + + /// + /// Represents a specific to . + /// + public class RibbonGroupsContainerScrollViewer : ScrollViewer + { + static RibbonGroupsContainerScrollViewer() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(RibbonGroupsContainerScrollViewer), new FrameworkPropertyMetadata(typeof(RibbonGroupsContainerScrollViewer))); + } + } +} \ No newline at end of file diff --git a/Fluent.Ribbon/Controls/RibbonScrollViewer.cs b/Fluent.Ribbon/Controls/RibbonScrollViewer.cs index b99c5688f..85724f265 100644 --- a/Fluent.Ribbon/Controls/RibbonScrollViewer.cs +++ b/Fluent.Ribbon/Controls/RibbonScrollViewer.cs @@ -5,7 +5,7 @@ namespace Fluent using System.Windows.Media; /// - /// Represents ScrollViewer with modified hit test + /// Represents with modified hit test /// public class RibbonScrollViewer : ScrollViewer { diff --git a/Fluent.Ribbon/Controls/RibbonTabItem.cs b/Fluent.Ribbon/Controls/RibbonTabItem.cs index b8d948838..c57d984fa 100644 --- a/Fluent.Ribbon/Controls/RibbonTabItem.cs +++ b/Fluent.Ribbon/Controls/RibbonTabItem.cs @@ -108,7 +108,7 @@ public string KeyTip /// /// Gets ribbon groups container /// - public ScrollViewer GroupsContainer { get; } = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Disabled }; + public ScrollViewer GroupsContainer { get; } = new RibbonGroupsContainerScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Disabled }; /// /// Gets or sets whether ribbon is minimized @@ -614,12 +614,13 @@ protected override void OnPreviewGotKeyboardFocus(KeyboardFocusChangedEventArgs /// protected override Size MeasureOverride(Size constraint) { - if (this.contentContainer == null) + if (this.contentContainer is null) { return base.MeasureOverride(constraint); } - if (this.IsContextual && this.Group != null && this.Group.Visibility == Visibility.Collapsed) + if (this.IsContextual + && this.Group?.Visibility == Visibility.Collapsed) { return Size.Empty; } diff --git a/Fluent.Ribbon/Controls/UniformGridWithItemSize.cs b/Fluent.Ribbon/Controls/UniformGridWithItemSize.cs index 89418b50b..d35fc59ba 100644 --- a/Fluent.Ribbon/Controls/UniformGridWithItemSize.cs +++ b/Fluent.Ribbon/Controls/UniformGridWithItemSize.cs @@ -5,6 +5,7 @@ namespace Fluent using System.Diagnostics; using System.Windows; using System.Windows.Controls; + using System.Windows.Data; using Fluent.Internal; using Fluent.Internal.KnownBoxes; @@ -18,12 +19,33 @@ public class UniformGridWithItemSize : Panel private int nonCollapsedCount; private int usedColumns; + /// + /// Gets or sets panel orientation + /// + public Orientation Orientation + { + get { return (Orientation)this.GetValue(OrientationProperty); } + set { this.SetValue(OrientationProperty, value); } + } + + /// + /// for . + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(UniformGridWithItemSize), + new FrameworkPropertyMetadata( + Orientation.Horizontal, + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure)); + /// /// Specifies the number of maximum columns in the grid /// public int MinColumns { - get => (int)this.GetValue(MinColumnsProperty); + get => this.Orientation == Orientation.Horizontal ? (int)this.GetValue(MinColumnsProperty) : 1; set => this.SetValue(MinColumnsProperty, value); } @@ -37,7 +59,7 @@ public int MinColumns typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata( IntBoxes.Zero, - FrameworkPropertyMetadataOptions.AffectsMeasure), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure), ValidateMinColumns); private static bool ValidateMinColumns(object o) @@ -50,7 +72,7 @@ private static bool ValidateMinColumns(object o) /// public int MaxColumns { - get => (int)this.GetValue(MaxColumnsProperty); + get => this.Orientation == Orientation.Horizontal ? (int)this.GetValue(MaxColumnsProperty) : 1; set => this.SetValue(MaxColumnsProperty, value); } @@ -64,7 +86,7 @@ public int MaxColumns typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata( IntBoxes.Zero, - FrameworkPropertyMetadataOptions.AffectsMeasure), + FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure), ValidateMaxColumns); private static bool ValidateMaxColumns(object o) @@ -76,7 +98,7 @@ private static bool ValidateMaxColumns(object o) /// DependencyProperty for property. /// public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register( - nameof(ItemWidth), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure)); + nameof(ItemWidth), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure)); /// /// Specifies the item width. @@ -91,7 +113,7 @@ public double ItemWidth /// DependencyProperty for property. /// public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register( - nameof(ItemHeight), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure)); + nameof(ItemHeight), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure)); /// /// Specifies the item height. @@ -116,8 +138,6 @@ public double ItemHeight /// Desired size protected override Size MeasureOverride(Size constraint) { - Debug.WriteLine($"MeasureOverride: {constraint}"); - this.UpdateComputedValues(this.MaxColumns); var useDefinedItemWidth = double.IsNaN(this.ItemWidth) == false && DoubleUtil.AreClose(this.ItemWidth, 0) == false; @@ -188,8 +208,6 @@ protected override Size MeasureOverride(Size constraint) /// Arrange size protected override Size ArrangeOverride(Size arrangeSize) { - Debug.WriteLine($"ArrangeOverride: {arrangeSize}"); - var childBounds = new Rect(0, 0, arrangeSize.Width / this.usedColumns, arrangeSize.Height / this.rows); var xStep = childBounds.Width; var xBound = arrangeSize.Width; diff --git a/Fluent.Ribbon/IScalableRibbonControl.cs b/Fluent.Ribbon/IScalableRibbonControl.cs index 50cab13e5..719561e77 100644 --- a/Fluent.Ribbon/IScalableRibbonControl.cs +++ b/Fluent.Ribbon/IScalableRibbonControl.cs @@ -8,18 +8,23 @@ public interface IScalableRibbonControl { /// - /// Enlarge control size + /// Resets the scale. + /// + void ResetScale(); + + /// + /// Enlarge control size. /// void Enlarge(); /// - /// Reduce control size + /// Reduce control size. /// void Reduce(); /// - /// Occurs when contol is scaled + /// Occurs when contol is scaled. /// event EventHandler Scaled; } -} +} \ No newline at end of file diff --git a/Fluent.Ribbon/Internal/ScopeGuard.cs b/Fluent.Ribbon/Internal/ScopeGuard.cs index 8e3dc0d36..0275117e3 100644 --- a/Fluent.Ribbon/Internal/ScopeGuard.cs +++ b/Fluent.Ribbon/Internal/ScopeGuard.cs @@ -27,14 +27,29 @@ public ScopeGuard(Action onEntry, Action onDispose) { this.onEntry = onEntry ?? throw new ArgumentNullException(nameof(onEntry)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - - this.onEntry(); } /// /// Gets whether this instance is still active (not disposed) or not. /// - public bool IsActive { get; private set; } = true; + public bool IsActive { get; private set; } + + /// + /// Starts the scope guard. + /// + /// The current instance for fluent usage. + public ScopeGuard Start() + { + if (this.IsActive) + { + return this; + } + + this.IsActive = true; + this.onEntry?.Invoke(); + + return this; + } /// public void Dispose() diff --git a/Fluent.Ribbon/Properties/AssemblyInfo.cs b/Fluent.Ribbon/Properties/AssemblyInfo.cs index bf12531f6..8f1bb3e98 100644 --- a/Fluent.Ribbon/Properties/AssemblyInfo.cs +++ b/Fluent.Ribbon/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows.Markup; diff --git a/Fluent.Ribbon/Services/KeyTipService.cs b/Fluent.Ribbon/Services/KeyTipService.cs index 373b3dc4b..b7b663b9b 100644 --- a/Fluent.Ribbon/Services/KeyTipService.cs +++ b/Fluent.Ribbon/Services/KeyTipService.cs @@ -205,7 +205,7 @@ private void OnWindowPreviewKeyDown(object sender, KeyEventArgs e) return; } - using var scopeGuard = new ScopeGuard(); + using var scopeGuard = new ScopeGuard().Start(); this.windowPreviewKeyDownScopeGuard = scopeGuard; if (e.IsRepeat diff --git a/Fluent.Ribbon/Themes/Common.xaml b/Fluent.Ribbon/Themes/Common.xaml index cff316124..6470038e1 100644 --- a/Fluent.Ribbon/Themes/Common.xaml +++ b/Fluent.Ribbon/Themes/Common.xaml @@ -45,4 +45,6 @@ diff --git a/Fluent.Ribbon/Themes/Controls/GalleryItem.xaml b/Fluent.Ribbon/Themes/Controls/GalleryItem.xaml index 9e33ad558..81a3a0028 100644 --- a/Fluent.Ribbon/Themes/Controls/GalleryItem.xaml +++ b/Fluent.Ribbon/Themes/Controls/GalleryItem.xaml @@ -15,13 +15,13 @@ Visibility="Collapsed" Background="{DynamicResource Fluent.Ribbon.Brushes.GalleryItem.Selected}" /> - - + + + + + + + diff --git a/Shared/GlobalAssemblyInfo.cs b/Shared/GlobalAssemblyInfo.cs index 063114e32..aaa673752 100644 --- a/Shared/GlobalAssemblyInfo.cs +++ b/Shared/GlobalAssemblyInfo.cs @@ -8,7 +8,6 @@ [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: CLSCompliant(false)] [assembly: NeutralResourcesLanguage("en-US")] [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] \ No newline at end of file