/* Copyright (c) 2023, Lars Brubaker, John Lewin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using global::MatterControl.Printing; using Markdig.Agg; using Markdig.Syntax.Inlines; using MatterControlLib.Library.OpenInto; using MatterHackers.Agg; using MatterHackers.Agg.Font; using MatterHackers.Agg.Image; using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; using MatterHackers.Agg.VertexSource; using MatterHackers.DataConverters3D; using MatterHackers.ImageProcessing; using MatterHackers.Localizations; using MatterHackers.MatterControl.CustomWidgets; using MatterHackers.MatterControl.DataStorage; using MatterHackers.MatterControl.DesignTools; using MatterHackers.MatterControl.DesignTools.Operations; using MatterHackers.MatterControl.Extensibility; using MatterHackers.MatterControl.Library; using MatterHackers.MatterControl.PartPreviewWindow; using MatterHackers.MatterControl.Plugins; using MatterHackers.MatterControl.PrinterCommunication; using MatterHackers.MatterControl.PrinterControls.PrinterConnections; using MatterHackers.MatterControl.PrintHistory; using MatterHackers.MatterControl.PrintQueue; using MatterHackers.MatterControl.SettingsManagement; using MatterHackers.MatterControl.SlicerConfiguration; using MatterHackers.MatterControl.Tour; using MatterHackers.PolygonMesh; using MatterHackers.PolygonMesh.Processors; using MatterHackers.VectorMath; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; [assembly: InternalsVisibleTo("MatterControl.Tests")] [assembly: InternalsVisibleTo("MatterControl.AutomationTests")] [assembly: InternalsVisibleTo("CloudServices.Tests")] namespace MatterHackers.MatterControl { [JsonConverter(typeof(StringEnumConverter))] public enum NamedTypeFace { Alfa_Slab, Audiowide, Bangers, Courgette, Damion, Firefly_Sung, Fredoka, Great_Vibes, Liberation_Mono, Liberation_Sans, Liberation_Sans_Bold, Lobster, Nunito_Regular, Nunito_Bold, Nunito_Bold_Italic, Nunito_Italic, Pacifico, Poppins, Questrial, Righteous, Russo, Titan, Titillium, } public class ApplicationController { public event EventHandler ApplicationError; public event EventHandler ApplicationEvent; public HelpArticle HelpArticles { get; set; } public ThemeConfig Theme => AppContext.Theme; public ThemeConfig MenuTheme => AppContext.MenuTheme; public event EventHandler ShellFileOpened; public event EventHandler AnyPrintStarted; public event EventHandler AnyPrintCanceled; public event EventHandler AnyPrintComplete; public static string[] ShellFileExtensions => new string[] { ".stl", ".amf", ".3mf", ".obj", ".mcx", ".png", ".jpg", ".jpeg" }; public bool IsMatterControlPro() { var result = ApplicationController.Instance.UserHasPro?.Invoke(); if (result != null) { return result.Value; } return false; } public RunningTasksConfig Tasks { get; set; } = new RunningTasksConfig(); public IEnumerable ActivePrinters => this.Workspaces.Where(w => w.Printer != null).Select(w => w.Printer); public ExtensionsConfig Extensions { get; } public PopupMenu GetActionMenuForSceneItem(bool addInSubmenu, View3DWidget view3DWidget) { var menuTheme = this.MenuTheme; var popupMenu = new PopupMenu(menuTheme); var sceneContext = view3DWidget?.sceneContext; var selectedItem = sceneContext?.Scene?.SelectedItem; var selectedItemType = selectedItem?.GetType(); if (selectedItem == null) { return popupMenu; } if (!selectedItemType.IsDefined(typeof(ImmutableAttribute), false)) { AddActionMenuItems(sceneContext, addInSubmenu, menuTheme, popupMenu); } var workspaceActions = GetWorkspaceActions(view3DWidget); var printer = view3DWidget.Printer; var actions = new[] { new ActionSeparator(), workspaceActions["Edit"], workspaceActions["PasteInto"], new ActionSeparator(), new NamedAction() { Title = "Save As".Localize(), Action = () => UiThread.RunOnIdle(() => { DialogWindow.Show( new SaveAsPage( (container, newName) => { sceneContext.SaveAs(container, newName); })); }), IsEnabled = () => sceneContext.EditableScene }, new NamedAction() { ID = "Export", Title = "Export".Localize(), Icon = StaticData.Instance.LoadIcon("cube_export.png", 16, 16).SetToColor(MenuTheme.TextColor), Action = () => { Instance.ExportLibraryItems( new[] { new InMemoryLibraryItem(selectedItem) }, centerOnBed: false, printer: printer); } }, new ActionSeparator(), workspaceActions["Delete"] }; menuTheme.CreateMenuItems(popupMenu, actions); if (selectedItem is IRightClickMenuProvider menuProvider) { menuProvider.AddRightClickMenuItemsItems(popupMenu, menuTheme); } var parent = selectedItem.Parent; if (parent != null) { var orderChildrenByIndex = parent.GetType().GetCustomAttributes(typeof(OrderChildrenByIndexAttribute), true).Any(); if (orderChildrenByIndex) { AddReorderChildrenRightClickMenuItems(popupMenu, selectedItem); } } return popupMenu; } public void AddReorderChildrenRightClickMenuItems(PopupMenu popupMenu, IObject3D itemRightClicked) { popupMenu.CreateSeparator(); var parent = itemRightClicked.Parent; if(parent == null) { return; } // move to the top var moveTopItem = popupMenu.CreateMenuItem("↑↑ Move Top".Localize()); moveTopItem.Enabled = parent.Children.IndexOf(itemRightClicked) != 0; moveTopItem.Click += (s, e) => { parent.Children.Modify((list) => { list.Remove(itemRightClicked); list.Insert(0, itemRightClicked); }); }; // move up one position var moveUpItem = popupMenu.CreateMenuItem("↑ Move Up".Localize()); moveUpItem.Enabled = parent.Children.IndexOf(itemRightClicked) != 0; moveUpItem.Click += (s, e) => { parent.Children.Modify((list) => { var index = list.IndexOf(itemRightClicked); list.Remove(itemRightClicked); list.Insert(index - 1, itemRightClicked); }); }; // move down one position var moveDownItem = popupMenu.CreateMenuItem("↓ Move Down".Localize()); moveDownItem.Enabled = parent.Children.IndexOf(itemRightClicked) != parent.Children.Count - 1; moveDownItem.Click += (s, e) => { parent.Children.Modify((list) => { var index = list.IndexOf(itemRightClicked); list.Remove(itemRightClicked); list.Insert(index + 1, itemRightClicked); }); }; // move to the bottom var moveBottomItem = popupMenu.CreateMenuItem("↓↓ Move Bottom".Localize()); moveBottomItem.Enabled = parent.Children.IndexOf(itemRightClicked) != parent.Children.Count - 1; moveBottomItem.Click += (s, e) => { parent.Children.Modify((list) => { var index = list.IndexOf(itemRightClicked); list.Remove(itemRightClicked); list.Add(itemRightClicked); }); }; } public PopupMenu GetModifyMenu(ISceneContext sceneContext) { var popupMenu = new PopupMenu(this.MenuTheme); AddActionMenuItems(sceneContext, false, this.MenuTheme, popupMenu); return popupMenu; } private static void AddActionMenuItems(ISceneContext sceneContext, bool useSubMenu, ThemeConfig menuTheme, PopupMenu popupMenu) { var renameMenuItem = popupMenu.CreateMenuItem("Rename".Localize()); renameMenuItem.Click += (s, e) => { var scene = sceneContext.Scene; var selectedItem = scene.SelectedItem; if (selectedItem != null) { selectedItem.ShowRenameDialog(scene.UndoBuffer); } }; popupMenu.CreateSeparator(); if (useSubMenu) { // Create items in a 'Modify' sub-menu popupMenu.CreateSubMenu("Modify".Localize(), menuTheme, (modifyMenu) => SceneOperations.AddModifyItems(modifyMenu, menuTheme, sceneContext)); if (OpenIntoExecutable.FoundInstalledExecutable) { popupMenu.CreateSubMenu("Open With".Localize(), menuTheme, (modifyMenu) => OpenIntoExecutable.AddOption(modifyMenu, menuTheme, sceneContext)); } } else { // Create items directly in the referenced menu SceneOperations.AddModifyItems(popupMenu, menuTheme, sceneContext); } } public void PersistOpenTabsLayout() { // Project workspace definitions to serializable structure var workspaces = this.Workspaces .Where(w => w.SceneContext?.EditContext?.SourceFilePath?.Contains("\\Library\\CloudData") == false) .Select(w => { if (w.Printer == null) { return new PartWorkspace(w.SceneContext) { ContentPath = w.SceneContext.EditContext?.SourceFilePath, }; } else { return new PartWorkspace(w.Printer) { ContentPath = w.SceneContext.EditContext?.SourceFilePath, }; } }); lock (workspaces) { var content = JsonConvert.SerializeObject( workspaces, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); // Persist workspace definition to disk File.WriteAllText(ProfileManager.Instance.OpenTabsPath, content); } } public async Task PersistPrintTabsContent() { // Persist all pending changes in all workspaces to disk foreach (var workspace in this.Workspaces.ToArray()) { if (workspace.Printer != null) { await this.Tasks.Execute("Saving".Localize() + $" \"{workspace.Name}\" ...", workspace, workspace.SceneContext.SaveChanges); } } } internal void ExportAsMatterControlConfig(PrinterConfig printer) { AggContext.FileDialogs.SaveFileDialog( new SaveFileDialogParams("MatterControl Printer Export|*.printer", title: "Export Printer Settings") { FileName = printer.PrinterName }, (saveParams) => { try { if (!string.IsNullOrWhiteSpace(saveParams.FileName)) { File.WriteAllText(saveParams.FileName, JsonConvert.SerializeObject(printer.Settings, Formatting.Indented)); } } catch (Exception e) { UiThread.RunOnIdle(() => { StyledMessageBox.ShowMessageBox(e.Message, "Couldn't save file".Localize()); }); } }); } public void LogError(string errorMessage) { this.ApplicationError?.Invoke(this, errorMessage); } public void LogInfo(string message) { this.ApplicationEvent?.Invoke(this, message); } public Action RedeemDesignCode { get; set; } public Action EnterShareCode { get; set; } // check permission to an IObject3D instance #if DEBUG public Func UserHasPermission { get; set; } = (item) => true; #else public Func UserHasPermission { get; set; } = (item) => false; #endif // check permission to a purchase public Func UserHasPro { get; set; } public Func GetUnlockData { get; set; } public Func>> PushPrintTaskToServer { get; set; } private static ApplicationController globalInstance; public RootedObjectEventHandler CloudSyncStatusChanged { get; private set; } = new RootedObjectEventHandler(); public RootedObjectEventHandler DoneReloadingAll = new RootedObjectEventHandler(); public RootedObjectEventHandler ActiveProfileModified = new RootedObjectEventHandler(); public event EventHandler WorkspacesChanged; public event EventHandler ReloadSettingsTriggered; public void UpdateAllSettingsStyles(PrinterConfig printer) { var printerTabPage = this.MainView.Descendants().Where(page => page.Printer == printer).FirstOrDefault(); if (printerTabPage != null) { var sliceSettingsWidget = printerTabPage.Descendants().FirstOrDefault(); if (sliceSettingsWidget != null) { sliceSettingsWidget.UpdateAllStyles(); } } } public void ReloadSettings(PrinterConfig printer) { var printerTabPage = this.MainView.Descendants().Where(page => page.Printer == printer).FirstOrDefault(); if (printerTabPage != null) { Instance.IsReloading = true; var settingsContext = new SettingsContext( printer, null, NamedSettingsLayers.All); var sideBar = printerTabPage.Descendants().FirstOrDefault(); if (printer.ViewState.ConfigurePrinterVisible) { sideBar.ReplacePage( "Printer", new ConfigurePrinterWidget(settingsContext, printer, Theme) { HAnchor = HAnchor.Stretch, VAnchor = VAnchor.Stretch, }, false); } sideBar.ReplacePage("Slice Settings", new SliceSettingsWidget(printer, settingsContext, Theme)); Instance.IsReloading = false; } ReloadSettingsTriggered?.Invoke(null, null); } public static Action WebRequestFailed; public static Action WebRequestSucceeded; public static Action ChangeToPrintNotification = null; #if DEBUG public const string EnvironmentName = "TestEnv_"; #else public const string EnvironmentName = ""; #endif public bool ApplicationExiting { get; internal set; } = false; public static Func>> GetProfileHistory; public void OnWorkspacesChanged(PartWorkspace workspace, WorkspacesChangedEventArgs.OperationType operationType) { this.WorkspacesChanged?.Invoke(this, new WorkspacesChangedEventArgs( workspace, operationType)); if (operationType != WorkspacesChangedEventArgs.OperationType.Restore) { Instance.PersistOpenTabsLayout(); } } public string GetFavIconUrl(string oemName) { if (OemSettings.Instance.OemUrls.TryGetValue(oemName, out string oemUrl) && !string.IsNullOrWhiteSpace(oemUrl)) { return "https://www.google.com/s2/favicons?domain=" + oemUrl; } return null; } public void ClosePrinter(PrinterConfig printer, bool allowChangedEvent = true) { // Actually clear printer ProfileManager.Instance.ClosePrinter(printer.Settings.ID); // Shutdown the printer connection printer.Connection.Disable(); if (allowChangedEvent) { if (this.Workspaces.FirstOrDefault(w => w.Printer?.Settings.ID == printer.Settings.ID) is PartWorkspace workspace) { this.Workspaces.Remove(workspace); this.OnWorkspacesChanged(workspace, WorkspacesChangedEventArgs.OperationType.Remove); } } printer.Dispose(); } public static void LaunchBrowser(string targetUri) { UiThread.RunOnIdle(() => { if (!string.IsNullOrEmpty(OemSettings.Instance.AffiliateCode) && targetUri.Contains("matterhackers.com")) { string internalLink = ""; // if we have a trailing internal link if (targetUri.Contains("#")) { internalLink = targetUri.Substring(targetUri.IndexOf("#")); targetUri = targetUri.Substring(0, targetUri.Length - internalLink.Length); } if (targetUri.Contains("?")) { targetUri += $"&aff={OemSettings.Instance.AffiliateCode}"; } else { targetUri += $"?aff={OemSettings.Instance.AffiliateCode}"; } targetUri += internalLink; } ProcessStart(targetUri); }); } public static void ProcessStart(string input) { try { var p = new Process(); p.StartInfo = new ProcessStartInfo(input) { UseShellExecute = true }; p.Start(); } catch { // hack because of this: https://github.com/dotnet/corefx/issues/10361 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { input = input.Replace("&", "^&"); Process.Start(new ProcessStartInfo("cmd", $"/c start {input}") { CreateNoWindow = true }); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Process.Start("xdg-open", input); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Process.Start("open", input); } else { throw; } } } internal void MakeGrayscale(ImageBuffer sourceImage) { var buffer = sourceImage.GetBuffer(); int destIndex = 0; for (int y = 0; y < sourceImage.Height; y++) { for (int x = 0; x < sourceImage.Width; x++) { int b = buffer[destIndex + 0]; int g = buffer[destIndex + 1]; int r = buffer[destIndex + 2]; int c = (r * 77) + (g * 151) + (b * 28); byte gray = (byte)(c >> 8); buffer[destIndex + 0] = gray; buffer[destIndex + 1] = gray; buffer[destIndex + 2] = gray; destIndex += 4; } } } // Plugin Registration Points // Returns the user printer profile from the webservices plugin public static Func> GetPrinterProfileAsync; // Executes the user printer profile sync logic in the webservices plugin public static Func, Task> SyncCloudProfiles; public static Action QueueCloudProfileSync; // Returns all public printer profiles from the webservices plugin public static Func> GetPublicProfileList; // Returns the public printer profile from the webservices plugin public static Func> DownloadPublicProfileAsync; // Indicates if guest, rather than an authenticated user, is active public static Func GuestUserActive { get; set; } // Returns the authentication dialog from the authentication plugin public static Func GetAuthPage; public SlicePresetsPage AcitveSlicePresetsPage { get; set; } public MainViewWidget MainView; private readonly Dictionary> registeredLibraryActions = new Dictionary>(); public ThumbnailsConfig Thumbnails { get; } public Dictionary GetWorkspaceActions(View3DWidget view3DWidget) { var sceneContext = view3DWidget.sceneContext; var printer = sceneContext.Printer; var theme = Instance.MenuTheme; // Build workspace actions, each having a unique ID var actions = new[] { new NamedAction() { ID = "Print", Title = "Print".Localize(), Shortcut = "Ctrl+P", Action = view3DWidget.PushToPrinterAndPrint, IsEnabled = () => { if (sceneContext.Printer != null) { return sceneContext.Printer.PrintButtonEnabled(); } return sceneContext.EditableScene || (sceneContext.EditContext.SourceItem is ILibraryAsset libraryAsset && string.Equals(Path.GetExtension(libraryAsset.FileName), ".gcode", StringComparison.OrdinalIgnoreCase)); }, }, new NamedActionGroup() { ID = "Edit", Title = "Edit", Group = new NamedAction[] { new NamedAction() { ID = "Cut", Title = "Cut".Localize(), Action = () => sceneContext.Scene.Cut(), IsEnabled = () => sceneContext.Scene.SelectedItem != null }, new NamedAction() { ID = "Copy", Title = "Copy".Localize(), Action = () => sceneContext.Scene.Copy(), IsEnabled = () => sceneContext.Scene.SelectedItem != null }, new NamedAction() { ID = "Paste", Title = "Paste".Localize(), Action = () => sceneContext.Paste(), IsEnabled = () => Clipboard.Instance.ContainsImage || Clipboard.Instance.GetText() == "!--IObjectSelection--!" } }, IsEnabled = () => true, }, new NamedAction() { ID = "PasteInto", Title = "Paste Into".Localize(), Action = () => sceneContext.PasteIntoSelection(), IsEnabled = () => { var selectedItem = sceneContext.Scene.SelectedItem; var clipboardItem = ApplicationController.ClipboardItem; // there is an object in the clipboard if (Clipboard.Instance.ContainsText && Clipboard.Instance.GetText() == "!--IObjectSelection--!" // there is a selected item to paste into && selectedItem != null // the selected item is not a primitve && !(selectedItem is PrimitiveObject3D) && clipboardItem != null) { return true; } return false; } }, new NamedAction() { ID = "Delete", Icon = StaticData.Instance.LoadIcon("remove.png", 16, 16).SetToColor(theme.TextColor).SetPreMultiply(), Title = "Remove".Localize(), Action = sceneContext.Scene.DeleteSelection, IsEnabled = () => sceneContext.Scene.SelectedItem != null }, new NamedAction() { ID = "Export", Title = "Export".Localize(), Icon = StaticData.Instance.LoadIcon("cube_export.png", 16, 16).SetToColor(theme.TextColor), Action = () => { ApplicationController.Instance.ExportLibraryItems( new[] { new InMemoryLibraryItem(sceneContext.Scene) }, centerOnBed: false, printer: printer); }, IsEnabled = () => sceneContext.EditableScene || (sceneContext.EditContext.SourceItem is ILibraryAsset libraryAsset && string.Equals(Path.GetExtension(libraryAsset.FileName), ".gcode", StringComparison.OrdinalIgnoreCase)) }, new NamedAction() { ID = "Save", Title = "Save".Localize(), Shortcut = "Ctrl+S", Action = () => { ApplicationController.Instance.Tasks.Execute("Saving".Localize(), printer, sceneContext.SaveChanges).ConfigureAwait(false); }, IsEnabled = () => sceneContext.EditableScene }, new NamedAction() { ID = "SaveAs", Title = "Save As".Localize(), Action = () => UiThread.RunOnIdle(() => { DialogWindow.Show( new SaveAsPage( async (container, newName) => { sceneContext.SaveAs(container, newName); })); }), IsEnabled = () => sceneContext.EditableScene }, new NamedAction() { ID = "ArrangeAll", Title = "Arrange All Parts".Localize(), Action = async () => { await sceneContext.Scene.AutoArrangeChildren(view3DWidget.BedCenter).ConfigureAwait(false); }, IsEnabled = () => sceneContext.EditableScene, Icon = StaticData.Instance.LoadIcon("arrange_all.png", 16, 16).SetToColor(theme.TextColor), }, new NamedAction() { ID = "ClearBed", Title = "Clear Bed".Localize(), Action = () => { UiThread.RunOnIdle(() => { view3DWidget.ClearPlate(); }); } } }; // Construct dictionary from workspace actions by ID return actions.ToDictionary(a => a.ID); } public static void OpenFileWithSystemDialog(Action openFiles) { var extensionsWithoutPeriod = new HashSet(ApplicationSettings.OpenDesignFileParams.Split('|').First().Split(',').Select(t => t.Trim().Trim('.'))); foreach (var extension in ApplicationController.Instance.Library.ContentProviders.Keys) { extensionsWithoutPeriod.Add(extension.ToUpper()); } var extensionsArray = extensionsWithoutPeriod.OrderBy(t => t).ToArray(); string filter = string.Format( "{0}|{1}", string.Join(",", extensionsArray), string.Join("", extensionsArray.Select(t => $"*.{t.ToLower()};").ToArray())); UiThread.RunOnIdle(() => { AggContext.FileDialogs.OpenFileDialog( new OpenFileDialogParams(filter, multiSelect: true), (openParams) => { if (openParams != null && openParams.FileNames != null) { openFiles?.Invoke(openParams.FileNames); } }); }, .1); } public async Task OpenIntoNewTab(IEnumerable selectedLibraryItems) { await this.MainView.CreateNewDesignTab(false); var workspace = this.Workspaces.Last(); var insertionGroup = workspace.SceneContext.AddToPlate(selectedLibraryItems); // wait for the insertion to finish await insertionGroup.LoadingItemsTask; // then clear the undo buffer so we don't ask to save and undoing does not remove the starting part workspace.SceneContext.Scene.UndoBuffer.ClearHistory(); } internal void BlinkTab(ITab tab) { var theme = this.Theme; if (tab is GuiWidget guiWidget) { guiWidget.Descendants().FirstOrDefault().FlashBackground(theme.PrimaryAccentColor.WithContrast(theme.TextColor, 6).ToColor()); } } public void ShowApplicationHelp(string guideKey) { this.ActivateHelpTab(guideKey); } public void ShowAboutPage() { UiThread.RunOnIdle(() => { DialogWindow.Show(); }); } public ImageSequence GetProcessingSequence(Color color) { int size = (int)Math.Round(80 * GuiWidget.DeviceScale); var workingAnimation = new ImageSequence(); var frameCount = 30.0; var strokeWidth = 4 * GuiWidget.DeviceScale; for (int i = 0; i < frameCount; i++) { var frame = new ImageBuffer(size, size); var graphics = frame.NewGraphics2D(); graphics.Render(new Stroke(new Arc(frame.Width / 2, frame.Height / 2, size / 4 - strokeWidth / 2, size / 4 - strokeWidth / 2, MathHelper.Tau / frameCount * i, MathHelper.Tau / 4 + MathHelper.Tau / frameCount * i), strokeWidth), color); workingAnimation.AddImage(frame); } return workingAnimation; } private static int applicationInstanceCount = 0; public static int ApplicationInstanceCount { get { if (AggContext.OperatingSystem == OSType.Mac) { return 1; } if (applicationInstanceCount == 0) { var mcAssembly = Assembly.GetEntryAssembly(); if (mcAssembly != null) { string applicationName = Path.GetFileNameWithoutExtension(mcAssembly.Location).ToUpper(); Process[] p1 = Process.GetProcesses(); foreach (System.Diagnostics.Process pro in p1) { try { if (pro?.ProcessName != null && pro.ProcessName.ToUpper().Contains(applicationName)) { applicationInstanceCount++; } } catch { } } } } return applicationInstanceCount; } } public LibraryConfig Library { get; } public ILibraryContext LibraryTabContext { get; private set; } private void InitializeLibrary() { this.Library.RegisterContainer( new DynamicContainerLink( "Computer".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "computer_icon.png")), () => new ComputerCollectionContainer())); var rootLibraryCollection = Datastore.Instance.dbSQLite.Table().Where(v => v.Name == "_library").Take(1).FirstOrDefault(); if (rootLibraryCollection != null) { var forceAddLocalLibrary = false; #if DEBUG forceAddLocalLibrary = true; #endif // only add the local library if there are items in it var localLibrary = new SqliteLibraryContainer(rootLibraryCollection.Id); localLibrary.Load(); if (forceAddLocalLibrary || localLibrary.ChildContainers.Any() || localLibrary.Items.Any()) { this.Library.RegisterContainer( new DynamicContainerLink( "Local Library".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "local_library_icon.png")), () => localLibrary)); } } var forceAddQueue = false; #if DEBUG forceAddQueue = true; #endif // only add the queue if there are items in it var queueDirectory = LegacyQueueFiles.QueueDirectory; LegacyQueueFiles.ImportFromLegacy(); if (forceAddQueue || Directory.Exists(queueDirectory)) { // make sure the queue directory exists Directory.CreateDirectory(queueDirectory); this.Library.RegisterContainer(new DynamicContainerLink( "Queue".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "queue_icon.png")), () => new FileSystemContainer(queueDirectory) { UseIncrementedNameDuringTypeChange = true, DefaultSort = new LibrarySortBehavior() { SortKey = SortKey.ModifiedDate, } })); } this.Library.BundledPartsCollectionContainer = new BundledPartsCollectionContainer(); // this.Library.LibraryCollectionContainer.HeaderMarkdown = "Here you can find the collection of libraries you can use".Localize(); this.Library.RegisterContainer( new DynamicContainerLink( "Bundled".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "design_apps_icon.png")), () => this.Library.BundledPartsCollectionContainer) { IsReadOnly = true }); if (File.Exists(ApplicationDataStorage.Instance.CustomLibraryFoldersPath)) { // Add each path defined in the CustomLibraryFolders file as a new FileSystemContainerItem foreach (string directory in File.ReadLines(ApplicationDataStorage.Instance.CustomLibraryFoldersPath)) { // if (Directory.Exists(directory)) { this.Library.RegisterContainer( new FileSystemContainer.DirectoryContainerLink(directory) { UseIncrementedNameDuringTypeChange = true }); } } } this.Library.PlatingHistory = new PlatingHistoryContainer(); this.Library.RegisterContainer( new DynamicContainerLink( "History".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "history_icon.png")), () => new RootHistoryContainer()) { IsReadOnly = true }); // Create a new library context for the SaveAs view this.LibraryTabContext = new LibraryConfig() { ActiveContainer = new WrappedLibraryContainer(this.Library.RootLibaryContainer) { ExtraContainers = new List() { new DynamicContainerLink( "Printers".Localize(), StaticData.Instance.LoadIcon(Path.Combine("Library", "folder.png")), StaticData.Instance.LoadIcon(Path.Combine("Library", "printer_icon.png")), () => new OpenPrintersContainer()) } } }; } public void ExportLibraryItems(IEnumerable libraryItems, bool centerOnBed = true, PrinterConfig printer = null) { UiThread.RunOnIdle(() => { if (printer != null || this.ActivePrinters.Count() == 1) { // If unspecified but count is one, select the one active printer if (printer == null) { printer = this.ActivePrinters.First(); } printer.ForceSceneSettingsUpdate(); DialogWindow.Show( new ExportPrintItemPage(libraryItems, centerOnBed, printer)); } else { // If there are no printers setup show the export dialog but have the gcode option disabled if (!ProfileManager.Instance.ActiveProfiles.Any() || ProfileManager.Instance.ActiveProfiles.Count() > 1) { DialogWindow.Show(new ExportPrintItemPage(libraryItems, centerOnBed, null)); } else // If there is only one printer constructed, use it. { var historyContainer = this.Library.PlatingHistory; var printerInfo = ProfileManager.Instance.ActiveProfiles.First(); ProfileManager.LoadSettingsAsync(printerInfo.ID).ContinueWith(task => { var settings = task.Result; var onlyPrinter = new PrinterConfig(settings); onlyPrinter.Bed.LoadEmptyContent( new EditContext() { ContentStore = historyContainer, SourceItem = historyContainer.NewBedPlate(onlyPrinter.Bed) }); UiThread.RunOnIdle(() => { DialogWindow.Show(new ExportPrintItemPage(libraryItems, centerOnBed, onlyPrinter)); }); }); } } }); } private ApplicationController() { Workspaces = new ObservableCollection(); // get markdown working correctly MarkdownWidget.LaunchBrowser = ApplicationController.LaunchBrowser; MarkdownWidget.RetrieveText = WebCache.RetrieveText; MarkdownWidget.RetrieveImageSquenceAsync = WebCache.RetrieveImageSquenceAsync; Workspaces.CollectionChanged += (s, e) => { if (!restoringWorkspaces) { PersistOpenTabsLayout(); } }; this.Thumbnails = new ThumbnailsConfig(); ProfileManager.UserChanged += (s, e) => { // _activePrinters = new List(); }; this.Extensions = new ExtensionsConfig(this.Library); this.Extensions.Register(new SheetEditor()); this.Extensions.Register(new PublicPropertyEditor()); HelpArticle helpArticle = null; string helpPath = Path.Combine("OEMSettings", "toc.json"); if (StaticData.Instance.FileExists(helpPath)) { try { helpArticle = JsonConvert.DeserializeObject(StaticData.Instance.ReadAllText(helpPath)); } catch { } } this.HelpArticles = helpArticle ?? new HelpArticle(); Object3D.AssetsPath = Path.Combine(ApplicationDataStorage.Instance.ApplicationLibraryDataPath, "Assets"); using (var meshSteam = StaticData.Instance.OpenStream(Path.Combine("Stls", "missing.stl"))) { Object3D.FileMissingMesh = StlProcessing.Load(meshSteam, CancellationToken.None); } ScrollBar.DefaultMargin = new BorderDouble(right: 1); ScrollBar.ScrollBarWidth = 11 * GuiWidget.DeviceScale; ScrollBar.GrowThumbBy = 3 * GuiWidget.DeviceScale; // Initialize statics Object3D.AssetsPath = ApplicationDataStorage.Instance.LibraryAssetsPath; this.Library = new LibraryConfig(); this.Library.ContentProviders.Add(new[] { "stl", "obj", "3mf", "amf", "mcx" }, new MeshContentProvider()); this.Library.ContentProviders.Add("gcode", new GCodeContentProvider()); this.Library.ContentProviders.Add(new[] { "png", "gif", "jpg", "jpeg" }, new ImageContentProvider()); this.Library.ContentProviders.Add(new[] { "scad" }, new OpenScadContentProvider()); this.InitializeLibrary(); } /// /// Show a notification on screen. This is usually due to a system error of some kind /// like a bad save or load. /// /// The message to show /// The length of time to show the message public void ShowNotification(string message, double durationSeconds = 10) { foreach(var printer in ActivePrinters) { var terminal = printer?.Connection?.TerminalLog; if (terminal != null) { terminal.WriteLine(message); } } // show the message for the time requested this.Tasks.Execute(message, null, (progress, cancellationToken) => { var time = UiThread.CurrentTimerMs; while (UiThread.CurrentTimerMs < time + durationSeconds * 1000) { Thread.Sleep(30); progress?.Invoke((UiThread.CurrentTimerMs - time) / 1000.0 / durationSeconds, null); } return Task.CompletedTask; }); } public void Connection_ErrorReported(object sender, string line) { if (line != null) { if (sender is PrinterConnection printerConnection) { string message = "Your printer is reporting a HARDWARE ERROR and has been paused. Check the error and cancel the print if required.".Localize() + "\n" + "\n" + "Error Reported".Localize() + ":" + $" \"{line}\"."; UiThread.RunOnIdle(() => { var prinerName = printerConnection.Printer.PrinterName; var messageBox = new StyledMessageBox.MessageBoxPage((clickedOk) => { if (clickedOk && printerConnection.Paused) { printerConnection.Resume(); } }, message, prinerName + " is reporting a Hardware Error".Localize(), StyledMessageBox.MessageType.YES_NO, null, 400, 300, "Resume".Localize(), "OK".Localize(), ApplicationController.Instance.Theme); var exportButton = Theme.CreateDialogButton("Export Print Log...".Localize()); exportButton.Click += (s, e) => { UiThread.RunOnIdle(() => TerminalLog.Export(printerConnection), .1); }; messageBox.AddPageAction(exportButton); DialogWindow.Show(messageBox); }); } } } public void Connection_TemporarilyHoldingTemp(object sender, EventArgs e) { if (sender is PrinterConnection printerConnection) { if (printerConnection.AnyHeatIsOn) { var paused = false; Tasks.Execute("", printerConnection.Printer, (reporter, cancellationToken) => { while (printerConnection.SecondsToHoldTemperature > 0 && !cancellationToken.IsCancellationRequested && printerConnection.ContinueHoldingTemperature) { var status = ""; if (paused) { status = "Holding Temperature".Localize(); } else { if (printerConnection.SecondsToHoldTemperature > 60) { status = string.Format( "{0} {1:0}m {2:0}s", "Automatic Heater Shutdown in".Localize(), (int)printerConnection.SecondsToHoldTemperature / 60, (int)printerConnection.SecondsToHoldTemperature % 60); } else { status = string.Format( "{0} {1:0}s", "Automatic Heater Shutdown in".Localize(), printerConnection.SecondsToHoldTemperature); } } var progress = printerConnection.SecondsToHoldTemperature / printerConnection.TimeToHoldTemperature; reporter?.Invoke(progress, status); Thread.Sleep(20); } return Task.CompletedTask; }, taskActions: new RunningTaskOptions() { PauseAction = () => UiThread.RunOnIdle(() => { paused = true; printerConnection.TimeHaveBeenHoldingTemperature.Stop(); }), PauseToolTip = "Pause automatic heater shutdown".Localize(), ResumeAction = () => UiThread.RunOnIdle(() => { paused = false; printerConnection.TimeHaveBeenHoldingTemperature.Start(); }), ResumeToolTip = "Resume automatic heater shutdown".Localize(), StopAction = (abortCancel) => UiThread.RunOnIdle(() => { printerConnection.TurnOffBedAndExtruders(TurnOff.Now); }), StopToolTip = "Immediately turn off heaters".Localize() }); } } } public void Shutdown() { // Ensure all threads shutdown gracefully on close // Release any waiting generator threads this.Thumbnails.Shutdown(); // Kill all long running tasks (this will release the slicing thread if running) foreach (var task in Tasks.RunningTasks) { task.CancelTask(); } } private static readonly Dictionary TypeFaceCache = new Dictionary() { [NamedTypeFace.Liberation_Sans] = LiberationSansFont.Instance, [NamedTypeFace.Liberation_Sans_Bold] = LiberationSansBoldFont.Instance, [NamedTypeFace.Liberation_Mono] = TypeFace.LoadFrom(StaticData.Instance.ReadAllText(Path.Combine("Fonts", "LiberationMono.svg"))) }; private static object locker = new object(); public static TypeFace GetTypeFace(NamedTypeFace namedTypeFace) { lock (locker) { if (!TypeFaceCache.ContainsKey(namedTypeFace)) { var typeFace = new TypeFace(); var path = Path.Combine("Fonts", $"{namedTypeFace}.ttf"); var exists = StaticData.Instance.FileExists(path); var stream = exists ? StaticData.Instance.OpenStream(path) : null; if (stream != null && typeFace.LoadTTF(stream)) { TypeFaceCache.Add(namedTypeFace, typeFace); } else { // try the svg path = Path.Combine("Fonts", $"{namedTypeFace}.svg"); exists = StaticData.Instance.FileExists(path); typeFace = exists ? TypeFace.LoadFrom(StaticData.Instance.ReadAllText(path)) : null; if (typeFace != null) { TypeFaceCache.Add(namedTypeFace, typeFace); } else { // assign it to the default TypeFaceCache.Add(namedTypeFace, TypeFaceCache[NamedTypeFace.Liberation_Sans]); } } stream?.Dispose(); } return TypeFaceCache[namedTypeFace]; } } private static TypeFace titilliumTypeFace = null; public static TypeFace TitilliumTypeFace { get { if (titilliumTypeFace == null) { titilliumTypeFace = TypeFace.LoadFrom(StaticData.Instance.ReadAllText(Path.Combine("Fonts", "TitilliumWeb-Black.svg"))); } return titilliumTypeFace; } } public static string LoadCachedFile(string cacheKey, string cacheScope) { string cachePath = CacheablePath(cacheScope, cacheKey); if (File.Exists(cachePath)) { // Load from cache and deserialize return File.ReadAllText(cachePath); } return null; } public static Task LoadCacheableAsync(string cacheKey, string cacheScope, string staticDataFallbackPath = null) where T : class { if (LoadCachedFile(cacheKey, cacheScope) is string cachedFile) { // Load from cache and deserialize return Task.FromResult( JsonConvert.DeserializeObject(cachedFile)); } try { if (staticDataFallbackPath != null && StaticData.Instance.FileExists(staticDataFallbackPath)) { return Task.FromResult( JsonConvert.DeserializeObject(StaticData.Instance.ReadAllText(staticDataFallbackPath))); } } catch { } return Task.FromResult(default(T)); } // Requests fresh content from online services, falling back to cached content if offline public static async Task LoadCacheableAsync(string cacheKey, string cacheScope, Func> collector, string staticDataFallbackPath = null) where T : class { string cachePath = CacheablePath(cacheScope, cacheKey); try { // Try to update the document T item = await collector(); if (item != null) { // update cache on success File.WriteAllText(cachePath, JsonConvert.SerializeObject(item, Formatting.Indented)); return item; } } catch { // Fall back to preexisting cache if failed } return await LoadCacheableAsync(cacheKey, cacheScope, staticDataFallbackPath); } public static string CacheablePath(string cacheScope, string cacheKey) { string scopeDirectory = Path.Combine(ApplicationDataStorage.Instance.CacheDirectory, cacheScope); // Ensure directory exists Directory.CreateDirectory(scopeDirectory); return Path.Combine(scopeDirectory, cacheKey); } // Indicates if given file can be opened on the design surface public bool IsLoadableFile(string filePath) { string extension = Path.GetExtension(filePath).ToLower(); string extensionWithoutPeriod = extension.Trim('.'); return !string.IsNullOrEmpty(extension) && (ApplicationSettings.OpenDesignFileParams.Contains(extension) || this.Library.ContentProviders.Keys.Contains(extensionWithoutPeriod)); } public bool IsReloading { get; private set; } = false; private GuiWidget reloadingOverlay; public async Task ReloadAll() { try { #if DEBUG StaticData.Instance.PurgeCache(); #endif this.IsReloading = true; var theme = ApplicationController.Instance.Theme; SingleWindowProvider.SetWindowTheme(theme.TextColor, theme.DefaultFontSize - 1, () => theme.CreateSmallResetButton(), theme.ToolbarPadding, theme.TabBarBackground, new Color(theme.PrimaryAccentColor, 175)); reloadingOverlay = new GuiWidget { HAnchor = HAnchor.Stretch, VAnchor = VAnchor.Stretch, BackgroundColor = this.Theme.DarkShade }; reloadingOverlay.AddChild(new TextWidget("Reloading".Localize() + "...", textColor: Color.White, pointSize: this.Theme.DefaultFontSize * 1.5) { HAnchor = HAnchor.Center, VAnchor = VAnchor.Center }); AppContext.RootSystemWindow.AddChild(reloadingOverlay); await Task.Delay(50); GuiWidget.LayoutCount = 0; using (new QuickTimer($"ReloadAll_{reloadCount++}:")) { MainView = new MainViewWidget(this.Theme); this.DoneReloadingAll?.CallEvents(null, null); using (new QuickTimer("Time to AddMainview: ")) { AppContext.RootSystemWindow.CloseChildren(); AppContext.RootSystemWindow.AddChild(MainView); } } } catch (Exception ex) { reloadingOverlay?.CloseOnIdle(); UiThread.RunOnIdle(() => { StyledMessageBox.ShowMessageBox("An unexpected error occurred during reload".Localize() + ": \n\n" + ex.Message, "Reload Failed".Localize()); }); } finally { this.IsReloading = false; } Debug.WriteLine($"LayoutCount: {GuiWidget.LayoutCount:0.0}"); } private static int reloadCount = 0; public void OnApplicationClosed() { this.Thumbnails.Shutdown(); ApplicationSettings.Instance.ReleaseClientToken(); } public static ApplicationController Instance { get { if (globalInstance == null) { globalInstance = new ApplicationController(); } return globalInstance; } } public bool SwitchToWorkspaceIfAlreadyOpen(string assetPath) { var mainViewWidget = Instance.MainView; foreach (var openWorkspace in Instance.Workspaces) { if (openWorkspace.SceneContext.EditContext.SourceFilePath == assetPath || (openWorkspace.SceneContext.EditContext.SourceItem is IAssetPath cloudItem && cloudItem.AssetPath == assetPath)) { foreach (var tab in mainViewWidget.TabControl.AllTabs) { if (tab.TabContent is DesignTabPage tabContent && (tabContent.sceneContext.EditContext.SourceFilePath == assetPath || (tabContent.sceneContext.EditContext.SourceItem is IAssetPath cloudItem2 && cloudItem2.AssetPath == assetPath))) { mainViewWidget.TabControl.ActiveTab = tab; return true; } } } } return false; } public DragDropData DragDropData { get; set; } = new DragDropData(); private string _uiHint = ""; /// /// Set or get the current ui hint for the thing the mouse is over /// public string UiHint { get => _uiHint; set { if (_uiHint != value) { _uiHint = value; UiHintChanged?.Invoke(this, null); } } } public event EventHandler UiHintChanged; public string ProductName { get { if (this.IsMatterControlPro()) { return OemSettings.Instance.RegisteredProductName; } return OemSettings.Instance.UnregisteredProductName; } } public void SwitchToPurchasedLibrary() { var purchasedContainer = Library.RootLibaryContainer.ChildContainers.Where(c => c.ID == "LibraryProviderPurchasedKey").FirstOrDefault(); if (purchasedContainer != null) { // TODO: Navigate to purchased container throw new NotImplementedException("SwitchToPurchasedLibrary"); } } public void OnLoadActions() { if (AssetObject3D.AssetManager == null) { AssetObject3D.AssetManager = new AssetManager(); } } private static void RunSetupIfRequired() { if (!ProfileManager.Instance.ActiveProfiles.Any()) { // Start the setup wizard if no profiles exist UiThread.RunOnIdle(() => DialogWindow.Show(PrinterSetup.GetBestStartPage())); } } public void SwitchToSharedLibrary() { // Switch to the shared library var libraryContainer = Library.RootLibaryContainer.ChildContainers.Where(c => c.ID == "LibraryProviderSharedKey").FirstOrDefault(); if (libraryContainer != null) { // TODO: Navigate to purchased container throw new NotImplementedException("SwitchToSharedLibrary"); } } public void ChangeCloudSyncStatus(bool userAuthenticated, string reason = "") { UserSettings.Instance.set(UserSettingsKey.CredentialsInvalid, userAuthenticated ? "false" : "true"); UserSettings.Instance.set(UserSettingsKey.CredentialsInvalidReason, userAuthenticated ? "" : reason); CloudSyncStatusChanged.CallEvents(this, new CloudSyncEventArgs() { IsAuthenticated = userAuthenticated }); if (!string.IsNullOrEmpty(AuthenticationData.Instance.ActiveSessionUsername) && AuthenticationData.Instance.ActiveSessionUsername != AuthenticationData.Instance.LastSessionUsername) { AuthenticationData.Instance.LastSessionUsername = AuthenticationData.Instance.ActiveSessionUsername; } // TODO: Unclear why we'd reload on status change - it seems like this state should be managed entirely from ProfileManager and removed from this location ProfileManager.ReloadActiveUser(); } public Stream LoadHttpAsset(string url) { string fingerPrint = ToSHA1(url); string cachePath = ApplicationController.CacheablePath("HttpAssets", fingerPrint); if (File.Exists(cachePath)) { return File.Open(cachePath, FileMode.Open); } else { var client = new WebClient(); var bytes = client.DownloadData(url); File.WriteAllBytes(cachePath, bytes); return new MemoryStream(bytes); } } public async Task LoadPrinter(string printerID) { var printer = this.ActivePrinters.FirstOrDefault(p => p.Settings.ID == printerID); if (printer == null) { if (!string.IsNullOrEmpty(printerID) && ProfileManager.Instance[printerID] != null) { printer = new PrinterConfig(await ProfileManager.LoadSettingsAsync(printerID)); } } if (printer != null && printer.Settings.PrinterSelected && printer.Settings.GetValue(SettingsKey.auto_connect)) { printer.Connection.Connect(); } return printer; } public async Task OpenEmptyPrinter(string printerID, bool addPhilToBed = false) { if (!string.IsNullOrEmpty(printerID) && ProfileManager.Instance[printerID] != null) { var printer = await this.LoadPrinter(printerID); // Add workspace for printer var workspace = new PartWorkspace(printer); var history = this.Library.PlatingHistory; await workspace.SceneContext.LoadContent(new EditContext() { ContentStore = history, SourceItem = history.NewBedPlate(workspace.Printer.Bed) }, null); this.OpenWorkspace(workspace); if (addPhilToBed) { workspace.SceneContext.AddPhilToBed(); } return printer; } return null; } public void OpenPrinter(PrinterInfo printerInfo) { if (this.ActivePrinters.FirstOrDefault(p => p.Settings.ID == printerInfo.ID) is PrinterConfig printer && this.MainView.TabControl.AllTabs.FirstOrDefault(t => t.TabContent is PrinterTabPage printerTabPage && printerTabPage.Printer == printer) is ITab tab) { // Switch to existing printer tab this.MainView.TabControl.ActiveTab = tab; } else { // Open new printer tab this.OpenEmptyPrinter(printerInfo.ID).ConfigureAwait(false); } } public void OpenWorkspace(PartWorkspace workspace) { this.OpenWorkspace(workspace, WorkspacesChangedEventArgs.OperationType.Add); } public void OpenWorkspace(PartWorkspace workspace, WorkspacesChangedEventArgs.OperationType operationType) { this.Workspaces.Add(workspace); this.OnWorkspacesChanged(workspace, operationType); } public void RestoreWorkspace(PartWorkspace workspace) { this.OpenWorkspace(workspace, WorkspacesChangedEventArgs.OperationType.Restore); } private string loadedUserTabs = null; public async Task RestoreUserTabs() { // Prevent reload of loaded user if (loadedUserTabs == ProfileManager.Instance.UserName) { return; } restoringWorkspaces = true; loadedUserTabs = ProfileManager.Instance.UserName; var history = this.Library.PlatingHistory; Workspaces.Clear(); if (File.Exists(ProfileManager.Instance.OpenTabsPath)) { try { string openTabsText = File.ReadAllText(ProfileManager.Instance.OpenTabsPath); var persistedWorkspaces = JsonConvert.DeserializeObject>( openTabsText, new ContentStoreConverter(), new LibraryItemConverter()); var loadedPrinters = new HashSet(); await Tasks.Execute( "Restoring".Localize() + "...", null, async (reporter, cancellationTokenSource) => { for (int i=0; i { var ratioPerWorkspace = 1.0 / persistedWorkspaces.Count; var completed = ratioPerWorkspace * i; var progress2 = completed + progress * ratioPerWorkspace; var status = message; reporter?.Invoke(progress2, status); }); this.RestoreWorkspace(workspace); } } catch { // Suppress workspace load exceptions and continue to the next workspace } } }); } catch { // Suppress deserialization issues with opentabs.json and continue with an empty Workspaces lists } } // If the use does not have a workspace open and has not setup any hardware, show the startup screen if (this.Workspaces.Count == 0 && !ProfileManager.Instance.ActiveProfiles.Any() && SystemWindow.AllOpenSystemWindows.Count() < 2) { UiThread.RunOnIdle(() => { DialogWindow.Show(); }); } restoringWorkspaces = false; } /// /// Compute hash for string encoded as UTF8 /// /// String to be hashed public static string ToSHA1(string s) { byte[] bytes = Encoding.UTF8.GetBytes(s); // var timer = Stopwatch.StartNew(); using (var sha1 = System.Security.Cryptography.SHA1.Create()) { byte[] hash = sha1.ComputeHash(bytes); string sHA1 = BitConverter.ToString(hash).Replace("-", string.Empty); // Console.WriteLine("{0} {1} {2}", SHA1, timer.ElapsedMilliseconds, filePath); return sHA1; } } /// /// Register the given PrintItemAction into the named section /// /// The section to register in /// The action to register public void RegisterLibraryAction(string section, LibraryAction printItemAction) { if (!registeredLibraryActions.TryGetValue(section, out List items)) { items = new List(); registeredLibraryActions.Add(section, items); } items.Add(printItemAction); } /// /// Enumerate the given section, returning all registered actions /// /// The section to enumerate /// The registered Actions public IEnumerable RegisteredLibraryActions(string section) { if (registeredLibraryActions.TryGetValue(section, out List items)) { return items; } return Enumerable.Empty(); } public static IObject3D ClipboardItem { get; internal set; } public Action ShareLibraryItem { get; set; } public ObservableCollection Workspaces { get; } public AppViewState ViewState { get; } = new AppViewState(); public Uri HelpArticleSource { get; set; } public Dictionary HelpArticlesByID { get; set; } public string MainTabKey { get => UserSettings.Instance.get(UserSettingsKey.MainTabKey); set => UserSettings.Instance.set(UserSettingsKey.MainTabKey, value); } public static List StartupActions { get; } = new List(); public static List StartupTasks { get; } = new List(); public static Type ServicesStatusType { get; set; } /// /// Gets a value indicating whether any ActivePrinter is running a print task, either in paused or printing states /// public bool AnyPrintTaskRunning => this.ActivePrinters.Any(p => p.Connection.Printing || p.Connection.Paused || p.Connection.CommunicationState == CommunicationStates.PreparingToPrint); public event EventHandler ApplicationTopBarCreated; public void NotifyPrintersTabRightElement(GuiWidget sourceExentionArea) { ApplicationTopBarCreated?.Invoke(this, new ApplicationTopBarCreatedEventArgs(sourceExentionArea)); // after adding content to the right side make sure we hold the space in the tab bar var leftChild = sourceExentionArea.Parent.Children.First(); var padding = leftChild.Padding; leftChild.Padding = new BorderDouble(padding.Left, padding.Bottom, sourceExentionArea.Width, padding.Height); } public async Task PrintPart(EditContext editContext, PrinterConfig printerConfig, Action reporter, CancellationToken cancellationToken, PrinterConnection.PrintingModes printingMode) { var partFilePath = editContext.SourceFilePath; var gcodeFilePath = await editContext.GCodeFilePath(printerConfig); var printItemName = editContext.SourceItem.Name; printerConfig.ForceSceneSettingsUpdate(); // Exit if called in a non-applicable state if (printerConfig.Connection.CommunicationState != CommunicationStates.Connected && printerConfig.Connection.CommunicationState != CommunicationStates.FinishedPrint) { return; } try { if (PrinterCalibrationWizard.SetupRequired(printerConfig, requiresLoadedFilament: true)) { UiThread.RunOnIdle(() => { DialogWindow.Show( new PrinterCalibrationWizard(printerConfig, AppContext.Theme), advanceToIncompleteStage: true); }); return; } printerConfig.Connection.PrintingItemName = printItemName; var errors = new List(); printerConfig.ValidateSettings(errors, validatePrintBed: !printerConfig.Bed.EditContext.IsGGCodeSource); if (errors.Any(e => e.ErrorLevel == ValidationErrorLevel.Error)) { this.ShowValidationErrors("Validation Error".Localize(), errors); } else // there are no errors continue printing { // clear the output cache prior to starting a print printerConfig.Connection.TerminalLog.Clear(); string hideGCodeWarning = ApplicationSettings.Instance.get(ApplicationSettingsKey.HideGCodeWarning); if (Path.GetExtension(partFilePath).ToUpper() == ".GCODE") { if (hideGCodeWarning != "true") { var hideGCodeWarningCheckBox = new CheckBox("Don't remind me again".Localize()) { TextColor = this.Theme.TextColor, Margin = new BorderDouble(top: 6, left: 6), HAnchor = Agg.UI.HAnchor.Left }; hideGCodeWarningCheckBox.Click += (sender, e) => { if (hideGCodeWarningCheckBox.Checked) { ApplicationSettings.Instance.set(ApplicationSettingsKey.HideGCodeWarning, "true"); } else { ApplicationSettings.Instance.set(ApplicationSettingsKey.HideGCodeWarning, null); } }; UiThread.RunOnIdle(() => { StyledMessageBox.ShowMessageBox( (messageBoxResponse) => { if (messageBoxResponse) { printerConfig.Connection.CommunicationState = CommunicationStates.PreparingToPrint; this.ArchiveAndStartPrint(partFilePath, gcodeFilePath, printerConfig, printingMode); } }, "The file you are attempting to print is a GCode file.\n\nIt is recommended that you only print Gcode files known to match your printer's configuration.\n\nAre you sure you want to print this GCode file?".Localize(), "Warning - GCode file".Localize(), new GuiWidget[] { hideGCodeWarningCheckBox }, StyledMessageBox.MessageType.YES_NO); }); } else { printerConfig.Connection.CommunicationState = CommunicationStates.PreparingToPrint; this.ArchiveAndStartPrint(partFilePath, gcodeFilePath, printerConfig, printingMode); } } else { printerConfig.Connection.CommunicationState = CommunicationStates.PreparingToPrint; (bool slicingSucceeded, string finalPath) = await this.SliceItemLoadOutput( printerConfig, printerConfig.Bed.Scene, gcodeFilePath); // Only start print if slicing completed if (slicingSucceeded) { this.ArchiveAndStartPrint(partFilePath, finalPath, printerConfig, printingMode); } else { // TODO: Need to reset printing state? This seems like I shouldn't own this indicator printerConfig.Connection.CommunicationState = CommunicationStates.Connected; } } } } catch (Exception) { } } public void ShowValidationErrors(string windowTitle, List errors) { UiThread.RunOnIdle(() => { var dialogPage = new DialogPage("Close".Localize()) { HAnchor = HAnchor.Stretch, WindowTitle = windowTitle, HeaderText = "Action Required".Localize() }; dialogPage.ContentRow.AddChild(new ValidationErrorsPanel(errors, AppContext.Theme) { HAnchor = HAnchor.Stretch }); DialogWindow.Show(dialogPage); }); } public void ResetTranslationMap() { LoadTranslationMap(); } public static void LoadTranslationMap() { // Select either the user supplied language name or the current thread language name string twoLetterIsoLanguageName = string.IsNullOrEmpty(UserSettings.Instance.Language) ? Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName.ToLower() : UserSettings.Instance.Language.ToLower(); if (twoLetterIsoLanguageName == "ja" || twoLetterIsoLanguageName == "zh") { AggContext.DefaultFont = ApplicationController.GetTypeFace(NamedTypeFace.Firefly_Sung); AggContext.DefaultFontBold = ApplicationController.GetTypeFace(NamedTypeFace.Firefly_Sung); AggContext.DefaultFontItalic = ApplicationController.GetTypeFace(NamedTypeFace.Firefly_Sung); AggContext.DefaultFontBoldItalic = ApplicationController.GetTypeFace(NamedTypeFace.Firefly_Sung); } else { AggContext.DefaultFont = LiberationSansFont.Instance; AggContext.DefaultFontBold = LiberationSansBoldFont.Instance; AggContext.DefaultFontItalic = LiberationSansFont.Instance; AggContext.DefaultFontBoldItalic = LiberationSansBoldFont.Instance; } string machineTranslation = Path.Combine("Translations", twoLetterIsoLanguageName, "Translation.txt"); string humanTranslation = Path.Combine("Translations", twoLetterIsoLanguageName, "override.txt"); if (twoLetterIsoLanguageName == "en") { machineTranslation = Path.Combine("Translations", "Master.txt"); humanTranslation = null; } if (StaticData.Instance.FileExists(machineTranslation)) { StreamReader humanTranlationReader = null; if (humanTranslation != null && StaticData.Instance.FileExists(humanTranslation)) { var humanTranslationStream = StaticData.Instance.OpenStream(humanTranslation); humanTranlationReader = new StreamReader(humanTranslationStream); } var machineTranslationStream = StaticData.Instance.OpenStream(machineTranslation); var machineTranlationReader = new StreamReader(machineTranslationStream); TranslationMap.ActiveTranslationMap = new TranslationMap(machineTranlationReader, humanTranlationReader, twoLetterIsoLanguageName); machineTranlationReader.Close(); humanTranlationReader?.Close(); } else { TranslationMap.ActiveTranslationMap = new TranslationMap(twoLetterIsoLanguageName); } } public void MonitorPrintTask(PrinterConfig printer) { this.Tasks.Execute( "Printing".Localize(), printer, (reporterB, cancellationTokenB) => { return Task.Run(() => { string printing = "Printing".Localize(); int totalLayers = printer.Connection.TotalLayersInPrint; while (!printer.Connection.Printing && !cancellationTokenB.IsCancellationRequested) { // Wait for printing Thread.Sleep(200); } while ((printer.Connection.Printing || printer.Connection.Paused) && !cancellationTokenB.IsCancellationRequested) { var layerCount = printer.Bed.LoadedGCode == null ? "?" : printer.Bed.LoadedGCode.LayerCount.ToString(); var status = $"{printing} ({printer.Connection.CurrentlyPrintingLayer + 1} of {layerCount}) - {printer.Connection.PercentComplete:0}%"; var progress0To1 = printer.Connection.PercentComplete / 100; reporterB?.Invoke(progress0To1, status); Thread.Sleep(200); } }); }, taskActions: new RunningTaskOptions() { ExpansionSerializationKey = $"{nameof(MonitorPrintTask)}_expanded", RichProgressWidget = () => PrinterTabPage.PrintProgressWidget(printer, this.Theme), PauseAction = () => UiThread.RunOnIdle(() => { printer?.Connection.TerminalLog.WriteLine("User Requested Pause"); printer.Connection.RequestPause(); }), IsPaused = () => { return printer.Connection.Paused || printer.Connection.WaitingToPause; }, PauseToolTip = "Pause Print".Localize(), PauseText = "Pause".Localize(), ResumeAction = () => UiThread.RunOnIdle(() => { printer.Connection.Resume(); }), ResumeToolTip = "Resume Print".Localize(), ResumeText = "Resume".Localize(), StopAction = (abortCancel) => UiThread.RunOnIdle(() => { printer.CancelPrint(abortCancel); }), StopToolTip = "Cancel Print".Localize(), StopText = "Stop".Localize(), }); } private static PluginManager pluginManager = null; private bool restoringWorkspaces; public static PluginManager Plugins { get { // PluginManager initialization must occur late, after the config is loaded and after localization libraries // have occurred, which currently is driven by MatterControlApplication init if (pluginManager == null) { pluginManager = new PluginManager(); } return pluginManager; } } public bool Allow32BitReSlice { get; set; } public Action KeepAwake { get; set; } /// /// Archives MCX and validates GCode results before starting a print operation /// /// The source file which originally caused the slice->print operation /// The resulting GCode to print private async void ArchiveAndStartPrint(string sourcePath, string gcodeFilePath, PrinterConfig printer, PrinterConnection.PrintingModes printingMode) { if (File.Exists(sourcePath) && File.Exists(gcodeFilePath)) { bool originalIsGCode = Path.GetExtension(sourcePath).ToUpper() == ".GCODE"; if (File.Exists(gcodeFilePath)) { // Create archive point for printing attempt if (Path.GetExtension(sourcePath).ToUpper() == ".MCX") { string now = "Workspace " + DateTime.Now.ToString("yyyy-MM-dd HH_mm_ss"); string archivePath = Path.Combine(ApplicationDataStorage.Instance.PrintHistoryPath, now + ".zip"); string settingsFilePath = ProfileManager.Instance.ProfilePath(printer.Settings.ID); // if the printer was deleted while printing the path can be null if (settingsFilePath != null) { using (var file = File.OpenWrite(archivePath)) { using (var zip = new ZipArchive(file, ZipArchiveMode.Create)) { zip.CreateEntryFromFile(sourcePath, "PrinterPlate.mcx"); zip.CreateEntryFromFile(settingsFilePath, printer.PrinterName + ".printer"); zip.CreateEntryFromFile(gcodeFilePath, "sliced.gcode"); } } } } if (originalIsGCode) { await printer.Connection.StartPrint(gcodeFilePath, printingMode: printingMode); MonitorPrintTask(printer); return; } else { // Ask for slicer specific gcode validation if (printer.Settings.Slicer.ValidateFile(gcodeFilePath)) { await printer.Connection.StartPrint(gcodeFilePath, printingMode: printingMode); MonitorPrintTask(printer); return; } } } printer.Connection.CommunicationState = CommunicationStates.Connected; } } /// /// Slice the given IObject3D to the target GCode file using the referenced printer settings /// /// The printer/settings to use /// The IObject3D to slice /// The path to write the file to /// A boolean indicating if the slicing operation completed without aborting public async Task<(bool, string)> SliceItemLoadOutput(PrinterConfig printer, IObject3D object3D, string gcodeFilePath) { // Slice bool slicingSucceeded = false; printer.ViewState.SlicingItem = true; await this.Tasks.Execute("Slicing".Localize(), printer, async (reporter, cancellationTokenSource) => { slicingSucceeded = await Slicer.SliceItem( object3D, gcodeFilePath, printer, reporter, cancellationTokenSource.Token); }); printer.ViewState.SlicingItem = false; // Skip loading GCode output if slicing failed if (!slicingSucceeded) { return (false, gcodeFilePath); } var postProcessors = printer.Bed.Scene.Children.OfType(); if (postProcessors.Any()) { using (var resultStream = File.OpenRead(gcodeFilePath)) { Stream contextStream = resultStream; // Execute each post processor foreach (var processor in postProcessors) { // Invoke the processor and store the resulting output to the context stream reference contextStream = processor.ProcessOutput(contextStream); // Reset to the beginning contextStream.Position = 0; } // Modify final file name gcodeFilePath = Path.ChangeExtension(gcodeFilePath, GCodeFile.PostProcessedExtension); // Copy the final stream to the revised gcodeFilePath using (var finalStream = File.OpenWrite(gcodeFilePath)) { contextStream.CopyTo(finalStream); } } } await this.Tasks.Execute("Loading GCode".Localize(), printer, (innerProgress, concelationTokenSource) => { printer.Bed.LoadActiveSceneGCode(gcodeFilePath, concelationTokenSource.Token, (progress0to1, statusText) => { UiThread.RunOnIdle(() => { var progress0To1 = progress0to1; var status = statusText; innerProgress?.Invoke(progress0To1, status); }); }); if (printer.Bed.LoadedGCode is GCodeMemoryFile gcodeMemoryFile) { // try to validate the gcode file and warn if it seems invalid. // for now the definition of invalid is that it has a print time of < 30 seconds var estimatedPrintSeconds = gcodeMemoryFile.EstimatedPrintSeconds(); if (estimatedPrintSeconds < 30) { var message = "The time to print this G-Code is estimated to be {0} seconds.\n\nPlease check your part for errors if this is unexpected." .Localize() .FormatWith((int)estimatedPrintSeconds); UiThread.RunOnIdle(() => { StyledMessageBox.ShowMessageBox(message, "Warning, very short print".Localize()); }); } } // Switch to the 3D layer view if on Model view and slicing succeeded if (printer.ViewState.ViewMode == PartViewMode.Model) { printer.ViewState.ViewMode = PartViewMode.Layers3D; } return Task.CompletedTask; }); return (slicingSucceeded, gcodeFilePath); } public void ShellOpenFile(string file) { UiThread.RunOnIdle(() => { ShellFileOpened?.Invoke(this, file); AppContext.RootSystemWindow.BringToFront(); }); } private void KeepAwakeIfNeeded() { KeepAwake?.Invoke(AnyPrintTaskRunning); } public void Connection_PrintStarted(object sender, EventArgs e) { KeepAwakeIfNeeded(); AnyPrintStarted?.Invoke(sender, e); } public void Connection_PrintFinished(object sender, (string printerName, string itemName) e) { if (sender is PrinterConnection printerConnection) { var activePrintTask = printerConnection.ActivePrintTask; switch (printerConnection.PrintingMode) { case PrinterConnection.PrintingModes.Normal: var printTasks = PrintHistoryData.Instance.GetHistoryItems(10); var printHistoryEditor = new PrintHistoryEditor(((PrinterConnection)sender).Printer, AppContext.Theme, activePrintTask, printTasks); printHistoryEditor.CollectInfoPrintFinished(); break; case PrinterConnection.PrintingModes.Calibration: break; case PrinterConnection.PrintingModes.Autopilot: break; } } KeepAwakeIfNeeded(); AnyPrintComplete?.Invoke(sender, null); } public void Connection_PrintCanceled(object sender, EventArgs e) { if (sender is PrinterConnection printerConnection && printerConnection.PrintingMode == PrinterConnection.PrintingModes.Normal) { var printTasks = PrintHistoryData.Instance.GetHistoryItems(10); var printHistoryEditor = new PrintHistoryEditor(((PrinterConnection)sender).Printer, AppContext.Theme, printerConnection.CanceledPrintTask, printTasks); printHistoryEditor.CollectInfoPrintCanceled(); } KeepAwakeIfNeeded(); AnyPrintCanceled?.Invoke(sender, e); } public void ConnectToPrinter(PrinterConfig printer) { if (!printer.Settings.PrinterSelected) { return; } bool listenForConnectFailed = true; long connectStartMs = UiThread.CurrentTimerMs; void Connection_Failed(object s, EventArgs e) { #if !__ANDROID__ // TODO: Someday this functionality should be revised to an awaitable Connect() call in the Connect button that // shows troubleshooting on failed attempts, rather than hooking the failed event and trying to determine if the // Connect button started the task if (listenForConnectFailed && UiThread.CurrentTimerMs - connectStartMs < 25000) { UiThread.RunOnIdle(() => { // User initiated connect attempt failed, show port selection dialog DialogWindow.Show(new SetupStepComPortOne(printer)); }); } #endif ClearEvents(); } void Connection_Succeeded(object s, EventArgs e) { ClearEvents(); } void ClearEvents() { listenForConnectFailed = false; printer.Connection.ConnectionFailed -= Connection_Failed; printer.Connection.ConnectionSucceeded -= Connection_Succeeded; } printer.Connection.ConnectionFailed += Connection_Failed; printer.Connection.ConnectionSucceeded += Connection_Succeeded; if (AppContext.Platform.HasPermissionToDevice(printer)) { printer.Connection.HaltConnectionThread(); printer.Connection.Connect(); } } /// /// Replace invalid filename characters with the given replacement value to ensure working paths for the current filesystem /// /// The filename name to consider /// The replacement character to use /// A sanitized file name that is safe to use on the current system public string SanitizeFileName(string name, string replacementCharacter = "_") { if (string.IsNullOrEmpty(name)) { return name; } string invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars())); string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars); return Regex.Replace(name, invalidRegStr, replacementCharacter); } public ChromeTab ActivateHelpTab(string guideKey) { var tabControl = this.MainView.TabControl; var theme = AppContext.Theme; var helpDocsTab = tabControl.AllTabs.FirstOrDefault(t => t.Key == "HelpDocs") as ChromeTab; if (helpDocsTab == null) { var helpTreePanel = new HelpTreePanel(theme, guideKey) { HAnchor = HAnchor.Stretch, VAnchor = VAnchor.Stretch }; var icon = StaticData.Instance.LoadIcon("help_page.png", 16, 16).SetToColor(theme.TextColor); helpDocsTab = new ChromeTab("HelpDocs", "Help".Localize(), tabControl, helpTreePanel, theme, icon) { MinimumSize = new Vector2(0, theme.TabButtonHeight), Name = "Help Tab", }; tabControl.AddTab(helpDocsTab); } else { } tabControl.ActiveTab = helpDocsTab; return helpDocsTab; } public class CloudSyncEventArgs : EventArgs { public bool IsAuthenticated { get; set; } } public class StartupTask { public string Title { get; set; } public int Priority { get; set; } public Func, CancellationTokenSource, Task> Action { get; set; } } public class StartupAction { public string Title { get; set; } public int Priority { get; set; } public Action Action { get; set; } } } }