// // Workbench.cs // // Author: // Lluis Sanchez Gual // // Copyright (C) 2005 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Linq; using System.IO; using System.Xml; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using MonoDevelop.Core.Execution; using MonoDevelop.Projects; using MonoDevelop.Projects.Text; using MonoDevelop.Core; using MonoDevelop.Ide.Codons; using MonoDevelop.Ide.Gui.Content; using MonoDevelop.Ide.Gui.Pads; using MonoDevelop.Ide.Gui.Dialogs; using MonoDevelop.Ide.Desktop; using Mono.Addins; using MonoDevelop.Ide.Projects; using MonoDevelop.Core.StringParsing; using MonoDevelop.Ide.Navigation; using MonoDevelop.Components.Docking; using MonoDevelop.Components.DockNotebook; using System.Text; using MonoDevelop.Ide.Editor; using MonoDevelop.Components; using System.Threading.Tasks; using System.Collections.Immutable; using MonoDevelop.Core.Instrumentation; namespace MonoDevelop.Ide.Gui { /// /// This is the basic interface to the workspace. /// public sealed class Workbench { readonly ProgressMonitorManager monitors = new ProgressMonitorManager (); ImmutableList documents = ImmutableList.Empty; DefaultWorkbench workbench; PadCollection pads; public event EventHandler ActiveDocumentChanged; public event EventHandler LayoutChanged; public event EventHandler GuiLocked; public event EventHandler GuiUnlocked; internal void Initialize (ProgressMonitor monitor) { monitor.BeginTask (GettextCatalog.GetString ("Initializing Main Window"), 4); try { Counters.Initialization.Trace ("Creating DefaultWorkbench"); workbench = new DefaultWorkbench (); monitor.Step (1); Counters.Initialization.Trace ("Initializing Workspace"); workbench.InitializeWorkspace(); monitor.Step (1); Counters.Initialization.Trace ("Initializing Layout"); workbench.InitializeLayout (); monitor.Step (1); ((Gtk.Window)workbench).Visible = false; workbench.ActiveWorkbenchWindowChanged += OnDocumentChanged; workbench.WorkbenchTabsChanged += WorkbenchTabsChanged; IdeApp.Workspace.StoringUserPreferences += OnStoringWorkspaceUserPreferences; IdeApp.Workspace.LoadingUserPreferences += OnLoadingWorkspaceUserPreferences; IdeApp.FocusOut += delegate(object o, EventArgs args) { SaveFileStatus (); }; IdeApp.FocusIn += delegate(object o, EventArgs args) { CheckFileStatus (); }; IdeApp.ProjectOperations.StartBuild += delegate { SaveFileStatus (); }; IdeApp.ProjectOperations.EndBuild += delegate { // The file status checks outputs as well. CheckFileStatus (); }; pads = null; // Make sure we get an up to date pad list. monitor.Step (1); } finally { monitor.EndTask (); } } internal void Show (string workbenchMemento) { Counters.Initialization.Trace ("Realizing Root Window"); RootWindow.Realize (); Counters.Initialization.Trace ("Loading memento"); var memento = PropertyService.Get (workbenchMemento, new Properties ()); Counters.Initialization.Trace ("Setting memento"); workbench.Memento = memento; Counters.Initialization.Trace ("Making Visible"); RootWindow.Visible = true; workbench.CurrentLayout = "Solution"; // now we have an layout set notify it Counters.Initialization.Trace ("Setting layout"); if (LayoutChanged != null) LayoutChanged (this, EventArgs.Empty); Counters.Initialization.Trace ("Initializing monitors"); monitors.Initialize (); Present (); } internal async Task Close () { return await workbench.Close(); } public ImmutableList Documents { get { return documents; } } /// /// This is a wrapper for use with AutoTest /// internal bool DocumentsDirty { get { return Documents.Any (d => d.IsDirty); } } public Document ActiveDocument { get { if (workbench.ActiveWorkbenchWindow == null) return null; return WrapDocument (workbench.ActiveWorkbenchWindow); } } public Document GetDocument (string name) { var fullPath = (FilePath) FileService.GetFullPath (name); foreach (Document doc in documents) { var fullDocPath = (FilePath) FileService.GetFullPath (doc.Name); if (fullDocPath == fullPath) return doc; } return null; } internal TextReader[] GetDocumentReaders (List filenames) { TextReader [] results = new TextReader [filenames.Count]; int idx = 0; foreach (var f in filenames) { var fullPath = (FilePath)FileService.GetFullPath (f); Document doc = documents.Find (d => d.Editor != null && (fullPath == FileService.GetFullPath (d.Name))); if (doc != null) { results [idx] = doc.Editor.CreateReader (); } else { results [idx] = null; } idx++; } return results; } public PadCollection Pads { get { if (pads == null) { pads = new PadCollection (); foreach (PadCodon pc in workbench.PadContentCollection) WrapPad (pc); } return pads; } } public Gtk.Window RootWindow { get { return workbench; } } /// /// When set to true, opened documents will automatically be reloaded when a change in the underlying /// file is detected (unless the document has unsaved changes) /// public bool AutoReloadDocuments { get; set; } /// /// Whether the root window or any undocked part of it has toplevel focus. /// public bool HasToplevelFocus { get { if (DesktopService.IsModalDialogRunning ()) return false; var windows = Gtk.Window.ListToplevels (); var toplevel = windows.FirstOrDefault (x => x.HasToplevelFocus); if (toplevel == null) return false; if (toplevel == RootWindow) return true; #if WIN32 var app = System.Windows.Application.Current; if (app != null) { var wpfWindow = app.Windows.OfType().SingleOrDefault (x => x.IsActive); if (wpfWindow != null) return true; } #endif var dock = toplevel as DockFloatingWindow; return dock != null && dock.DockParent == RootWindow; } } public void Present () { //HACK: window resets its size on Win32 on Present if it was maximized by snapping to top edge of screen //partially work around this by avoiding the present call if it's already toplevel if (Platform.IsWindows && RootWindow.HasToplevelFocus) return; //FIXME: this should do a "request for attention" dock bounce on MacOS but only in some cases. //Doing it for all Present calls is excessive and annoying. Maybe we have too many Present calls... //Mono.TextEditor.GtkWorkarounds.PresentWindowWithNotification (RootWindow); RootWindow.Present (); } public void GrabDesktopFocus () { DesktopService.GrabDesktopFocus (RootWindow); } public bool FullScreen { get { return workbench.FullScreen; } set { workbench.FullScreen = value; } } public string CurrentLayout { get { return workbench.CurrentLayout; } set { if (value != workbench.CurrentLayout) { workbench.CurrentLayout = value; if (LayoutChanged != null) LayoutChanged (this, EventArgs.Empty); } } } public IList Layouts { get { return workbench.Layouts; } } public ProgressMonitorManager ProgressMonitors { get { return monitors; } } public StatusBar StatusBar { get { return workbench.StatusBar.MainContext; } } public void ShowCommandBar (string barId) { workbench.Toolbar.ShowCommandBar (barId); } public void HideCommandBar (string barId) { workbench.Toolbar.HideCommandBar (barId); } internal MonoDevelop.Components.MainToolbar.MainToolbarController Toolbar { get { return workbench.Toolbar; } } public Pad GetPad () { foreach (Pad pad in Pads) { if (typeof(T).FullName == pad.InternalContent.ClassName) return pad; } return null; } public void DeleteLayout (string name) { workbench.DeleteLayout (name); if (LayoutChanged != null) LayoutChanged (this, EventArgs.Empty); } public void LockGui () { if (IdeApp.CommandService.LockAll ()) { if (GuiLocked != null) GuiLocked (this, EventArgs.Empty); } } public void UnlockGui () { if (IdeApp.CommandService.UnlockAll ()) { if (GuiUnlocked != null) GuiUnlocked (this, EventArgs.Empty); } } public void SaveAll () { ITimeTracker tt = Counters.SaveAllTimer.BeginTiming (); try { // Make a copy of the list, since it may change during save Document[] docs = new Document [Documents.Count]; Documents.CopyTo (docs, 0); foreach (Document doc in docs) doc.Save (); } finally { tt.End (); } } internal async Task SaveAllDirtyFiles () { Document[] docs = Documents.Where (doc => doc.IsDirty && doc.Window.ViewContent != null).ToArray (); if (!docs.Any ()) return true; foreach (Document doc in docs) { AlertButton result = PromptToSaveChanges (doc); if (result == AlertButton.Cancel) return false; if (result == AlertButton.CloseWithoutSave) { doc.Window.ViewContent.DiscardChanges (); await doc.Window.CloseWindow (true); continue; } await doc.Save (); if (doc.IsDirty) { doc.Select (); return false; } } return true; } static AlertButton PromptToSaveChanges (Document doc) { return MessageService.GenericAlert (MonoDevelop.Ide.Gui.Stock.Warning, GettextCatalog.GetString ("Save the changes to document '{0}' before creating a new solution?", (object)(doc.Window.ViewContent.IsUntitled ? doc.Window.ViewContent.UntitledName : System.IO.Path.GetFileName (doc.Window.ViewContent.ContentName))), GettextCatalog.GetString ("If you don't save, all changes will be permanently lost."), AlertButton.CloseWithoutSave, AlertButton.Cancel, doc.Window.ViewContent.IsUntitled ? AlertButton.SaveAs : AlertButton.Save); } public void CloseAllDocuments (bool leaveActiveDocumentOpen) { Document[] docs = new Document [Documents.Count]; Documents.CopyTo (docs, 0); // The active document is the last one to close. // It avoids firing too many ActiveDocumentChanged events. foreach (Document doc in docs) { if (doc != ActiveDocument) doc.Close (); } if (!leaveActiveDocumentOpen && ActiveDocument != null) ActiveDocument.Close (); } internal Pad ShowPad (PadCodon content) { workbench.ShowPad (content); return WrapPad (content); } internal Pad AddPad (PadCodon content) { workbench.AddPad (content); return WrapPad (content); } public Pad AddPad (PadContent padContent, string id, string label, string defaultPlacement, IconId icon) { return AddPad (new PadCodon (padContent, id, label, defaultPlacement, icon)); } public Pad AddPad (PadContent padContent, string id, string label, string defaultPlacement, DockItemStatus defaultStatus, IconId icon) { return AddPad (new PadCodon (padContent, id, label, defaultPlacement, defaultStatus, icon)); } public Pad ShowPad (PadContent padContent, string id, string label, string defaultPlacement, IconId icon) { return ShowPad (new PadCodon (padContent, id, label, defaultPlacement, icon)); } public Pad ShowPad (PadContent padContent, string id, string label, string defaultPlacement, DockItemStatus defaultStatus, IconId icon) { return ShowPad (new PadCodon (padContent, id, label, defaultPlacement, defaultStatus, icon)); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, bool bringToFront)")] public Task OpenDocument (FilePath fileName, bool bringToFront) { return OpenDocument (fileName, bringToFront ? OpenDocumentOptions.Default : OpenDocumentOptions.Default & ~OpenDocumentOptions.BringToFront); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, OpenDocumentOptions options = OpenDocumentOptions.Default)")] public Task OpenDocument (FilePath fileName, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, -1, -1, options, null, null); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default)")] public Task OpenDocument (FilePath fileName, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, -1, -1, options, encoding, null); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, int line, int column, OpenDocumentOptions options = OpenDocumentOptions.Default)")] public Task OpenDocument (FilePath fileName, int line, int column, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, line, column, options, null, null); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, int line, int column, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default)")] public Task OpenDocument (FilePath fileName, int line, int column, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, line, column, options, encoding, null); } [Obsolete("Use OpenDocument (FilePath fileName, Project project, int line, int column, OpenDocumentOptions options, Encoding encoding, IViewDisplayBinding binding)")] internal Task OpenDocument (FilePath fileName, int line, int column, OpenDocumentOptions options, Encoding encoding, IViewDisplayBinding binding) { var openFileInfo = new FileOpenInformation (fileName, null) { Options = options, Line = line, Column = column, DisplayBinding = binding, Encoding = encoding }; return OpenDocument (openFileInfo); } public Task OpenDocument (FilePath fileName, Project project, bool bringToFront) { return OpenDocument (fileName, project, bringToFront ? OpenDocumentOptions.Default : OpenDocumentOptions.Default & ~OpenDocumentOptions.BringToFront); } public Task OpenDocument (FilePath fileName, Project project, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, project, -1, -1, options, null, null); } public Task OpenDocument (FilePath fileName, Project project, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, project, -1, -1, options, encoding, null); } public Task OpenDocument (FilePath fileName, Project project, int line, int column, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, project, line, column, options, null, null); } public Task OpenDocument (FilePath fileName, Project project, int line, int column, Encoding encoding, OpenDocumentOptions options = OpenDocumentOptions.Default) { return OpenDocument (fileName, project, line, column, options, encoding, null); } internal Task OpenDocument (FilePath fileName, Project project, int line, int column, OpenDocumentOptions options, Encoding encoding, IViewDisplayBinding binding) { var openFileInfo = new FileOpenInformation (fileName, project) { Options = options, Line = line, Column = column, DisplayBinding = binding, Encoding = encoding }; return OpenDocument (openFileInfo); } static void ScrollToRequestedCaretLocation (Document doc, FileOpenInformation info) { var ipos = doc.Editor; if ((info.Line >= 1 || info.Offset >= 0) && ipos != null) { doc.DisableAutoScroll (); doc.RunWhenLoaded (() => { var loc = new DocumentLocation (info.Line, info.Column >= 1 ? info.Column : 1); if (info.Offset >= 0) { loc = ipos.OffsetToLocation (info.Offset); } if (loc.IsEmpty) return; ipos.SetCaretLocation (loc, info.Options.HasFlag (OpenDocumentOptions.HighlightCaretLine), info.Options.HasFlag (OpenDocumentOptions.CenterCaretLine)); }); } } internal Task OpenDocument (FilePath fileName, Project project, int line, int column, OpenDocumentOptions options, Encoding Encoding, IViewDisplayBinding binding, DockNotebook dockNotebook) { var openFileInfo = new FileOpenInformation (fileName, project) { Options = options, Line = line, Column = column, DisplayBinding = binding, Encoding = Encoding, DockNotebook = dockNotebook }; return OpenDocument (openFileInfo); } public async Task OpenDocument (FileOpenInformation info) { if (string.IsNullOrEmpty (info.FileName)) return null; // Ensure that paths like /a/./a.cs are equalized using (Counters.OpenDocumentTimer.BeginTiming ("Opening file " + info.FileName)) { NavigationHistoryService.LogActiveDocument (); Counters.OpenDocumentTimer.Trace ("Look for open document"); foreach (Document doc in Documents) { BaseViewContent vcFound = null; //search all ViewContents to see if they can "re-use" this filename if (doc.Window.ViewContent.CanReuseView (info.FileName)) vcFound = doc.Window.ViewContent; //old method as fallback if ((vcFound == null) && (doc.FileName.CanonicalPath == info.FileName)) // info.FileName is already Canonical vcFound = doc.Window.ViewContent; //if found, try to reuse or close the old view if (vcFound != null) { // reuse the view if the binidng didn't change if (info.Options.HasFlag (OpenDocumentOptions.TryToReuseViewer) || vcFound.Binding == info.DisplayBinding) { if (info.Project != null && doc.Project != info.Project) { doc.SetProject (info.Project); } ScrollToRequestedCaretLocation (doc, info); if (info.Options.HasFlag (OpenDocumentOptions.BringToFront)) { doc.Select (); doc.Window.SelectWindow (); NavigationHistoryService.LogActiveDocument (); } return doc; } else { if (!await doc.Close ()) return doc; break; } } } Counters.OpenDocumentTimer.Trace ("Initializing monitor"); ProgressMonitor pm = ProgressMonitors.GetStatusProgressMonitor ( GettextCatalog.GetString ("Opening {0}", info.Project != null ? info.FileName.ToRelative (info.Project.ParentSolution.BaseDirectory) : info.FileName), Stock.StatusWorking, true ); await RealOpenFile (pm, info); pm.Dispose (); if (info.NewContent != null) { Counters.OpenDocumentTimer.Trace ("Wrapping document"); Document doc = WrapDocument (info.NewContent.WorkbenchWindow); ScrollToRequestedCaretLocation (doc, info); if (doc != null && info.Options.HasFlag (OpenDocumentOptions.BringToFront)) { doc.RunWhenLoaded (() => { if (doc.Window != null) doc.Window.SelectWindow (); }); } return doc; } return null; } } async Task BatchOpenDocument (ProgressMonitor monitor, FilePath fileName, Project project, int line, int column, DockNotebook dockNotebook) { if (string.IsNullOrEmpty (fileName)) return null; using (Counters.OpenDocumentTimer.BeginTiming ("Batch opening file " + fileName)) { var openFileInfo = new FileOpenInformation (fileName, project) { Options = OpenDocumentOptions.OnlyInternalViewer, Line = line, Column = column, DockNotebook = dockNotebook }; await RealOpenFile (monitor, openFileInfo); return openFileInfo.NewContent; } } public Document OpenDocument (ViewContent content, bool bringToFront) { workbench.ShowView (content, bringToFront); if (bringToFront) Present (); return WrapDocument (content.WorkbenchWindow); } public void ToggleMaximize () { workbench.ToggleFullViewMode (); } public Document NewDocument (string defaultName, string mimeType, string content) { MemoryStream ms = new MemoryStream (); byte[] data = System.Text.Encoding.UTF8.GetBytes (content); ms.Write (data, 0, data.Length); ms.Position = 0; return NewDocument (defaultName, mimeType, ms); } public Document NewDocument (string defaultName, string mimeType, Stream content) { IViewDisplayBinding binding = DisplayBindingService.GetDefaultViewBinding (null, mimeType, null); if (binding == null) throw new ApplicationException("Can't create display binding for mime type: " + mimeType); ViewContent newContent = binding.CreateContent (defaultName, mimeType, null); using (content) { newContent.LoadNew (content, mimeType); } if (newContent == null) throw new ApplicationException(String.Format("Created view content was null{3}DefaultName:{0}{3}MimeType:{1}{3}Content:{2}", defaultName, mimeType, content, Environment.NewLine)); newContent.UntitledName = defaultName; newContent.IsDirty = false; newContent.Binding = binding; workbench.ShowView (newContent, true, binding); var document = WrapDocument (newContent.WorkbenchWindow); document.Editor.Encoding = Encoding.UTF8; document.StartReparseThread (); return document; } public void ShowGlobalPreferencesDialog (Gtk.Window parentWindow) { ShowGlobalPreferencesDialog (parentWindow, null); } static Properties properties = ((Properties) PropertyService.Get ( "MonoDevelop.TextEditor.Document.Document.DefaultDocumentAggregatorProperties", new Properties())); public void ShowGlobalPreferencesDialog (Window parentWindow, string panelId, Action configurationAction = null) { if (parentWindow == null) parentWindow = IdeApp.Workbench.RootWindow; OptionsDialog ops = new OptionsDialog ( parentWindow, properties, "/MonoDevelop/Ide/GlobalOptionsDialog"); ops.Title = Platform.IsWindows ? GettextCatalog.GetString ("Options") : GettextCatalog.GetString ("Preferences"); try { if (panelId != null) ops.SelectPanel (panelId); if (configurationAction != null) configurationAction (ops); if (MessageService.RunCustomDialog (ops, parentWindow) == (int) Gtk.ResponseType.Ok) { PropertyService.SaveProperties (); MonoDevelop.Projects.Policies.PolicyService.SavePolicies (); } } finally { ops.Destroy (); ops.Dispose (); } } public void ShowDefaultPoliciesDialog (Window parentWindow) { ShowDefaultPoliciesDialog (parentWindow, null); } public void ShowDefaultPoliciesDialog (Window parentWindow, string panelId) { if (parentWindow == null) parentWindow = IdeApp.Workbench.RootWindow; var ops = new DefaultPolicyOptionsDialog (parentWindow); try { if (panelId != null) ops.SelectPanel (panelId); MessageService.RunCustomDialog (ops, parentWindow); } finally { ops.Destroy (); ops.Dispose (); } } public StringTagModelDescription GetStringTagModelDescription () { StringTagModelDescription model = new StringTagModelDescription (); model.Add (typeof (Project)); model.Add (typeof (Solution)); model.Add (typeof (DotNetProjectConfiguration)); model.Add (typeof (Workbench)); return model; } public StringTagModel GetStringTagModel () { StringTagModel source = new StringTagModel (); source.Add (this); if (IdeApp.ProjectOperations.CurrentSelectedSolutionItem != null) source.Add (IdeApp.ProjectOperations.CurrentSelectedSolutionItem.GetStringTagModel (IdeApp.Workspace.ActiveConfiguration)); else if (IdeApp.ProjectOperations.CurrentSelectedWorkspaceItem != null) source.Add (IdeApp.ProjectOperations.CurrentSelectedWorkspaceItem.GetStringTagModel ()); return source; } internal void ShowNext () { // Shows the next item in a pad that implements ILocationListPad. if (activeLocationList != null) { NavigationPoint next = activeLocationList.GetNextLocation (); if (next != null) next.ShowDocument (); } } internal void ShowPrevious () { // Shows the previous item in a pad that implements ILocationListPad. if (activeLocationList != null) { NavigationPoint next = activeLocationList.GetPreviousLocation (); if (next != null) next.ShowDocument (); } } ILocationList activeLocationList; public ILocationList ActiveLocationList { get { return activeLocationList; } set { activeLocationList = value; } } void OnDocumentChanged (object s, EventArgs a) { if (ActiveDocumentChanged != null) ActiveDocumentChanged (s, a); if (ActiveDocument != null) ActiveDocument.LastTimeActive = DateTime.Now; } internal Document WrapDocument (IWorkbenchWindow window) { if (window == null) return null; Document doc = FindDocument (window); if (doc != null) return doc; doc = new Document (window); window.Closing += OnWindowClosing; window.Closed += OnWindowClosed; documents = documents.Add (doc); doc.OnDocumentAttached (); OnDocumentOpened (new DocumentEventArgs (doc)); return doc; } Pad WrapPad (PadCodon padContent) { if (pads == null) { foreach (Pad p in Pads) { if (p.InternalContent == padContent) return p; } } Pad pad = new Pad (workbench, padContent); Pads.Add (pad); pad.Window.PadDestroyed += delegate { Pads.Remove (pad); }; return pad; } async Task OnWindowClosing (object sender, WorkbenchWindowEventArgs args) { var window = (IWorkbenchWindow) sender; var viewContent = window.ViewContent; if (!args.Forced && viewContent != null && viewContent.IsDirty) { AlertButton result = MessageService.GenericAlert (Stock.Warning, GettextCatalog.GetString ("Save the changes to document '{0}' before closing?", viewContent.IsUntitled ? viewContent.UntitledName : System.IO.Path.GetFileName (viewContent.ContentName)), GettextCatalog.GetString ("If you don't save, all changes will be permanently lost."), AlertButton.CloseWithoutSave, AlertButton.Cancel, viewContent.IsUntitled ? AlertButton.SaveAs : AlertButton.Save); if (result == AlertButton.Save) { args.Cancel = true; await FindDocument (window).Save (); viewContent.IsDirty = false; window.CloseWindow (true); return; } else if (result == AlertButton.SaveAs) { args.Cancel = true; var resultSaveAs = await FindDocument (window).SaveAs (); if (resultSaveAs) { viewContent.IsDirty = false; window.CloseWindow (true); } else { window.SelectWindow (); } return; } else { args.Cancel |= result != AlertButton.CloseWithoutSave; if (!args.Cancel) viewContent.DiscardChanges (); } } OnDocumentClosing (FindDocument (window)); } void OnWindowClosed (object sender, WorkbenchWindowEventArgs args) { IWorkbenchWindow window = (IWorkbenchWindow) sender; var doc = FindDocument (window); window.Closing -= OnWindowClosing; window.Closed -= OnWindowClosed; documents = documents.Remove (doc); OnDocumentClosed (doc); doc.DisposeDocument (); } // When looking for the project to which the file belongs, look first // in the active project, then the active solution, and so on internal static Project GetProjectContainingFile (FilePath fileName) { Project project = null; if (IdeApp.ProjectOperations.CurrentSelectedProject != null) { if (IdeApp.ProjectOperations.CurrentSelectedProject.Files.GetFile (fileName) != null) project = IdeApp.ProjectOperations.CurrentSelectedProject; else if (IdeApp.ProjectOperations.CurrentSelectedProject.FileName == fileName) project = IdeApp.ProjectOperations.CurrentSelectedProject; } if (project == null && IdeApp.ProjectOperations.CurrentSelectedWorkspaceItem != null) { project = IdeApp.ProjectOperations.CurrentSelectedWorkspaceItem.GetProjectsContainingFile (fileName).FirstOrDefault (); if (project == null) { WorkspaceItem it = IdeApp.ProjectOperations.CurrentSelectedWorkspaceItem.ParentWorkspace; while (it != null && project == null) { project = it.GetProjectsContainingFile (fileName).FirstOrDefault (); it = it.ParentWorkspace; } } } if (project == null) { project = IdeApp.Workspace.GetProjectsContainingFile (fileName).FirstOrDefault (); } return project; } async Task RealOpenFile (ProgressMonitor monitor, FileOpenInformation openFileInfo) { FilePath fileName; Counters.OpenDocumentTimer.Trace ("Checking file"); string origName = openFileInfo.FileName; if (origName == null) { monitor.ReportError (GettextCatalog.GetString ("Invalid file name"), null); return false; } fileName = openFileInfo.FileName; if (!origName.StartsWith ("http://", StringComparison.Ordinal)) fileName = fileName.FullPath; //Debug.Assert(FileService.IsValidPath(fileName)); if (FileService.IsDirectory (fileName)) { monitor.ReportError (GettextCatalog.GetString ("{0} is a directory", fileName), null); return false; } // test, if file fileName exists if (!origName.StartsWith ("http://", StringComparison.Ordinal)) { // test, if an untitled file should be opened if (!Path.IsPathRooted(origName)) { foreach (Document doc in Documents) { if (doc.Window.ViewContent.IsUntitled && doc.Window.ViewContent.UntitledName == origName) { doc.Select (); openFileInfo.NewContent = doc.Window.ViewContent; return true; } } } if (!File.Exists (fileName)) { monitor.ReportError (GettextCatalog.GetString ("File not found: {0}", fileName), null); return false; } } Counters.OpenDocumentTimer.Trace ("Looking for binding"); IDisplayBinding binding = null; IViewDisplayBinding viewBinding = null; Project project = openFileInfo.Project ?? GetProjectContainingFile (fileName); if (openFileInfo.DisplayBinding != null) { binding = viewBinding = openFileInfo.DisplayBinding; } else { var bindings = DisplayBindingService.GetDisplayBindings (fileName, null, project).ToList (); if (openFileInfo.Options.HasFlag (OpenDocumentOptions.OnlyInternalViewer)) { binding = bindings.OfType().FirstOrDefault (d => d.CanUseAsDefault) ?? bindings.OfType().FirstOrDefault (); viewBinding = (IViewDisplayBinding) binding; } else if (openFileInfo.Options.HasFlag (OpenDocumentOptions.OnlyExternalViewer)) { binding = bindings.OfType().FirstOrDefault (d => d.CanUseAsDefault); viewBinding = null; } else { binding = bindings.FirstOrDefault (d => d.CanUseAsDefault); viewBinding = binding as IViewDisplayBinding; } } try { if (binding != null) { if (viewBinding != null) { var fw = new LoadFileWrapper (monitor, workbench, viewBinding, project, openFileInfo); await fw.Invoke (fileName); } else { var extBinding = (IExternalDisplayBinding)binding; var app = extBinding.GetApplication (fileName, null, project); app.Launch (fileName); } Counters.OpenDocumentTimer.Trace ("Adding to recent files"); DesktopService.RecentFiles.AddFile (fileName, project); } else if (!openFileInfo.Options.HasFlag (OpenDocumentOptions.OnlyInternalViewer)) { try { Counters.OpenDocumentTimer.Trace ("Showing in browser"); DesktopService.OpenFile (fileName); } catch (Exception ex) { LoggingService.LogError ("Error opening file: " + fileName, ex); MessageService.ShowError (GettextCatalog.GetString ("File '{0}' could not be opened", fileName)); return false; } } } catch (Exception ex) { monitor.ReportError ("", ex); return false; } return true; } void OnStoringWorkspaceUserPreferences (object s, UserPreferencesEventArgs args) { WorkbenchUserPrefs prefs = new WorkbenchUserPrefs (); var nbId = 0; var fwId = 1; foreach (var window in DockWindow.GetAllWindows ()) { int x, y; window.GetPosition (out x, out y); var fwp = new FloatingWindowUserPrefs { WindowId = fwId, X = x, Y = y, Width = window.Allocation.Width, Height = window.Allocation.Height }; foreach (var nb in window.Container.GetNotebooks ()) AddNotebookDocuments (args, fwp.Files, nb, nbId++); if (fwp.Files.Count > 0) { prefs.FloatingWindows.Add (fwp); fwId++; } } var mainContainer = workbench.TabControl.Container; foreach (var nb in mainContainer.GetNotebooks ()) AddNotebookDocuments (args, prefs.Files, nb, nbId++); foreach (Pad pad in Pads) { IMementoCapable mc = pad.GetMementoCapable (); if (mc != null) { ICustomXmlSerializer mem = mc.Memento; if (mem != null) { PadUserPrefs data = new PadUserPrefs (); data.Id = pad.Id; StringWriter w = new StringWriter (); XmlTextWriter tw = new XmlTextWriter (w); mem.WriteTo (tw); XmlDocument doc = new XmlDocument (); doc.LoadXml (w.ToString ()); data.State = doc.DocumentElement; prefs.Pads.Add (data); } } } if (ActiveDocument != null) prefs.ActiveDocument = FileService.AbsoluteToRelativePath (args.Item.BaseDirectory, ActiveDocument.FileName); args.Properties.SetValue ("MonoDevelop.Ide.Workbench", prefs); } static void AddNotebookDocuments (UserPreferencesEventArgs args, List files, DockNotebook notebook, int notebookId) { foreach (var tab in notebook.Tabs) { var sdiwindow = (SdiWorkspaceWindow)tab.Content; var document = sdiwindow.Document; if (!String.IsNullOrEmpty (document.FileName)) { var dp = CreateDocumentPrefs (args, document); dp.NotebookId = notebookId; files.Add (dp); } } } static DocumentUserPrefs CreateDocumentPrefs (UserPreferencesEventArgs args, Document document) { var dp = new DocumentUserPrefs (); dp.FileName = FileService.AbsoluteToRelativePath (args.Item.BaseDirectory, document.FileName); if (document.Editor != null) { dp.Line = document.Editor.CaretLine; dp.Column = document.Editor.CaretColumn; } return dp; } async Task OnLoadingWorkspaceUserPreferences (object s, UserPreferencesEventArgs args) { WorkbenchUserPrefs prefs = args.Properties.GetValue ("MonoDevelop.Ide.Workbench"); if (prefs == null) return; try { IdeApp.Workbench.LockActiveWindowChangeEvent (); NavigationHistoryService.LogActiveDocument (); List> docViews = new List> (); FilePath baseDir = args.Item.BaseDirectory; var floatingWindows = new List (); using (ProgressMonitor pm = ProgressMonitors.GetStatusProgressMonitor (GettextCatalog.GetString ("Loading workspace documents"), Stock.StatusSolutionOperation, true)) { var docList = prefs.Files.Distinct (new DocumentUserPrefsFilenameComparer ()).OrderBy (d => d.NotebookId).ToList (); await OpenDocumentsInContainer (pm, baseDir, docViews, docList, workbench.TabControl.Container); foreach (var fw in prefs.FloatingWindows) { var dockWindow = new DockWindow (); dockWindow.Move (fw.X, fw.Y); dockWindow.Resize (fw.Width, fw.Height); docList = fw.Files.Distinct (new DocumentUserPrefsFilenameComparer ()).OrderBy (d => d.NotebookId).ToList (); await OpenDocumentsInContainer (pm, baseDir, docViews, docList, dockWindow.Container); floatingWindows.Add (dockWindow); } // Note: At this point, the progress monitor will be disposed which causes the gtk main-loop to be pumped. // This is EXTREMELY important, because without this main-loop pumping action, the next foreach() loop will // not cause the Solution tree-view to properly expand, nor will the ActiveDocument be set properly. } string currentFileName = prefs.ActiveDocument != null ? baseDir.Combine (prefs.ActiveDocument).FullPath : null; Document activeDoc = null; foreach (var t in docViews) { Document doc = WrapDocument (t.Item1.WorkbenchWindow); if (t.Item2 == currentFileName) activeDoc = doc; } if (activeDoc == null) { activeDoc = docViews.Select (t => WrapDocument (t.Item1.WorkbenchWindow)).FirstOrDefault (); } foreach (PadUserPrefs pi in prefs.Pads) { foreach (Pad pad in IdeApp.Workbench.Pads) { if (pi.Id == pad.Id) { pad.InternalContent.SetPreferences(pi); break; } } } foreach (var w in floatingWindows) w.ShowAll (); if (activeDoc != null) { activeDoc.RunWhenLoaded (() => { var window = activeDoc.Window; if (window != null) window.SelectWindow (); }); } } finally { IdeApp.Workbench.UnlockActiveWindowChangeEvent (); } } async Task OpenDocumentsInContainer (ProgressMonitor pm, FilePath baseDir, List> docViews, List list, DockNotebookContainer container) { int currentNotebook = -1; DockNotebook nb = container.GetFirstNotebook (); foreach (var doc in list) { string fileName = baseDir.Combine (doc.FileName).FullPath; if (GetDocument(fileName) == null && File.Exists (fileName)) { if (doc.NotebookId != currentNotebook) { if (currentNotebook != -1 || nb == null) nb = container.InsertRight (null); currentNotebook = doc.NotebookId; } // TODO: Get the correct project. var view = await IdeApp.Workbench.BatchOpenDocument (pm, fileName, null, doc.Line, doc.Column, nb); if (view != null) { var t = new Tuple (view, fileName); docViews.Add (t); } } } return true; } internal Document FindDocument (IWorkbenchWindow window) { foreach (Document doc in Documents) if (doc.Window == window) return doc; return null; } internal Pad FindPad (PadContent padContent) { foreach (Pad pad in Pads) if (pad.Content == padContent) return pad; return null; } internal void ReorderTab (int oldPlacement, int newPlacement) { workbench.ReorderTab (oldPlacement, newPlacement); } internal void ReorderDocuments (int oldPlacement, int newPlacement) { ViewContent content = workbench.InternalViewContentCollection[oldPlacement]; workbench.InternalViewContentCollection.RemoveAt (oldPlacement); workbench.InternalViewContentCollection.Insert (newPlacement, content); Document doc = documents [oldPlacement]; documents = documents.RemoveAt (oldPlacement).Insert (newPlacement, doc); } internal void LockActiveWindowChangeEvent () { workbench.LockActiveWindowChangeEvent (); } internal void UnlockActiveWindowChangeEvent () { workbench.UnlockActiveWindowChangeEvent (); } List fileStatus; SemaphoreSlim fileStatusLock = new SemaphoreSlim (1, 1); // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc(v=vs.110).aspx static DateTime NonExistentFile = new DateTime(1601, 1, 1); internal void SaveFileStatus () { // DateTime t = DateTime.Now; List files = new List (GetKnownFiles ()); fileStatus = new List (files.Count); // Console.WriteLine ("SaveFileStatus(0) " + (DateTime.Now - t).TotalMilliseconds + "ms " + files.Count); Task.Run (async delegate { // t = DateTime.Now; try { await fileStatusLock.WaitAsync ().ConfigureAwait (false); if (fileStatus == null) return; foreach (FilePath file in files) { try { DateTime ft = File.GetLastWriteTimeUtc (file); FileData fd = new FileData (file, ft != NonExistentFile ? ft : DateTime.MinValue); fileStatus.Add (fd); } catch { // Ignore } } } finally { fileStatusLock.Release (); } // Console.WriteLine ("SaveFileStatus " + (DateTime.Now - t).TotalMilliseconds + "ms " + fileStatus.Count); }); } internal void CheckFileStatus () { if (fileStatus == null) return; Task.Run (async delegate { try { // DateTime t = DateTime.Now; await fileStatusLock.WaitAsync ().ConfigureAwait (false); if (fileStatus == null) return; List modified = new List (fileStatus.Count); foreach (FileData fd in fileStatus) { try { DateTime ft = File.GetLastWriteTimeUtc (fd.File); if (ft != NonExistentFile) { if (ft != fd.TimeUtc) modified.Add (fd.File); } else if (fd.TimeUtc != DateTime.MinValue) { FileService.NotifyFileRemoved (fd.File); } } catch { // Ignore } } if (modified.Count > 0) FileService.NotifyFilesChanged (modified); // Console.WriteLine ("CheckFileStatus " + (DateTime.Now - t).TotalMilliseconds + "ms " + fileStatus.Count); fileStatus = null; } finally { fileStatusLock.Release (); } }); } IEnumerable GetKnownFiles () { foreach (WorkspaceItem item in IdeApp.Workspace.Items) { foreach (FilePath file in item.GetItemFiles (true)) yield return file; } foreach (Document doc in documents) { if (!doc.HasProject && doc.IsFile) yield return doc.FileName; } } struct FileData { public FileData (FilePath file, DateTime timeUtc) { this.File = file; this.TimeUtc = timeUtc; } public FilePath File; public DateTime TimeUtc; } void OnDocumentOpened (DocumentEventArgs e) { try { DocumentOpened?.Invoke (this, e); } catch (Exception ex) { LoggingService.LogError ("Exception while opening documents", ex); } } void OnDocumentClosed (Document doc) { try { var e = new DocumentEventArgs (doc); DocumentClosed?.Invoke (this, e); } catch (Exception ex) { LoggingService.LogError ("Exception while closing documents", ex); } } void OnDocumentClosing (Document doc) { try { var e = new DocumentEventArgs (doc); DocumentClosing?.Invoke (this, e); } catch (Exception ex) { LoggingService.LogError ("Exception before closing documents", ex); } } public event EventHandler DocumentOpened; public event EventHandler DocumentClosed; public event EventHandler DocumentClosing; public void ReparseOpenDocuments () { foreach (var doc in Documents) { if (doc.ParsedDocument != null) doc.ReparseDocument (); } } System.Timers.Timer tabsChangedTimer = null; void DisposeTimerAndSave (object o, EventArgs e) { Runtime.RunInMainThread (() => { tabsChangedTimer.Stop (); tabsChangedTimer.Elapsed -= DisposeTimerAndSave; tabsChangedTimer.Dispose (); tabsChangedTimer = null; IdeApp.Workspace.SavePreferences (); }); } void WorkbenchTabsChanged (object sender, EventArgs ev) { if (tabsChangedTimer != null) { // Timer already started, and we want to allow it to complete // so it can't be interrupted by triggering WorkbenchTabsChanged // every few seconds. return; } tabsChangedTimer = new System.Timers.Timer (10000); tabsChangedTimer.AutoReset = false; tabsChangedTimer.Elapsed += DisposeTimerAndSave; tabsChangedTimer.Start (); } } public class FileSaveInformation { FilePath fileName; public FilePath FileName { get { return fileName; } set { fileName = value.CanonicalPath; if (fileName.IsNullOrEmpty) LoggingService.LogError ("FileName == null\n" + Environment.StackTrace); } } public Encoding Encoding { get; set; } public FileSaveInformation (FilePath fileName, Encoding encoding = null) { this.FileName = fileName; this.Encoding = encoding; } } public class FileOpenInformation { FilePath fileName; public FilePath FileName { get { return fileName; } set { fileName = value.CanonicalPath.ResolveLinks (); if (fileName.IsNullOrEmpty) LoggingService.LogError ("FileName == null\n" + Environment.StackTrace); } } public OpenDocumentOptions Options { get; set; } int offset = -1; public int Offset { get { return offset; } set { offset = value; } } public int Line { get; set; } public int Column { get; set; } public IViewDisplayBinding DisplayBinding { get; set; } public ViewContent NewContent { get; set; } public Encoding Encoding { get; set; } public Project Project { get; set; } /// /// Is true when the file is already open and reload is requested. /// public bool IsReloadOperation { get; set; } internal DockNotebook DockNotebook { get; set; } [Obsolete("Use FileOpenInformation (FilePath filePath, Project project, int line, int column, OpenDocumentOptions options)")] public FileOpenInformation (string fileName, int line, int column, OpenDocumentOptions options) { this.FileName = fileName; this.Line = line; this.Column = column; this.Options = options; } public FileOpenInformation (FilePath filePath, Project project = null) { this.FileName = filePath; this.Project = project; this.Options = OpenDocumentOptions.Default; } public FileOpenInformation (FilePath filePath, Project project, int line, int column, OpenDocumentOptions options) { this.FileName = filePath; this.Project = project; this.Line = line; this.Column = column; this.Options = options; } public FileOpenInformation (FilePath filePath, Project project, bool bringToFront) { this.FileName = filePath; this.Project = project; this.Options = OpenDocumentOptions.Default; if (bringToFront) { this.Options |= OpenDocumentOptions.BringToFront; } else { this.Options &= ~OpenDocumentOptions.BringToFront; } } static FilePath ResolveSymbolicLink (FilePath fileName) { if (fileName.IsEmpty) return fileName; try { var alreadyVisted = new HashSet (); while (true) { if (alreadyVisted.Contains (fileName)) { LoggingService.LogError ("Cyclic links detected: " + fileName); return FilePath.Empty; } alreadyVisted.Add (fileName); var linkInfo = new Mono.Unix.UnixSymbolicLinkInfo (fileName); if (linkInfo.IsSymbolicLink && linkInfo.HasContents) { FilePath contentsPath = linkInfo.ContentsPath; if (contentsPath.IsAbsolute) { fileName = linkInfo.ContentsPath; } else { fileName = fileName.ParentDirectory.Combine (contentsPath); } fileName = fileName.CanonicalPath; continue; } return ResolveSymbolicLink (fileName.ParentDirectory).Combine (fileName.FileName).CanonicalPath; } } catch (Exception) { return fileName; } } } class LoadFileWrapper { IViewDisplayBinding binding; Project project; FileOpenInformation fileInfo; DefaultWorkbench workbench; ProgressMonitor monitor; ViewContent newContent; public LoadFileWrapper (ProgressMonitor monitor, DefaultWorkbench workbench, IViewDisplayBinding binding, FileOpenInformation fileInfo) { this.monitor = monitor; this.workbench = workbench; this.fileInfo = fileInfo; this.binding = binding; } public LoadFileWrapper (ProgressMonitor monitor, DefaultWorkbench workbench, IViewDisplayBinding binding, Project project, FileOpenInformation fileInfo) : this (monitor, workbench, binding, fileInfo) { this.project = project; } public async Task Invoke (string fileName) { try { Counters.OpenDocumentTimer.Trace ("Creating content"); string mimeType = DesktopService.GetMimeTypeForUri (fileName); if (binding.CanHandle (fileName, mimeType, project)) { try { newContent = binding.CreateContent (fileName, mimeType, project); } catch (InvalidEncodingException iex) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not opened. {1}", fileName, iex.Message), null); return false; } catch (OverflowException) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not opened. File too large.", fileName), null); return false; } } else { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not be opened.", fileName), null); } if (newContent == null) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not be opened.", fileName), null); return false; } newContent.Binding = binding; if (project != null) newContent.Project = project; Counters.OpenDocumentTimer.Trace ("Loading file"); try { await newContent.Load (fileInfo); } catch (InvalidEncodingException iex) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not opened. {1}", fileName, iex.Message), null); return false; } catch (OverflowException) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not opened. File too large.", fileName), null); return false; } } catch (Exception ex) { monitor.ReportError (GettextCatalog.GetString ("The file '{0}' could not be opened.", fileName), ex); return false; } // content got re-used if (newContent.WorkbenchWindow != null) { newContent.WorkbenchWindow.SelectWindow (); fileInfo.NewContent = newContent; return true; } Counters.OpenDocumentTimer.Trace ("Showing view"); workbench.ShowView (newContent, fileInfo.Options.HasFlag (OpenDocumentOptions.BringToFront), binding, fileInfo.DockNotebook); newContent.WorkbenchWindow.DocumentType = binding.Name; var ipos = (TextEditor) newContent.GetContent (typeof(TextEditor)); if (fileInfo.Line > 0 && ipos != null) { FileSettingsStore.Remove (fileName); ipos.RunWhenLoaded (JumpToLine); } fileInfo.NewContent = newContent; return true; } void JumpToLine () { var ipos = (TextEditor) newContent.GetContent (typeof(TextEditor)); var loc = new DocumentLocation (Math.Max(1, fileInfo.Line), Math.Max(1, fileInfo.Column)); if (fileInfo.Offset >= 0) { loc = ipos.OffsetToLocation (fileInfo.Offset); } ipos.SetCaretLocation (loc, fileInfo.Options.HasFlag (OpenDocumentOptions.HighlightCaretLine)); } } [Flags] public enum OpenDocumentOptions { None = 0, BringToFront = 1, CenterCaretLine = 1 << 1, HighlightCaretLine = 1 << 2, OnlyInternalViewer = 1 << 3, OnlyExternalViewer = 1 << 4, TryToReuseViewer = 1 << 5, Default = BringToFront | CenterCaretLine | HighlightCaretLine | TryToReuseViewer, Debugger = BringToFront | CenterCaretLine | TryToReuseViewer, DefaultInternal = Default | OnlyInternalViewer, } }