// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using ICSharpCode.Core;
namespace ICSharpCode.SharpDevelop.Workbench
{
///
/// Options for use with
///
[Flags]
public enum GetModelOptions
{
None = 0,
///
/// Return stale models without reloading.
///
AllowStale = 1,
///
/// Do not load any models:
/// Returns null if the model is not already loaded, or if it is stale and the AllowStale option isn't in use.
///
DoNotLoad = 2,
///
/// Allows showing modal dialogs during the GetModel() call.
/// For example, the previous model might ask the user details on how to save.
///
AllowUserInteraction = 4,
}
///
/// Option that control how handles the dirty flag.
///
public enum ReplaceModelMode
{
///
/// The new model is marked as dirty; and any other models are marked as stale.
///
SetAsDirty,
///
/// The new model is marked as valid; the status of any other models is unchanged.
///
SetAsValid,
///
/// The new model is marked as dirty or valid (depending on whether the OpenedFile was previously dirty),
/// the previously dirty model (if any) is marked as stale, and any other models are unchanged.
/// This mode is intended for use in implementations.
///
TransferDirty
}
///
/// Represents an opened file.
///
///
/// This class uses reference counting: the AddReference() and ReleaseReference()
/// methods are used to track an explicit reference count.
/// When the last reference is released, the opened file will be disposed.
/// View contents must maintain a reference to their opened files
/// (this usually happens via the AbstractViewContent.Files collection).
///
public class OpenedFile : ICanBeDirty
{
abstract class ModelEntry
{
public bool IsStale;
public abstract object Provider { get; }
public abstract void Save(OpenedFile file, FileSaveOptions options);
public abstract void SaveCopyAs(OpenedFile file, FileName outputFileName, FileSaveOptions options);
public abstract void NotifyRename(OpenedFile file, FileName oldName, FileName newName);
public abstract void NotifyStale(OpenedFile file);
public abstract void NotifyLoaded(OpenedFile file);
public abstract void NotifyUnloaded(OpenedFile file);
public abstract bool NeedsSaveForLoadInto(IFileModelProvider modelProvider) where T : class;
}
class ModelEntry : ModelEntry where T : class
{
readonly IFileModelProvider provider;
public T Model;
public ModelEntry(IFileModelProvider provider, T model)
{
Debug.Assert(provider != null);
Debug.Assert(model != null);
this.provider = provider;
this.Model = model;
}
public override object Provider { get { return provider; } }
public override void Save(OpenedFile file, FileSaveOptions options)
{
provider.Save(file, Model, options);
}
public override void SaveCopyAs(OpenedFile file, FileName outputFileName, FileSaveOptions options)
{
provider.SaveCopyAs(file, Model, outputFileName, options);
}
public override void NotifyRename(OpenedFile file, FileName oldName, FileName newName)
{
provider.NotifyRename(file, Model, oldName, newName);
}
public override void NotifyStale(OpenedFile file)
{
provider.NotifyStale(file, Model);
}
public override void NotifyLoaded(OpenedFile file)
{
provider.NotifyLoaded(file, Model);
}
public override void NotifyUnloaded(OpenedFile file)
{
provider.NotifyUnloaded(file, Model);
}
public override bool NeedsSaveForLoadInto(IFileModelProvider modelProvider)
{
return modelProvider.CanLoadFrom(provider);
}
}
readonly List entries = new List();
ModelEntry dirtyEntry;
bool preventLoading;
#region IsDirty implementation
bool isDirty;
public event EventHandler IsDirtyChanged;
///
/// Gets/sets if the file is has unsaved changes.
///
public bool IsDirty {
get { return isDirty; }
private set {
if (isDirty != value) {
isDirty = value;
if (IsDirtyChanged != null) {
IsDirtyChanged(this, EventArgs.Empty);
}
}
}
}
#endregion
#region FileName
///
/// Gets if the file is untitled. Untitled files show a "Save as" dialog when they are saved.
///
public bool IsUntitled {
get {
return fileName.ToString().StartsWith("untitled:", StringComparison.Ordinal);
}
}
FileName fileName;
///
/// Gets the name of the file.
///
public FileName FileName {
get { return fileName; }
set {
if (fileName != value) {
ChangeFileName(value);
}
}
}
///
/// Occurs when the file name has changed.
///
public event EventHandler FileNameChanged;
protected virtual void ChangeFileName(FileName newValue)
{
SD.MainThread.VerifyAccess();
FileName oldName = fileName;
fileName = newValue;
foreach (var entry in entries)
entry.NotifyRename(this, oldName, newValue);
if (FileNameChanged != null) {
FileNameChanged(this, EventArgs.Empty);
}
}
#endregion
#region ReloadFromDisk
///
/// This method sets all models to 'stale', causing the file to be re-loaded from disk
/// on the next GetModel() call.
///
/// The file is untitled.
public void ReloadFromDisk()
{
CheckDisposed();
if (IsUntitled)
throw new InvalidOperationException("Cannot reload an untitled file from disk.");
// First set all entries to stale, then call NotifyStale().
foreach (var entry in entries)
entry.IsStale = true;
foreach (var entry in entries)
entry.NotifyStale(this);
this.IsDirty = false;
}
#endregion
#region SaveToDisk
///
/// Saves the file to disk.
///
/// If the file is saved successfully, the dirty flag will be cleared (the dirty model becomes valid instead).
/// The file is untitled.
public void SaveToDisk(FileSaveOptions options)
{
CheckDisposed();
if (IsUntitled)
throw new InvalidOperationException("Cannot save an untitled file to disk.");
SaveToDisk(this.FileName, options);
}
///
/// Changes the file name, and saves the file to disk.
///
/// If the file is saved successfully, the dirty flag will be cleared (the dirty model becomes valid instead).
public virtual void SaveToDisk(FileName fileName, FileSaveOptions options)
{
CheckDisposed();
bool safeSaving = SD.FileService.SaveUsingTemporaryFile && SD.FileSystem.FileExists(fileName);
FileName saveAs = safeSaving ? FileName.Create(fileName + ".bak") : fileName;
SaveCopyTo(saveAs, options);
if (safeSaving) {
DateTime creationTime = File.GetCreationTimeUtc(fileName);
File.Delete(fileName);
try {
File.Move(saveAs, fileName);
} catch (UnauthorizedAccessException) {
// sometime File.Move raise exception (TortoiseSVN, Anti-vir ?)
// try again after short delay
System.Threading.Thread.Sleep(250);
File.Move(saveAs, fileName);
}
File.SetCreationTimeUtc(fileName, creationTime);
}
dirtyEntry = null;
this.FileName = fileName;
this.IsDirty = false;
}
///
/// Saves a copy of the file to disk. Does not change the name of the OpenedFile to the specified file name, and does not reset the dirty flag.
///
public virtual void SaveCopyAs(FileName fileName, FileSaveOptions options)
{
CheckDisposed();
SaveCopyTo(fileName, options);
}
void SaveCopyTo(FileName outputFileName, FileSaveOptions options)
{
preventLoading = true;
try {
var entry = PickValidEntry();
if (entry != null) {
entry.SaveCopyAs(this, outputFileName, options);
} else if (outputFileName != this.FileName) {
SD.FileSystem.CopyFile(this.FileName, outputFileName, true);
}
} finally {
preventLoading = false;
}
}
#endregion
#region GetModel
ModelEntry GetEntry(IFileModelProvider modelProvider) where T : class
{
CheckDisposed();
foreach (var entry in entries) {
if (object.Equals(entry.Provider, modelProvider))
return (ModelEntry)entry;
}
return null;
}
///
/// Retrieves a file model, loading it if necessary.
///
/// The model provider for the desired model type. Built-in model providers can be found in the class.
/// Options that control how
/// The model instance, or possibly null if GetModelOptions.DoNotLoad is in use.
/// Error loading the file.
/// Cannot construct the model because the underyling data is in an invalid format.
public T GetModel(IFileModelProvider modelProvider, GetModelOptions options = GetModelOptions.None) where T : class
{
if (modelProvider == null)
throw new ArgumentNullException("modelProvider");
if (preventLoading && (options & GetModelOptions.DoNotLoad) == 0)
throw new InvalidOperationException("GetModel() operations that potentially load models are not permitted at this point. (to retrieve existing models, use the DoNotLoad option)");
ModelEntry entry = GetEntry(modelProvider);
// Return existing model if possible:
if (entry != null && ((options & GetModelOptions.AllowStale) != 0 || !entry.IsStale))
return entry.Model;
// If we aren't allowed to load, just return null:
if ((options & GetModelOptions.DoNotLoad) != 0)
return null;
Debug.Assert(!preventLoading);
preventLoading = true;
try {
// Before we can load the requested model, save the dirty model (if necessary):
while (dirtyEntry != null && dirtyEntry.NeedsSaveForLoadInto(modelProvider)) {
var saveOptions = FileSaveOptions.SaveForGetModel;
if ((options & GetModelOptions.AllowUserInteraction) != 0)
saveOptions |= FileSaveOptions.AllowUserInteraction;
dirtyEntry.Save(this, saveOptions);
// re-fetch entry because it's possible that it was created/replaced/unloaded
entry = GetEntry(modelProvider);
// if the entry was made valid by the save operation, return it directly
if (entry != null && !entry.IsStale)
return entry.Model;
}
} finally {
preventLoading = false;
}
// Load the model. Note that we do allow (and expect) recursive loads at this point.
T model = modelProvider.Load(this);
// re-fetch entry because it's possible that it was created/replaced/unloaded (normally this shouldn't happen, but let's be on the safe side)
entry = GetEntry(modelProvider);
if (entry == null) {
// No entry for the model provider exists; we need to create a new one.
entry = new ModelEntry(modelProvider, model);
entries.Add(entry);
entry.NotifyLoaded(this);
} else if (entry.Model == model) {
// The existing stale model was reused
entry.IsStale = false;
} else {
// The model is being replaced
entry.NotifyUnloaded(this);
entry.Model = model;
entry.IsStale = false;
entry.NotifyLoaded(this);
}
return model;
}
#endregion
#region MakeDirty
ModelEntry PickValidEntry()
{
if (dirtyEntry != null)
return dirtyEntry; // prefer dirty entry
foreach (var entry in entries) {
if (!entry.IsStale) {
return entry;
}
}
return null;
}
///
/// Takes a valid model and marks it as dirty.
/// If no valid model exists, this method has no effect.
/// If multiple valid models exist, one is picked at random.
///
///
/// This method is used when SharpDevelop detects that the file was changed externally;
/// but the user does not want to reload the file.
///
public void MakeDirty()
{
var entry = PickValidEntry();
if (entry != null) {
dirtyEntry = entry;
this.IsDirty = true;
}
}
///
/// Sets the model associated with the specified model provider to be dirty.
/// All other models are marked as stale. If another model was previously dirty, those earlier changes will be lost.
///
///
/// This method is usually called by the model provider. In a well-designed model, any change to the model
/// should result in the model provider calling MakeDirty() automatically.
///
public void MakeDirty(IFileModelProvider modelProvider) where T : class
{
if (modelProvider == null)
throw new ArgumentNullException("modelProvider");
var entry = GetEntry(modelProvider);
if (entry == null)
throw new ArgumentException("There is no model loaded for the specified model provider.");
entry.IsStale = false;
dirtyEntry = entry;
MarkAllAsStaleExcept(entry);
this.IsDirty = true;
}
void MarkAllAsStaleExcept(ModelEntry entry)
{
foreach (var otherEntry in entries) {
if (otherEntry != entry) {
otherEntry.IsStale = true;
}
}
// Raise events after all state is updated:
foreach (var otherEntry in entries) {
if (otherEntry != entry) {
otherEntry.NotifyStale(this);
}
}
}
#endregion
#region UnloadModel
///
/// Unloads the model associated with the specified model provider.
/// Unloading the dirty model will cause changes to be lost.
///
public void UnloadModel(IFileModelProvider modelProvider) where T : class
{
if (modelProvider == null)
throw new ArgumentNullException("modelProvider");
var entry = GetEntry(modelProvider);
if (entry != null) {
if (dirtyEntry == entry) {
dirtyEntry = null;
}
entries.Remove(entry);
entry.NotifyUnloaded(this);
}
}
#endregion
#region ReplaceModel
///
/// Replaces the model associated with the specified model provider with a different instance.
///
/// The model provider for the model type.
/// The new model instance.
/// Specifies how the dirty flag is handled during the replacement.
/// By default, the new model is marked as dirty and all other models are marked as stale.
/// In implementations, you should use instead.
public void ReplaceModel(IFileModelProvider modelProvider, T model, ReplaceModelMode mode = ReplaceModelMode.SetAsDirty) where T : class
{
if (modelProvider == null)
throw new ArgumentNullException("modelProvider");
var entry = GetEntry(modelProvider);
if (entry == null) {
entry = new ModelEntry(modelProvider, model);
} else {
if (entry.Model != model) {
entry.NotifyUnloaded(this);
entry.Model = model;
entry.NotifyLoaded(this);
}
entry.IsStale = false;
}
switch (mode) {
case ReplaceModelMode.SetAsDirty:
dirtyEntry = entry;
MarkAllAsStaleExcept(entry);
this.IsDirty = true;
break;
case ReplaceModelMode.SetAsValid:
if (dirtyEntry == entry) {
dirtyEntry = null;
this.IsDirty = false;
}
break;
case ReplaceModelMode.TransferDirty:
if (dirtyEntry != null)
dirtyEntry = entry;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
#endregion
#region Reference Counting
int referenceCount = 1;
void CheckDisposed()
{
if (referenceCount <= 0)
throw new ObjectDisposedException("OpenedFile");
}
///
/// Gets the reference count of the OpenedFile.
///
public int ReferenceCount {
get { return referenceCount; }
}
public void AddReference()
{
CheckDisposed();
referenceCount++;
}
public void ReleaseReference()
{
CheckDisposed();
if (--referenceCount == 0) {
UnloadFile();
}
}
///
/// Unloads the file, this method is called once after the reference count has reached zero.
///
protected virtual void UnloadFile()
{
Debug.Assert(referenceCount == 0);
foreach (var entry in entries) {
entry.NotifyUnloaded(this);
}
// Free memory consumed by models even if the OpenedFile is leaked somewhere
entries.Clear();
dirtyEntry = null;
}
#endregion
}
/*
///
/// Represents an opened file.
///
public abstract class OpenedFile : ICanBeDirty
{
protected IViewContent currentView;
bool inLoadOperation;
bool inSaveOperation;
///
/// holds unsaved file content in memory when view containing the file was closed but no other view
/// activated
///
byte[] fileData;
///
/// Use this method to save the file to disk using a new name.
///
public void SaveToDisk(FileName newFileName)
{
this.FileName = newFileName;
this.IsUntitled = false;
SaveToDisk();
}
public abstract void RegisterView(IViewContent view);
public abstract void UnregisterView(IViewContent view);
public virtual void CloseIfAllViewsClosed()
{
}
///
/// Forces initialization of the specified view.
///
public virtual void ForceInitializeView(IViewContent view)
{
if (view == null)
throw new ArgumentNullException("view");
bool success = false;
try {
if (currentView != view) {
if (currentView == null) {
SwitchedToView(view);
} else {
try {
inLoadOperation = true;
using (Stream sourceStream = OpenRead()) {
view.Load(this, sourceStream);
}
} finally {
inLoadOperation = false;
}
}
}
success = true;
} finally {
// Only in case of exceptions:
// (try-finally with bool is better than try-catch-rethrow because it causes the debugger to stop
// at the original error location, not at the rethrow)
if (!success) {
view.Dispose();
}
}
}
///
/// Gets the list of view contents registered with this opened file.
///
public abstract IList RegisteredViewContents {
get;
}
///
/// Gets the view content that currently edits this file.
/// If there are multiple view contents registered, this returns the view content that was last
/// active. The property might return null even if view contents are registered if the last active
/// content was closed. In that case, the file is stored in-memory and loaded when one of the
/// registered view contents becomes active.
///
public IViewContent CurrentView {
get { return currentView; }
}
///
/// Opens the file for reading.
///
public virtual Stream OpenRead()
{
if (fileData != null) {
return new MemoryStream(fileData, false);
} else {
return new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
}
}
///
/// Sets the internally stored data to the specified byte array.
/// This method should only be used when there is no current view or by the
/// current view.
///
///
/// Use this method to specify the initial file content if you use a OpenedFile instance
/// for a file that doesn't exist on disk but should be automatically created when a view
/// with the file is saved, e.g. for .resx files created by the forms designer.
///
public virtual void SetData(byte[] fileData)
{
if (fileData == null)
throw new ArgumentNullException("fileData");
if (inLoadOperation)
throw new InvalidOperationException("SetData cannot be used while loading");
if (inSaveOperation)
throw new InvalidOperationException("SetData cannot be used while saving");
this.fileData = fileData;
}
///
/// Save the file to disk using the current name.
///
public virtual void SaveToDisk()
{
if (IsUntitled)
throw new InvalidOperationException("Cannot save an untitled file to disk!");
LoggingService.Debug("Save " + FileName);
bool safeSaving = SD.FileService.SaveUsingTemporaryFile && File.Exists(FileName);
string saveAs = safeSaving ? FileName + ".bak" : FileName;
using (FileStream fs = new FileStream(saveAs, FileMode.Create, FileAccess.Write)) {
if (currentView != null) {
SaveCurrentViewToStream(fs);
} else {
fs.Write(fileData, 0, fileData.Length);
}
}
if (safeSaving) {
DateTime creationTime = File.GetCreationTimeUtc(FileName);
File.Delete(FileName);
try {
File.Move(saveAs, FileName);
} catch (UnauthorizedAccessException) {
// sometime File.Move raise exception (TortoiseSVN, Anti-vir ?)
// try again after short delay
System.Threading.Thread.Sleep(250);
File.Move(saveAs, FileName);
}
File.SetCreationTimeUtc(FileName, creationTime);
}
IsDirty = false;
}
// ///
// /// Called before saving the current view. This event is raised both when saving to disk and to memory (for switching between views).
// ///
// public event EventHandler SavingCurrentView;
//
// ///
// /// Called after saving the current view. This event is raised both when saving to disk and to memory (for switching between views).
// ///
// public event EventHandler SavedCurrentView;
void SaveCurrentViewToStream(Stream stream)
{
// if (SavingCurrentView != null)
// SavingCurrentView(this, EventArgs.Empty);
inSaveOperation = true;
try {
currentView.Save(this, stream);
} finally {
inSaveOperation = false;
}
// if (SavedCurrentView != null)
// SavedCurrentView(this, EventArgs.Empty);
}
protected void SaveCurrentView()
{
using (MemoryStream memoryStream = new MemoryStream()) {
SaveCurrentViewToStream(memoryStream);
fileData = memoryStream.ToArray();
}
}
public void SwitchedToView(IViewContent newView)
{
if (newView == null)
throw new ArgumentNullException("newView");
if (currentView == newView)
return;
if (currentView != null) {
if (newView.SupportsSwitchToThisWithoutSaveLoad(this, currentView)
|| currentView.SupportsSwitchFromThisWithoutSaveLoad(this, newView))
{
// switch without Save/Load
currentView.SwitchFromThisWithoutSaveLoad(this, newView);
newView.SwitchToThisWithoutSaveLoad(this, currentView);
currentView = newView;
return;
}
SaveCurrentView();
}
try {
inLoadOperation = true;
Properties memento = GetMemento(newView);
using (Stream sourceStream = OpenRead()) {
IViewContent oldView = currentView;
bool success = false;
try {
currentView = newView;
// don't reset fileData if the file is untitled, because OpenRead() wouldn't be able to read it otherwise
if (this.IsUntitled == false)
fileData = null;
newView.Load(this, sourceStream);
success = true;
} finally {
// Use finally instead of catch+rethrow so that the debugger
// breaks at the original crash location.
if (!success) {
// stay with old view in case of exceptions
currentView = oldView;
}
}
}
RestoreMemento(newView, memento);
} finally {
inLoadOperation = false;
}
}
public virtual void ReloadFromDisk()
{
var r = FileUtility.ObservedLoad(ReloadFromDiskInternal, FileName);
if (r == FileOperationResult.Failed) {
if (currentView != null && currentView.WorkbenchWindow != null) {
currentView.WorkbenchWindow.CloseWindow(true);
}
}
}
void ReloadFromDiskInternal()
{
fileData = null;
if (currentView != null) {
try {
inLoadOperation = true;
Properties memento = GetMemento(currentView);
using (Stream sourceStream = OpenRead()) {
currentView.Load(this, sourceStream);
}
IsDirty = false;
RestoreMemento(currentView, memento);
} finally {
inLoadOperation = false;
}
}
}
static Properties GetMemento(IViewContent viewContent)
{
IMementoCapable mementoCapable = viewContent.GetService();
if (mementoCapable == null) {
return null;
} else {
return mementoCapable.CreateMemento();
}
}
static void RestoreMemento(IViewContent viewContent, Properties memento)
{
if (memento != null) {
((IMementoCapable)viewContent).SetMemento(memento);
}
}
}
*/
}