From cb6eb4397255776038db781ffbcee9f2a480c53c Mon Sep 17 00:00:00 2001 From: John Lewin Date: Thu, 13 Jun 2019 08:16:50 -0700 Subject: [PATCH] Add support for grouping scene operations into a drop menu button - Issue MatterHackers/MCCentral#5664 consider adding a dual align quick button --- .../ApplicationView/ApplicationController.cs | 99 ++++++----- .../ApplicationView/Themes/ThemeConfig.cs | 59 +++++++ MatterControlLib/CustomWidgets/NamedAction.cs | 14 ++ .../CustomWidgets/SimpleButton.cs | 5 +- .../PartPreviewWindow/ViewControls3D.cs | 164 +++++++++++------- .../MatterControl/MatterControlUtilities.cs | 2 +- 6 files changed, 229 insertions(+), 114 deletions(-) diff --git a/MatterControlLib/ApplicationView/ApplicationController.cs b/MatterControlLib/ApplicationView/ApplicationController.cs index 1d413b02e..890e73ae6 100644 --- a/MatterControlLib/ApplicationView/ApplicationController.cs +++ b/MatterControlLib/ApplicationView/ApplicationController.cs @@ -783,53 +783,61 @@ namespace MatterHackers.MatterControl Icon = (invertIcon) => AggContext.StaticData.LoadIcon("lay_flat.png", 16, 16).SetPreMultiply(), }, new SceneSelectionSeparator(), - new SceneSelectionOperation() + new OperationGroup() { - OperationType = typeof(CombineObject3D_2), - TitleResolver = () => "Combine".Localize(), - Action = (sceneContext) => new CombineObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), - Icon = (invertIcon) => AggContext.StaticData.LoadIcon("combine.png").SetPreMultiply(), - IsEnabled = (scene) => + StickySelection = true, + IsEnabled = (scene) => scene.SelectedItem?.VisibleMeshes().Count() > 1, + Operations = new List() { - var selectedItem = scene.SelectedItem; - return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; - }, - }, - new SceneSelectionOperation() - { - OperationType = typeof(SubtractObject3D_2), - TitleResolver = () => "Subtract".Localize(), - Action = (sceneContext) => new SubtractObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), - Icon = (invertIcon) => AggContext.StaticData.LoadIcon("subtract.png").SetPreMultiply(), - IsEnabled = (scene) => - { - var selectedItem = scene.SelectedItem; - return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; - }, - }, - new SceneSelectionOperation() - { - OperationType = typeof(IntersectionObject3D_2), - TitleResolver = () => "Intersect".Localize(), - Action = (sceneContext) => new IntersectionObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), - Icon = (invertIcon) => AggContext.StaticData.LoadIcon("intersect.png"), - IsEnabled = (scene) => - { - var selectedItem = scene.SelectedItem; - return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; - }, - }, - new SceneSelectionOperation() - { - OperationType = typeof(SubtractAndReplaceObject3D_2), - TitleResolver = () => "Subtract & Replace".Localize(), - Action = (sceneContext) => new SubtractAndReplaceObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), - Icon = (invertIcon) => AggContext.StaticData.LoadIcon("subtract_and_replace.png").SetPreMultiply(), - IsEnabled = (scene) => - { - var selectedItem = scene.SelectedItem; - return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; - }, + new SceneSelectionOperation() + { + OperationType = typeof(CombineObject3D_2), + TitleResolver = () => "Combine".Localize(), + Action = (sceneContext) => new CombineObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), + Icon = (invertIcon) => AggContext.StaticData.LoadIcon("combine.png").SetPreMultiply(), + IsEnabled = (scene) => + { + var selectedItem = scene.SelectedItem; + return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; + }, + }, + new SceneSelectionOperation() + { + OperationType = typeof(SubtractObject3D_2), + TitleResolver = () => "Subtract".Localize(), + Action = (sceneContext) => new SubtractObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), + Icon = (invertIcon) => AggContext.StaticData.LoadIcon("subtract.png").SetPreMultiply(), + IsEnabled = (scene) => + { + var selectedItem = scene.SelectedItem; + return selectedItem != null && scene.SelectedItem.VisibleMeshes().Count() > 1; + }, + }, + new SceneSelectionOperation() + { + OperationType = typeof(IntersectionObject3D_2), + TitleResolver = () => "Intersect".Localize(), + Action = (sceneContext) => new IntersectionObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), + Icon = (invertIcon) => AggContext.StaticData.LoadIcon("intersect.png"), + IsEnabled = (scene) => + { + var selectedItem = scene.SelectedItem; + return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; + }, + }, + new SceneSelectionOperation() + { + OperationType = typeof(SubtractAndReplaceObject3D_2), + TitleResolver = () => "Subtract & Replace".Localize(), + Action = (sceneContext) => new SubtractAndReplaceObject3D_2().WrapSelectedItemAndSelect(sceneContext.Scene), + Icon = (invertIcon) => AggContext.StaticData.LoadIcon("subtract_and_replace.png").SetPreMultiply(), + IsEnabled = (scene) => + { + var selectedItem = scene.SelectedItem; + return selectedItem != null && selectedItem.VisibleMeshes().Count() > 1; + }, + } + } }, new SceneSelectionSeparator(), new SceneSelectionOperation() @@ -1176,6 +1184,7 @@ namespace MatterHackers.MatterControl } static int applicationInstanceCount = 0; + public static int ApplicationInstanceCount { get diff --git a/MatterControlLib/ApplicationView/Themes/ThemeConfig.cs b/MatterControlLib/ApplicationView/Themes/ThemeConfig.cs index 7895b575d..043897e33 100644 --- a/MatterControlLib/ApplicationView/Themes/ThemeConfig.cs +++ b/MatterControlLib/ApplicationView/Themes/ThemeConfig.cs @@ -410,6 +410,45 @@ namespace MatterHackers.MatterControl return popupMenu; } + public PopupMenuButton CreateSplitButton(SplitButtonParams buttonParams) + { + PopupMenuButton menuButton = null; + + var innerButton = new IconButton(buttonParams.Icon, this) + { + Name = buttonParams.ButtonName + " Inner SplitButton", + ToolTipText = buttonParams.DefaultActionTooltip, + }; + + innerButton.Click += (s, e) => + { + buttonParams.DefaultAction.Invoke(menuButton); + }; + + // Remove right Padding for drop style + innerButton.Padding = innerButton.Padding.Clone(right: 0); + + menuButton = new PopupMenuButton(innerButton, this) + { + DynamicPopupContent = () => + { + var popupMenu = new PopupMenu(ApplicationController.Instance.MenuTheme); + buttonParams.ExtendPopupMenu?.Invoke(popupMenu); + + return popupMenu; + }, + Name = buttonParams.ButtonName + " Menu SplitButton", + BackgroundColor = this.ToolbarButtonBackground, + HoverColor = this.ToolbarButtonHover, + MouseDownColor = this.ToolbarButtonDown, + DrawArrow = true, + Margin = this.ButtonSpacing, + }; + + innerButton.Selectable = true; + return menuButton; + } + private static ImageBuffer ColorCircle(int size, Color color) { ImageBuffer imageBuffer = new ImageBuffer(size, size); @@ -546,15 +585,35 @@ namespace MatterHackers.MatterControl public class PresetColors { public Color MaterialPreset { get; set; } = Color.Orange; + public Color QualityPreset { get; set; } = Color.Yellow; + public Color UserOverride { get; set; } = new Color(68, 95, 220, 150); } public class GridColors { public Color Red { get; set; } + public Color Green { get; set; } + public Color Blue { get; set; } + public Color Line { get; set; } } + + public class SplitButtonParams + { + public ImageBuffer Icon { get; set; } + + public Action DefaultAction { get; set; } + + public string DefaultActionTooltip { get; set; } + + public Action MenuAction { get; set; } + + public Action ExtendPopupMenu { get; set; } + + public string ButtonName { get; set; } + } } \ No newline at end of file diff --git a/MatterControlLib/CustomWidgets/NamedAction.cs b/MatterControlLib/CustomWidgets/NamedAction.cs index 3de0e88cb..5b9ecdbf0 100644 --- a/MatterControlLib/CustomWidgets/NamedAction.cs +++ b/MatterControlLib/CustomWidgets/NamedAction.cs @@ -49,9 +49,23 @@ namespace MatterHackers.Agg.UI public Func TitleResolver { get; set; } public string Title => this.TitleResolver?.Invoke(); + + public Func HelpTextResolver { get; set; } + + public string HelpText => this.HelpTextResolver?.Invoke(); } public class SceneSelectionSeparator : SceneSelectionOperation { } + + public class OperationGroup : SceneSelectionOperation + { + public List Operations { get; set; } = new List(); + + public bool StickySelection { get; internal set; } + + public string GroupName { get; set; } + } + } \ No newline at end of file diff --git a/MatterControlLib/CustomWidgets/SimpleButton.cs b/MatterControlLib/CustomWidgets/SimpleButton.cs index 7f4cbe194..dea1a0b5e 100644 --- a/MatterControlLib/CustomWidgets/SimpleButton.cs +++ b/MatterControlLib/CustomWidgets/SimpleButton.cs @@ -252,14 +252,11 @@ namespace MatterHackers.MatterControl.CustomWidgets public ImageBuffer IconImage => this.Enabled ? image : this.DisabledImage; - /// - /// Switch icons without computing disabled image - use case for non-disableable toggle widgets - /// - /// internal void SetIcon(ImageBuffer icon) { image = icon; imageWidget.Image = icon; + _disabledImage = null; } private ImageBuffer _disabledImage; diff --git a/MatterControlLib/PartPreviewWindow/ViewControls3D.cs b/MatterControlLib/PartPreviewWindow/ViewControls3D.cs index 091e1dd4f..da8a56c99 100644 --- a/MatterControlLib/PartPreviewWindow/ViewControls3D.cs +++ b/MatterControlLib/PartPreviewWindow/ViewControls3D.cs @@ -68,6 +68,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow public class ViewModeChangedEventArgs : EventArgs { public PartViewMode ViewMode { get; set; } + public PartViewMode PreviousMode { get; set; } } @@ -454,7 +455,67 @@ namespace MatterHackers.MatterControl.PartPreviewWindow GuiWidget button; - if (namedAction.Icon != null) + if (namedAction is OperationGroup operationGroup) + { + SceneSelectionOperation defaultOperation; + + string groupRecordID = $"ActiveButton_{operationGroup.GroupName}_Group"; + + if (operationGroup.StickySelection) + { + int.TryParse(UserSettings.Instance.get(groupRecordID), out int activeButtonID); + + activeButtonID = agg_basics.Clamp(activeButtonID, 0, operationGroup.Operations.Count - 1); + + defaultOperation = operationGroup.Operations[activeButtonID]; + } + else + { + defaultOperation = operationGroup.Operations.First(); + } + + PopupMenuButton groupButton = null; + + groupButton = theme.CreateSplitButton(new SplitButtonParams() + { + Icon = defaultOperation.Icon(theme.InvertIcons), + DefaultAction = (menuButton) => + { + defaultOperation.Action.Invoke(sceneContext); + }, + DefaultActionTooltip = defaultOperation.HelpText ?? defaultOperation.Title, + ButtonName = defaultOperation.Title, + ExtendPopupMenu = (PopupMenu popupMenu) => + { + foreach (var operation in operationGroup.Operations) + { + var operationMenu = popupMenu.CreateMenuItem(operation.Title, operation.Icon?.Invoke(theme.InvertIcons)); + operationMenu.Click += (s, e) => UiThread.RunOnIdle(() => + { + if (operationGroup.StickySelection + && defaultOperation != operation) + { + // Update button + var iconButton = groupButton.Children.OfType().First(); + iconButton.SetIcon(operation.Icon(theme.InvertIcons)); + iconButton.ToolTipText = operation.HelpText ?? operation.Title; + + UserSettings.Instance.set(groupRecordID, operationGroup.Operations.IndexOf(operation).ToString()); + + defaultOperation = operation; + + iconButton.Invalidate(); + } + + operation.Action?.Invoke(sceneContext); + }); + } + } + }); + + button = groupButton; + } + else if (namedAction.Icon != null) { // add the create support before the align if (namedAction.OperationType == typeof(AlignObject3D)) @@ -487,16 +548,18 @@ namespace MatterHackers.MatterControl.PartPreviewWindow operationButtons.Add((button, namedAction)); - button.Click += (s, e) => + // Only bind Click event if not a SplitButton + if (!(button is PopupMenuButton)) { - UiThread.RunOnIdle(() => + button.Click += (s, e) => UiThread.RunOnIdle(() => { namedAction.Action.Invoke(sceneContext); var partTab = button.Parents().FirstOrDefault(); var view3D = partTab.Descendants().FirstOrDefault(); view3D.InteractionLayer.Focus(); }); - }; + } + this.AddChild(button); } @@ -776,83 +839,56 @@ namespace MatterHackers.MatterControl.PartPreviewWindow private GuiWidget CreateSaveButton(ThemeConfig theme) { - PopupMenuButton saveButton = null; - - var iconButton = new IconButton( - AggContext.StaticData.LoadIcon("save_grey_16x.png", 16, 16, theme.InvertIcons), - theme) + return theme.CreateSplitButton(new SplitButtonParams() { - ToolTipText = "Save".Localize(), - }; - - iconButton.Click += (s, e) => - { - ApplicationController.Instance.Tasks.Execute("Saving".Localize(), sceneContext.Printer, async(progress, cancellationToken) => + ButtonName = "Save", + Icon = AggContext.StaticData.LoadIcon("save_grey_16x.png", 16, 16, theme.InvertIcons), + DefaultAction = (menuButton) => { - saveButton.Enabled = false; - - try + ApplicationController.Instance.Tasks.Execute("Saving".Localize(), sceneContext.Printer, async (progress, cancellationToken) => { - await sceneContext.SaveChanges(progress, cancellationToken); - } - catch - { - } + menuButton.Enabled = false; - saveButton.Enabled = true; - }).ConfigureAwait(false); - }; + try + { + await sceneContext.SaveChanges(progress, cancellationToken); + } + catch (Exception ex) + { + ApplicationController.Instance.LogError("Error saving file".Localize() + ": " + ex.Message); + } - // Remove right Padding for drop style - iconButton.Padding = iconButton.Padding.Clone(right: 0); - - saveButton = new PopupMenuButton(iconButton, theme) - { - Name = "Save SplitButton", - ToolTipText = "Save As".Localize(), - DynamicPopupContent = () => + menuButton.Enabled = true; + }).ConfigureAwait(false); + }, + DefaultActionTooltip = "Save".Localize(), + ExtendPopupMenu = (PopupMenu popupMenu) => { - var popupMenu = new PopupMenu(ApplicationController.Instance.MenuTheme); - var saveAs = popupMenu.CreateMenuItem("Save As".Localize()); saveAs.Click += (s, e) => UiThread.RunOnIdle(() => { - UiThread.RunOnIdle(() => - { - DialogWindow.Show( - new SaveAsPage( - async (newName, destinationContainer) => + DialogWindow.Show( + new SaveAsPage( + (newName, destinationContainer) => + { + // Save to the destination provider + if (destinationContainer is ILibraryWritableContainer writableContainer) { - // Save to the destination provider - if (destinationContainer is ILibraryWritableContainer writableContainer) + // Wrap stream with ReadOnlyStream library item and add to container + writableContainer.Add(new[] { - // Wrap stream with ReadOnlyStream library item and add to container - writableContainer.Add(new[] - { new InMemoryLibraryItem(sceneContext.Scene) { Name = newName } - }); + }); - destinationContainer.Dispose(); - } - })); - }); + destinationContainer.Dispose(); + } + })); }); - - return popupMenu; - }, - BackgroundColor = theme.ToolbarButtonBackground, - HoverColor = theme.ToolbarButtonHover, - MouseDownColor = theme.ToolbarButtonDown, - DrawArrow = true, - Margin = theme.ButtonSpacing, - }; - - iconButton.Selectable = true; - - return saveButton; + } + }); } public override void OnClosed(EventArgs e) diff --git a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs index a65851203..8658e80a1 100644 --- a/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs +++ b/Tests/MatterControl.Tests/MatterControl/MatterControlUtilities.cs @@ -686,7 +686,7 @@ namespace MatterHackers.MatterControl.Tests.Automation public static void SaveBedplateToFolder(this AutomationRunner testRunner, string newFileName, string folderName) { - testRunner.ClickByName("Save SplitButton", offset: new Point2D(8, 0)); + testRunner.ClickByName("Save Menu SplitButton", offset: new Point2D(8, 0)); testRunner.ClickByName("Save As Menu Item");