diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a253756 --- /dev/null +++ b/.gitignore @@ -0,0 +1,246 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +InnoSetup/output/ diff --git a/App/App.csproj b/App/App.csproj new file mode 100644 index 0000000..07cd5cf --- /dev/null +++ b/App/App.csproj @@ -0,0 +1,160 @@ + + + + + Debug + AnyCPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4} + WinExe + Properties + App + App + v4.0 + 512 + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + full + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + {533ab47a-d1b5-45db-a37e-f053fa3699c4} + CollectionManagerDll + + + {2bdf5d5f-1cb0-47a6-8138-e4db961740f2} + CollectionManagerExtensionsDll + + + {14768636-102a-4a27-ab5a-9b5d1ba316a6} + Common + + + {9f6c4bfe-5696-4513-bb06-90dc14a56ccf} + GuiComponents + + + {573a1557-c916-4abe-bd52-94760d19cc29} + MusicPlayer + + + {18FEDA0C-D147-4286-B39A-01204808106A} + ObjectListView2012 + + + + + + + + + \ No newline at end of file diff --git a/App/BeatmapListingActionsHandler.cs b/App/BeatmapListingActionsHandler.cs new file mode 100644 index 0000000..3dbbdc9 --- /dev/null +++ b/App/BeatmapListingActionsHandler.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using App.Interfaces; +using App.Misc; +using CollectionManager.Modules.CollectionsManager; +using CollectionManagerExtensionsDll.Utils; +using Common; +using GuiComponents.Interfaces; + +namespace App +{ + public class BeatmapListingActionsHandler + { + private readonly ICollectionEditor _collectionEditor; + private readonly IUserDialogs _userDialogs; + private readonly ILoginFormView _loginForm; + + public BeatmapListingActionsHandler(ICollectionEditor collectionEditor, IUserDialogs userDialogs, ILoginFormView loginForm) + { + _collectionEditor = collectionEditor; + _userDialogs = userDialogs; + _loginForm = loginForm; + } + + public void Bind(IBeatmapListingModel beatmapListingModel) + { + beatmapListingModel.DownloadBeatmapsManaged += DownloadBeatmapsManaged; + beatmapListingModel.DownloadBeatmaps += DownloadBeatmaps; + beatmapListingModel.DeleteBeatmapsFromCollection += DeleteBeatmapsFromCollection; + beatmapListingModel.OpenBeatmapPages += OpenBeatmapPages; + } + + public void UnBind(IBeatmapListingModel beatmapListingModel) + { + beatmapListingModel.DownloadBeatmapsManaged -= DownloadBeatmapsManaged; + beatmapListingModel.DownloadBeatmaps -= DownloadBeatmaps; + beatmapListingModel.DeleteBeatmapsFromCollection -= DeleteBeatmapsFromCollection; + beatmapListingModel.OpenBeatmapPages -= OpenBeatmapPages; + } + + private void DownloadBeatmapsManaged(object sender, EventArgs args) + { + var model = (IBeatmapListingModel)sender; + var manager = OsuDownloadManager.Instance; + + if (manager.AskUserForSaveDirectoryAndLogin(_userDialogs, _loginForm)) + OsuDownloadManager.Instance.DownloadBeatmaps(model.SelectedBeatmaps); + } + private void DeleteBeatmapsFromCollection(object sender, EventArgs args) + { + var model = (IBeatmapListingModel)sender; + _collectionEditor.EditCollection(CollectionEditArgs.RemoveBeatmaps(model.CurrentCollection.Name, model.SelectedBeatmaps)); + } + + private void DownloadBeatmaps(object sender, EventArgs args) + { + var model = (IBeatmapListingModel)sender; + + var mapIds = model.SelectedBeatmaps.GetUniqueMapSetIds(); + MassOpen(mapIds, @"https://osu.ppy.sh/d/{0}"); + } + + private void OpenBeatmapPages(object sender, EventArgs args) + { + var model = (IBeatmapListingModel)sender; + + var mapIds = model.SelectedBeatmaps.GetUniqueMapSetIds(); + MassOpen(mapIds, @"https://osu.ppy.sh/s/{0}"); + } + + + private void MassOpen(HashSet dataSet, string urlFormat) + { + bool shouldContinue = true; + if (dataSet.Count > 100) + { + shouldContinue = _userDialogs.YesNoMessageBox("You are going to open " + dataSet.Count + + " map links at the same time in your default browser", "Are you sure?", MessageBoxType.Question); + } + if (shouldContinue) + { + foreach (var d in dataSet) + { + OpenLink(string.Format(urlFormat, d)); + } + } + } + private void OpenLink(string url) + { + Process.Start(url); + + } + } +} \ No newline at end of file diff --git a/App/CollectionEditor.cs b/App/CollectionEditor.cs new file mode 100644 index 0000000..1e403cd --- /dev/null +++ b/App/CollectionEditor.cs @@ -0,0 +1,55 @@ +using App.Interfaces; +using App.Misc; +using CollectionManager.DataTypes; +using CollectionManager.Enums; +using CollectionManager.Interfaces; +using CollectionManager.Modules.CollectionsManager; +using CollectionManager.Modules.FileIO.OsuDb; +using GuiComponents.Interfaces; + +namespace App +{ + public class CollectionEditor : ICollectionEditor, ICollectionNameValidator + { + private readonly ICollectionEditor _collectionEditor; + private readonly ICollectionNameValidator _collectionNameValidator; + private readonly ICollectionAddRenameForm _collectionAddRenameForm; + private readonly MapCacher _mapCacher; + + public CollectionEditor(ICollectionEditor collectionEditor, ICollectionNameValidator collectionNameValidator, + ICollectionAddRenameForm collectionAddRenameForm, MapCacher mapCacher) + { + _collectionEditor = collectionEditor; + _collectionNameValidator = collectionNameValidator; + _collectionAddRenameForm = collectionAddRenameForm; + _mapCacher = mapCacher; + } + + + public void EditCollection(CollectionEditArgs e) + { + if (e.Action == CollectionEdit.Rename || e.Action == CollectionEdit.Add) + { + bool isRenameform = e.Action == CollectionEdit.Rename; + + var newCollectionName = _collectionAddRenameForm + .GetCollectionName(IsCollectionNameValid, e.OrginalName, isRenameform); + + if (newCollectionName == "") + return; + if (e.Action == CollectionEdit.Rename) + e = CollectionEditArgs.RenameCollection(e.OrginalName, newCollectionName); + else + e = CollectionEditArgs.AddCollections(new Collections() { new Collection(_mapCacher) { Name = newCollectionName } }); + + } + + _collectionEditor.EditCollection(e); + } + + public bool IsCollectionNameValid(string name) + { + return _collectionNameValidator.IsCollectionNameValid(name); + } + } +} \ No newline at end of file diff --git a/App/GuiActionsHandler.cs b/App/GuiActionsHandler.cs new file mode 100644 index 0000000..5fc551a --- /dev/null +++ b/App/GuiActionsHandler.cs @@ -0,0 +1,33 @@ +using App.Interfaces; +using App.Presenters.Forms; +using CollectionManager.Modules.FileIO; +using GuiComponents.Interfaces; + +namespace App +{ + public class GuiActionsHandler: IBeatmapListingBindingProvider + { + + private SidePanelActionsHandler _sidePanelActionsHandler; + private BeatmapListingActionsHandler _beatmapListingActionsHandler; + + + public GuiActionsHandler(OsuFileIo osuFileIo, ICollectionEditor collectionEditor, IUserDialogs userDialogs, IMainFormView mainFormView, MainFormPresenter mainFormPresenter, ILoginFormView loginForm) + { + _sidePanelActionsHandler = new SidePanelActionsHandler(osuFileIo, collectionEditor, userDialogs, mainFormView,this,mainFormPresenter, loginForm); + + _beatmapListingActionsHandler = new BeatmapListingActionsHandler(collectionEditor,userDialogs,loginForm); + _beatmapListingActionsHandler.Bind(mainFormPresenter.BeatmapListingModel); + } + + public void Bind(IBeatmapListingModel model) + { + _beatmapListingActionsHandler.Bind(model); + } + + public void UnBind(IBeatmapListingModel model) + { + _beatmapListingActionsHandler.UnBind(model); + } + } +} \ No newline at end of file diff --git a/App/Initalizer.cs b/App/Initalizer.cs new file mode 100644 index 0000000..52b3d10 --- /dev/null +++ b/App/Initalizer.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using System.Windows.Forms; +using App.Interfaces; +using App.Misc; +using App.Models; +using App.Models.Forms; +using App.Presenters.Forms; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using CollectionManager.Modules.FileIO; +using CollectionManagerExtensionsDll.Modules.CollectionListGenerator; +using CollectionManagerExtensionsDll.Utils; +using Common; +using GuiComponents.Interfaces; + +namespace App +{ + public class Initalizer : ApplicationContext + { + public static OsuFileIo OsuFileIo = new OsuFileIo(); + public static CollectionsManagerWithCounts CollectionsManager; + public static Beatmaps LoadedBeatmaps => OsuFileIo.LoadedMaps.Beatmaps; + public static Collections LoadedCollections => CollectionsManager.LoadedCollections; + public static string OsuDirectory; + public static CollectionEditor CollectionEditor { get; private set; } + private IUserDialogs UserDialogs { get; set; }// = new GuiComponents.UserDialogs(); + + + public void Run() + { + //IUserDialogs can be implemented in WinForm or WPF or Gtk or Console or...? + UserDialogs = GuiComponentsProvider.Instance.GetClassImplementing(); + + //Get osu! directory, or end if it can't be found + OsuDirectory = OsuFileIo.OsuPathResolver.GetOsuDir(UserDialogs.IsThisPathCorrect, UserDialogs.SelectDirectory); + if (OsuDirectory == string.Empty) + { + UserDialogs.OkMessageBox("Valid osu! directory is required to run Collection Manager" + Environment.NewLine + "Exiting...", "Error", MessageBoxType.Error); + Quit(); + } + + //Load osu database and setting files + var osuDbFile = Path.Combine(OsuDirectory, @"osu!.db"); + OsuFileIo.OsuDatabase.Load(osuDbFile); + OsuFileIo.OsuSettings.Load(OsuDirectory); + BeatmapUtils.OsuSongsDirectory = OsuFileIo.OsuSettings.CustomBeatmapDirectoryLocation; + + //Init "main" classes + CollectionsManager = new CollectionsManagerWithCounts(LoadedBeatmaps); + + var collectionAddRemoveForm = GuiComponentsProvider.Instance.GetClassImplementing(); + CollectionEditor = new CollectionEditor(CollectionsManager, CollectionsManager, collectionAddRemoveForm, OsuFileIo.LoadedMaps); + + var UpdateChecker = new UpdateChecker(); + UpdateChecker.currentVersion = System.Reflection.Assembly.GetExecutingAssembly() + .GetName() + .Version + .ToString(); + var infoTextModel = new InfoTextModel(UpdateChecker); + + var mainForm = GuiComponentsProvider.Instance.GetClassImplementing(); + var mainPresenter = new MainFormPresenter(mainForm, new MainFormModel(CollectionEditor, UserDialogs), infoTextModel); + + //set initial text info and update events + SetTextData(infoTextModel); + + + var loginForm = GuiComponentsProvider.Instance.GetClassImplementing(); + new GuiActionsHandler(OsuFileIo, CollectionsManager, UserDialogs, mainForm, mainPresenter, loginForm); + + HandleMainWindowActions(mainForm); + + mainForm.ShowAndBlock(); + Quit(); + } + + private void SetTextData(IInfoTextModel model) + { + model.SetBeatmapCount(LoadedBeatmaps.Count); + CollectionsManager.LoadedCollections.CollectionChanged += (s, a) => + { + model.SetCollectionCount(CollectionsManager.CollectionsCount, CollectionsManager.BeatmapsInCollectionsCount); + model.SetMissingBeatmapCount(CollectionsManager.MissingBeatmapCount); + }; + LoadedBeatmaps.CollectionChanged += (s, a) => + { + model.SetBeatmapCount(LoadedBeatmaps.Count); + }; + } + private void HandleMainWindowActions(IMainFormView form) + { + + //TODO: export Listing of maps to CollectionTextPresenter(and refactor it as needed) + form.SidePanelView.ListAllMaps += delegate + { + var fileLocation = UserDialogs.SaveFile("Where list of all maps should be saved?", "Txt(.txt)|*.txt|Html(.html)|*.html"); + if (fileLocation == string.Empty) return; + var listGenerator = new ListGenerator(); + var CollectionListSaveType = Path.GetExtension(fileLocation).ToLower() == ".txt" + ? CollectionManagerExtensionsDll.Enums.CollectionListSaveType.Txt + : CollectionManagerExtensionsDll.Enums.CollectionListSaveType.Html; + var contents = listGenerator.GetAllMapsList(LoadedCollections, CollectionListSaveType); + File.WriteAllText(fileLocation, contents); + }; + form.SidePanelView.ListMissingMaps += delegate + { + var fileLocation = UserDialogs.SaveFile("Where list of all maps should be saved?", "Txt(.txt)|*.txt|Html(.html)|*.html"); + if (fileLocation == string.Empty) return; + var listGenerator = new ListGenerator(); + var CollectionListSaveType = Path.GetExtension(fileLocation).ToLower() == ".txt" + ? CollectionManagerExtensionsDll.Enums.CollectionListSaveType.Txt + : CollectionManagerExtensionsDll.Enums.CollectionListSaveType.Html; + var contents = listGenerator.GetMissingMapsList(LoadedCollections, CollectionListSaveType); + File.WriteAllText(fileLocation, contents); + }; + + } + + + private static void Quit() + { + if (System.Windows.Forms.Application.MessageLoop) + { + System.Windows.Forms.Application.Exit(); + } + else + { + System.Environment.Exit(1); + } + } + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IBeatmapListingModel.cs b/App/Interfaces/Controls/IBeatmapListingModel.cs new file mode 100644 index 0000000..7f14ffc --- /dev/null +++ b/App/Interfaces/Controls/IBeatmapListingModel.cs @@ -0,0 +1,37 @@ +using System; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using Gui.Misc; + +namespace App.Interfaces +{ + public interface IBeatmapListingModel + { + event EventHandler BeatmapsChanged; + event EventHandler FilteringStarted; + event EventHandler FilteringFinished; + + event EventHandler SelectedBeatmapChanged; + event EventHandler SelectedBeatmapsChanged; + + event EventHandler OpenBeatmapPages; + event EventHandler DownloadBeatmaps; + event EventHandler DownloadBeatmapsManaged; + event EventHandler DeleteBeatmapsFromCollection; + event GuiHelpers.BeatmapsEventArgs BeatmapsDropped; + + Beatmaps SelectedBeatmaps { get; set; } + Beatmap SelectedBeatmap { get; set; } + Collection CurrentCollection { get; } + void EmitOpenBeatmapPages(); + void EmitDownloadBeatmaps(); + void EmitDownloadBeatmapsManaged(); + void EmitDeleteBeatmapsFromCollection(); + void EmitBeatmapsDropped(object sender,Beatmaps beatmaps); + Beatmaps GetBeatmaps(); + void SetBeatmaps(Beatmaps beatmaps); + void SetCollection(Collection collection); + void FilterBeatmaps(string text); + IModelFilter GetFilter(); + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IBeatmapListingPresenter.cs b/App/Interfaces/Controls/IBeatmapListingPresenter.cs new file mode 100644 index 0000000..f328831 --- /dev/null +++ b/App/Interfaces/Controls/IBeatmapListingPresenter.cs @@ -0,0 +1,9 @@ +using CollectionManager.DataTypes; + +namespace App.Interfaces +{ + public interface IBeatmapListingPresenter + { + Beatmaps Beatmaps { get; set; } + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IBeatmapThumbnailModel.cs b/App/Interfaces/Controls/IBeatmapThumbnailModel.cs new file mode 100644 index 0000000..d108b75 --- /dev/null +++ b/App/Interfaces/Controls/IBeatmapThumbnailModel.cs @@ -0,0 +1,6 @@ +namespace App.Interfaces +{ + public interface IBeatmapThumbnailModel : IGenericMapSetterModel + { + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/ICollectionAddRenameModel.cs b/App/Interfaces/Controls/ICollectionAddRenameModel.cs new file mode 100644 index 0000000..a617e40 --- /dev/null +++ b/App/Interfaces/Controls/ICollectionAddRenameModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace App.Interfaces +{ + public interface ICollectionAddRenameModel + { + event EventHandler Submited; + Func IsCollectionNameValid { get; } + string OrginalCollectionName { get; } + string NewCollectionName { get; set; } + bool NewCollectionNameIsValid { get; set; } + bool UserCanceled { get; set; } + void EmitSubmited(); + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/ICollectionListingModel.cs b/App/Interfaces/Controls/ICollectionListingModel.cs new file mode 100644 index 0000000..f9cbb20 --- /dev/null +++ b/App/Interfaces/Controls/ICollectionListingModel.cs @@ -0,0 +1,18 @@ +using System; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; + +namespace App.Interfaces +{ + public interface ICollectionListingModel + { + event EventHandler CollectionsChanged; + event EventHandler SelectedCollectionsChanged; + event EventHandler CollectionEditing; + Collections GetCollections(); + Collections SelectedCollections { get; set; } + + void EmitCollectionEditing(CollectionEditArgs args); + + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/ICollectionTextModel.cs b/App/Interfaces/Controls/ICollectionTextModel.cs new file mode 100644 index 0000000..70ddb47 --- /dev/null +++ b/App/Interfaces/Controls/ICollectionTextModel.cs @@ -0,0 +1,12 @@ +using System; +using CollectionManager.DataTypes; + +namespace App.Interfaces +{ + public interface ICollectionTextModel + { + event EventHandler CollectionChanged; + void SetCollections(Collections collections); + Collections Collections { get; } + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/ICombinedBeatmapPreviewModel.cs b/App/Interfaces/Controls/ICombinedBeatmapPreviewModel.cs new file mode 100644 index 0000000..f1f5dd4 --- /dev/null +++ b/App/Interfaces/Controls/ICombinedBeatmapPreviewModel.cs @@ -0,0 +1,7 @@ +namespace App.Interfaces +{ + public interface ICombinedBeatmapPreviewModel : IGenericMapSetterModel + { + + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IDownloadManagerModel.cs b/App/Interfaces/Controls/IDownloadManagerModel.cs new file mode 100644 index 0000000..4a1da5b --- /dev/null +++ b/App/Interfaces/Controls/IDownloadManagerModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using Gui.Misc; + +namespace App.Interfaces +{ + public interface IDownloadManagerModel + { + event EventHandler DownloadItemsChanged; + event EventHandler> DownloadItemUpdated; + + event EventHandler LogInStatusChanged; + event EventHandler LogInRequest; + event EventHandler StartDownloads; + event EventHandler StopDownloads; + void EmitStartDownloads(); + void EmitStopDownloads(); + void EmitLoginRequest(); + ICollection DownloadItems { get; set; } + bool IsLoggedIn { get; set; } + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IInfoTextModel.cs b/App/Interfaces/Controls/IInfoTextModel.cs new file mode 100644 index 0000000..26344a2 --- /dev/null +++ b/App/Interfaces/Controls/IInfoTextModel.cs @@ -0,0 +1,20 @@ +using System; + +namespace App.Interfaces +{ + public interface IInfoTextModel + { + void SetBeatmapCount(int beatmapCount); + void SetCollectionCount(int collectionsCount, int beatmapsInCollectionsCount); + void SetMissingBeatmapCount(int missingBeatmapsCount); + int BeatmapCount { get; } + int BeatmapsInCollectionsCount { get; } + int MissingBeatmapsCount { get; } + int CollectionsCount { get; } + IUpdateModel GetUpdater(); + void EmitUpdateTextClicked(); + event EventHandler CountsUpdated; + event EventHandler UpdateTextClicked; + + } +} \ No newline at end of file diff --git a/App/Interfaces/Controls/IMusicControlModel.cs b/App/Interfaces/Controls/IMusicControlModel.cs new file mode 100644 index 0000000..cb07289 --- /dev/null +++ b/App/Interfaces/Controls/IMusicControlModel.cs @@ -0,0 +1,10 @@ +using System; + +namespace App.Interfaces +{ + public interface IMusicControlModel : IGenericMapSetterModel, IFormEvents + { + event EventHandler NextMapRequest; + void EmitNextMapRequest(); + } +} \ No newline at end of file diff --git a/App/Interfaces/Forms/IMainFormModel.cs b/App/Interfaces/Forms/IMainFormModel.cs new file mode 100644 index 0000000..257e45e --- /dev/null +++ b/App/Interfaces/Forms/IMainFormModel.cs @@ -0,0 +1,11 @@ +using CollectionManager.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Interfaces.Forms +{ + public interface IMainFormModel + { + ICollectionEditor GetCollectionEditor(); + IUserDialogs GetUserDialogs(); + } +} \ No newline at end of file diff --git a/App/Interfaces/IBeatmapListingBindingProvider.cs b/App/Interfaces/IBeatmapListingBindingProvider.cs new file mode 100644 index 0000000..2fadd13 --- /dev/null +++ b/App/Interfaces/IBeatmapListingBindingProvider.cs @@ -0,0 +1,8 @@ +namespace App.Interfaces +{ + public interface IBeatmapListingBindingProvider + { + void Bind(IBeatmapListingModel model); + void UnBind(IBeatmapListingModel model); + } +} \ No newline at end of file diff --git a/App/Interfaces/IFormEvents.cs b/App/Interfaces/IFormEvents.cs new file mode 100644 index 0000000..72963cd --- /dev/null +++ b/App/Interfaces/IFormEvents.cs @@ -0,0 +1,12 @@ +using System; + +namespace App.Interfaces +{ + public interface IFormEvents + { + event EventHandler FormClosed; + event EventHandler FormClosing; + void EmitFormClosing(); + void EmitFormClosed(); + } +} \ No newline at end of file diff --git a/App/Interfaces/IGenericMapSetterModel.cs b/App/Interfaces/IGenericMapSetterModel.cs new file mode 100644 index 0000000..d970441 --- /dev/null +++ b/App/Interfaces/IGenericMapSetterModel.cs @@ -0,0 +1,13 @@ +using System; +using CollectionManager.DataTypes; + +namespace App.Interfaces +{ + public interface IGenericMapSetterModel + { + void SetBeatmap(Beatmap beatmap); + Beatmap CurrentBeatmap { get; } + + event EventHandler BeatmapChanged; + } +} \ No newline at end of file diff --git a/App/Interfaces/IUpdateModel.cs b/App/Interfaces/IUpdateModel.cs new file mode 100644 index 0000000..7cfd328 --- /dev/null +++ b/App/Interfaces/IUpdateModel.cs @@ -0,0 +1,12 @@ +namespace App.Interfaces +{ + public interface IUpdateModel + { + bool IsUpdateAvaliable(); + bool Error { get; } + string newVersion { get; } + string newVersionLink { get; } + string currentVersion { get; } + void CheckIfUpdateIsAvaliable(); + } +} \ No newline at end of file diff --git a/App/Misc/BeatmapListFilter.cs b/App/Misc/BeatmapListFilter.cs new file mode 100644 index 0000000..48bba65 --- /dev/null +++ b/App/Misc/BeatmapListFilter.cs @@ -0,0 +1,75 @@ +using System; +using System.Windows.Forms; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Modules.BeatmapFilter; + +namespace App.Misc +{ + public class BeatmapListFilter: IModelFilter + { + private readonly BeatmapFilter _beatmapFilter; + private string _searchString; + private readonly object _searchStringLockingObject = new object(); + private Timer timer; + public event EventHandler FilteringStarted; + public event EventHandler FilteringFinished; + public BeatmapListFilter(Beatmaps beatmaps) + { + _beatmapFilter = new BeatmapFilter(beatmaps); + timer = new Timer(); + timer.Interval = 400; + timer.Tick += Timer_Tick; + } + + public void SetBeatmaps(Beatmaps beatmaps) + { + _beatmapFilter.SetBeatmaps(beatmaps); + } + private void Timer_Tick(object sender, EventArgs e) + { + lock (timer) + if (timer.Enabled) + timer.Stop(); + lock (_searchStringLockingObject) + { + OnFilteringStarted(); + _beatmapFilter.UpdateSearch(_searchString); + OnFilteringFinished(); + } + } + + public bool Filter(object modelObject) + { + if (modelObject is BeatmapExtension) + { + if (_beatmapFilter.BeatmapHashHidden.ContainsKey(((BeatmapExtension)modelObject).Md5)) + return !_beatmapFilter.BeatmapHashHidden[((BeatmapExtension)modelObject).Md5]; + return true; + } + return false; + } + + public void UpdateSearch(string value) + { + lock (_searchStringLockingObject) + _searchString = value; + lock (timer) + { + if (timer.Enabled) + timer.Stop(); + timer.Start(); + } + } + + protected virtual void OnFilteringFinished() + { + FilteringFinished?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnFilteringStarted() + { + FilteringStarted?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/App/Misc/GuiComponentsProvider.cs b/App/Misc/GuiComponentsProvider.cs new file mode 100644 index 0000000..529c311 --- /dev/null +++ b/App/Misc/GuiComponentsProvider.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace App.Misc +{ + public sealed class GuiComponentsProvider + { + public static GuiComponentsProvider Instance = new GuiComponentsProvider(); + + private GuiComponentsProvider() + { + LoadGuiDll(); + } + private string GuiDllLocation { get; set; } + private Assembly GuiDllAssembly { get; set; } + private void LoadGuiDll() + { + GuiDllLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "GuiComponents.dll"); + GuiDllAssembly = Assembly.LoadFile(GuiDllLocation); + } + + private IEnumerable GetTypesWithInterface(Assembly asm) + { + var it = typeof(T); + return asm.GetLoadableTypes().Where(it.IsAssignableFrom).ToList(); + } + + public T GetClassImplementing() + { + return GetClassImplementing(GuiDllAssembly); + } + + private T GetClassImplementing(Assembly asm) + { + foreach (var tt in GetTypesWithInterface(asm)) + { + return (T)Activator.CreateInstance(tt); + } + return default(T); + } + } +} \ No newline at end of file diff --git a/App/Misc/Helpers.cs b/App/Misc/Helpers.cs new file mode 100644 index 0000000..7ad428c --- /dev/null +++ b/App/Misc/Helpers.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using App.Interfaces; +using App.Models; +using App.Presenters.Forms; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using GuiComponents.Interfaces; + +namespace App.Misc +{ + public static class Helpers + { + public static string StripInvalidCharacters(string name) + { + foreach (var invalidChar in Path.GetInvalidFileNameChars()) + { + name = name.Replace(invalidChar.ToString(), string.Empty); + } + return name.Replace(".", string.Empty); + } + public static IEnumerable GetLoadableTypes(this Assembly assembly) + { + if (assembly == null) throw new ArgumentNullException("assembly"); + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null); + } + } + + public static LoginData GetLoginData(this ILoginFormView loginForm) + { + loginForm.ShowAndBlock(); + LoginData loginData = new LoginData(); + if (loginForm.ClickedLogin) + { + loginData.Username = loginForm.Login; + loginData.Password = loginForm.Password; + } + + return loginData.isValid() ? loginData : null; + } + + public static string GetCollectionName(this ICollectionAddRenameForm form, Func isCollectionNameValid, string orginalName = "", + bool isRenameForm = false) + { + ICollectionAddRenameModel model = new CollectionAddRenameModel(isCollectionNameValid,orginalName); + new CollectionAddRenameFormPresenter(form, model); + form.IsRenameForm = isRenameForm; + form.CollectionRenameView.OrginalCollectionName = orginalName; + form.ShowAndBlock(); + + return model.NewCollectionNameIsValid ? model.NewCollectionName : ""; + } + } +} diff --git a/App/Models/Controls/BeatmapListingModel.cs b/App/Models/Controls/BeatmapListingModel.cs new file mode 100644 index 0000000..ad052e5 --- /dev/null +++ b/App/Models/Controls/BeatmapListingModel.cs @@ -0,0 +1,139 @@ +using System; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using App.Interfaces; +using App.Misc; +using Gui.Misc; + +namespace App.Models +{ + public class BeatmapListingModel : IBeatmapListingModel + { + public event EventHandler BeatmapsChanged; + public event EventHandler FilteringStarted; + public event EventHandler FilteringFinished; + public event EventHandler SelectedBeatmapChanged; + public event EventHandler SelectedBeatmapsChanged; + public event EventHandler OpenBeatmapPages; + public event EventHandler DownloadBeatmaps; + public event EventHandler DownloadBeatmapsManaged; + public event EventHandler DeleteBeatmapsFromCollection; + public event GuiHelpers.BeatmapsEventArgs BeatmapsDropped; + + private BeatmapListFilter _beatmapListFilter; + private Beatmaps _beatmapsDataSource; + private Beatmap _selectedBeatmap; + public Beatmap SelectedBeatmap + { + get + { + return _selectedBeatmap; + } + set + { + _selectedBeatmap = value; + SelectedBeatmapChanged?.Invoke(this, EventArgs.Empty); + } + } + + public Collection CurrentCollection { get; private set; } + private Beatmaps _selectedBeatmaps; + public Beatmaps SelectedBeatmaps + { + get + { + return _selectedBeatmaps; + } + set + { + _selectedBeatmaps = value; + SelectedBeatmapsChanged?.Invoke(this, EventArgs.Empty); + + } + } + public void EmitOpenBeatmapPages() + { + OpenBeatmapPages?.Invoke(this, EventArgs.Empty); + } + + public void EmitDownloadBeatmaps() + { + DownloadBeatmaps?.Invoke(this, EventArgs.Empty); + } + + public void EmitDownloadBeatmapsManaged() + { + DownloadBeatmapsManaged?.Invoke(this, EventArgs.Empty); + } + + public void EmitDeleteBeatmapsFromCollection() + { + DeleteBeatmapsFromCollection?.Invoke(this, EventArgs.Empty); + } + + public void EmitBeatmapsDropped(object sender, Beatmaps beatmaps) + { + BeatmapsDropped?.Invoke(sender, beatmaps); + } + + + public BeatmapListingModel(Beatmaps dataSource) + { + SetBeatmaps(dataSource); + _beatmapListFilter = new BeatmapListFilter(GetBeatmaps()); + _beatmapListFilter.FilteringStarted += delegate { OnFilteringStarted(); }; + _beatmapListFilter.FilteringFinished += delegate { OnFilteringFinished(); }; + } + + public Beatmaps GetBeatmaps() + { + return _beatmapsDataSource; + } + public void SetBeatmaps(Beatmaps beatmaps) + { + _beatmapsDataSource = beatmaps; + OnBeatmapsChanged(); + } + + public void SetCollection(Collection collection) + { + if (collection == null) + { + SetBeatmaps(null); + CurrentCollection = collection; + return; + } + CurrentCollection = collection; + var maps = new Beatmaps(); + maps.AddRange(collection.AllBeatmaps()); + SetBeatmaps(maps); + } + + public void FilterBeatmaps(string text) + { + _beatmapListFilter.UpdateSearch(text); + } + + public IModelFilter GetFilter() + { + return _beatmapListFilter; + } + + protected virtual void OnFilteringStarted() + { + FilteringStarted?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnFilteringFinished() + { + FilteringFinished?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnBeatmapsChanged() + { + if (_beatmapsDataSource != null) + _beatmapListFilter?.SetBeatmaps(_beatmapsDataSource); + BeatmapsChanged?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/App/Models/Controls/BeatmapThumbnailModel.cs b/App/Models/Controls/BeatmapThumbnailModel.cs new file mode 100644 index 0000000..4ac2dc8 --- /dev/null +++ b/App/Models/Controls/BeatmapThumbnailModel.cs @@ -0,0 +1,8 @@ +using App.Interfaces; + +namespace App.Models +{ + public class BeatmapThumbnailModel : GenericMapSetterModel, IBeatmapThumbnailModel + { + } +} \ No newline at end of file diff --git a/App/Models/Controls/CollectionListingModel.cs b/App/Models/Controls/CollectionListingModel.cs new file mode 100644 index 0000000..479fdbc --- /dev/null +++ b/App/Models/Controls/CollectionListingModel.cs @@ -0,0 +1,58 @@ +using System; +using CollectionManager.DataTypes; +using App.Interfaces; +using CollectionManager.Modules.CollectionsManager; + +namespace App.Models +{ + public class CollectionListingModel : ICollectionListingModel + { + public event EventHandler CollectionsChanged; + public event EventHandler SelectedCollectionsChanged; + public event EventHandler CollectionEditing; + + private Collections _collections; + public CollectionListingModel(Collections collections) + { + SetCollections(collections); + } + public Collections GetCollections() + { + return _collections; + } + + private Collections _selectedCollections; + public Collections SelectedCollections + { + get { return _selectedCollections; } + set + { + _selectedCollections = value; + SelectedCollectionsChanged?.Invoke(this, EventArgs.Empty); + } + } + + public void EmitCollectionEditing(CollectionEditArgs args) + { + CollectionEditing?.Invoke(this, args); + } + + public void SetCollections(Collections collections) + { + _collections = collections; + _collections.CollectionChanged += _collections_CollectionChanged; + + OnCollectionsChanged(); + } + + private void _collections_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + OnCollectionsChanged(); + } + + protected virtual void OnCollectionsChanged() + { + CollectionsChanged?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/App/Models/Controls/CollectionRenameModel.cs b/App/Models/Controls/CollectionRenameModel.cs new file mode 100644 index 0000000..bac22bf --- /dev/null +++ b/App/Models/Controls/CollectionRenameModel.cs @@ -0,0 +1,32 @@ +using System; +using App.Interfaces; + +namespace App.Models +{ + public class CollectionAddRenameModel : ICollectionAddRenameModel + { + public event EventHandler Submited; + public Func IsCollectionNameValid { get; } + public string OrginalCollectionName { get; } + public string NewCollectionName { get; set; } = ""; + private bool _newCollectionNameIsValid = true; + public bool UserCanceled { get; set; } = false; + + public bool NewCollectionNameIsValid + { + get { return !UserCanceled && _newCollectionNameIsValid; } + set { _newCollectionNameIsValid = value; } + } + + public void EmitSubmited() + { + Submited?.Invoke(this, EventArgs.Empty); + } + + public CollectionAddRenameModel(Func isCollectionNameValid, string orginalCollectionName="") + { + IsCollectionNameValid = isCollectionNameValid; + OrginalCollectionName = orginalCollectionName; + } + } +} \ No newline at end of file diff --git a/App/Models/Controls/CollectionTextModel.cs b/App/Models/Controls/CollectionTextModel.cs new file mode 100644 index 0000000..4d8df52 --- /dev/null +++ b/App/Models/Controls/CollectionTextModel.cs @@ -0,0 +1,35 @@ +using System; +using CollectionManager.DataTypes; +using App.Interfaces; + +namespace App.Models +{ + public class CollectionTextModel: ICollectionTextModel + { + public event EventHandler CollectionChanged; + public void SetCollections(Collections collections) + { + Collections = collections; + } + + private Collections _collections; + + public Collections Collections + { + get + { + return _collections; + } + set + { + _collections = value; + OnCollectionChanged(); + } + } + + protected virtual void OnCollectionChanged() + { + CollectionChanged?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/App/Models/Controls/CombinedBeatmapPreviewModel.cs b/App/Models/Controls/CombinedBeatmapPreviewModel.cs new file mode 100644 index 0000000..bd43090 --- /dev/null +++ b/App/Models/Controls/CombinedBeatmapPreviewModel.cs @@ -0,0 +1,8 @@ +using App.Interfaces; + +namespace App.Models +{ + public class CombinedBeatmapPreviewModel : GenericMapSetterModel, ICombinedBeatmapPreviewModel + { + } +} \ No newline at end of file diff --git a/App/Models/Controls/DownloadManagerModel.cs b/App/Models/Controls/DownloadManagerModel.cs new file mode 100644 index 0000000..402a1d7 --- /dev/null +++ b/App/Models/Controls/DownloadManagerModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using App.Interfaces; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using Gui.Misc; + +namespace App.Models +{ + public class DownloadManagerModel : IDownloadManagerModel + { + private readonly OsuDownloadManager _osuDownloadManager; + public event EventHandler DownloadItemsChanged; + public event EventHandler> DownloadItemUpdated; + public event EventHandler LogInStatusChanged; + public event EventHandler LogInRequest; + public event EventHandler StartDownloads; + public event EventHandler StopDownloads; + public void EmitStartDownloads() + { + StartDownloads?.Invoke(this, EventArgs.Empty); + _osuDownloadManager?.ResumeDownloads(); + } + + public void EmitStopDownloads() + { + StopDownloads?.Invoke(this, EventArgs.Empty); + _osuDownloadManager?.PauseDownloads(); + } + + public void EmitLoginRequest() + { + LogInRequest?.Invoke(this, EventArgs.Empty); + } + + private ICollection _downloadItems; + + public ICollection DownloadItems + { + get + { + return _downloadItems; + } + set + { + _downloadItems = value; + DownloadItemsChanged?.Invoke(this, EventArgs.Empty); + } + } + + + private bool _isLoggedIn; + + public DownloadManagerModel(OsuDownloadManager osuDownloadManager) + { + _osuDownloadManager = osuDownloadManager; + _downloadItems = _osuDownloadManager.DownloadItems; + _osuDownloadManager.DownloadItemsChanged += OsuDownloadManagerOnDownloadItemsChanged; + _osuDownloadManager.DownloadItemUpdated += OsuDownloadManagerOnDownloadItemUpdated; + } + + private void OsuDownloadManagerOnDownloadItemUpdated(object sender, EventArgs eventArgs) + { + DownloadItemUpdated?.Invoke(this, eventArgs); + } + + private void OsuDownloadManagerOnDownloadItemsChanged(object sender, EventArgs eventArgs) + { + DownloadItems = _osuDownloadManager.DownloadItems; + } + + public bool IsLoggedIn + { + get { return _isLoggedIn; } + set + { + _isLoggedIn = value; + LogInStatusChanged?.Invoke(this, EventArgs.Empty); + } + } + } +} \ No newline at end of file diff --git a/App/Models/Controls/InfoTextModel.cs b/App/Models/Controls/InfoTextModel.cs new file mode 100644 index 0000000..a67dc58 --- /dev/null +++ b/App/Models/Controls/InfoTextModel.cs @@ -0,0 +1,56 @@ +using System; +using App.Interfaces; + +namespace App.Models +{ + public class InfoTextModel : IInfoTextModel + { + public InfoTextModel(IUpdateModel updateModel) + { + UpdateModel = updateModel; + } + + public int BeatmapCount { get; set; } + public int BeatmapsInCollectionsCount { get; set; } + public int MissingBeatmapsCount { get; set; } + public int CollectionsCount { get; set; } + private IUpdateModel UpdateModel { get; } + public IUpdateModel GetUpdater() + { + return UpdateModel; + } + + public void EmitUpdateTextClicked() + { + UpdateTextClicked?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler CountsUpdated; + public event EventHandler UpdateTextClicked; + + public void SetBeatmapCount(int beatmapCount) + { + BeatmapCount = beatmapCount; + OnCountsUpdated(); + } + + public void SetCollectionCount(int collectionsCount, int beatmapsInCollectionsCount) + { + CollectionsCount = collectionsCount; + BeatmapsInCollectionsCount = beatmapsInCollectionsCount; + OnCountsUpdated(); + } + + public void SetMissingBeatmapCount(int missingBeatmapsCount) + { + MissingBeatmapsCount = missingBeatmapsCount; + OnCountsUpdated(); + } + + protected virtual void OnCountsUpdated() + { + CountsUpdated?.Invoke(this, EventArgs.Empty); + } + + } +} \ No newline at end of file diff --git a/App/Models/Controls/MusicControlModel.cs b/App/Models/Controls/MusicControlModel.cs new file mode 100644 index 0000000..18840b6 --- /dev/null +++ b/App/Models/Controls/MusicControlModel.cs @@ -0,0 +1,15 @@ +using System; +using App.Interfaces; + +namespace App.Models +{ + public class MusicControlModel : GenericMapSetterModel, IMusicControlModel + { + public void EmitFormClosing() { FormClosing?.Invoke(this, EventArgs.Empty); } + public void EmitFormClosed() { FormClosed?.Invoke(this, EventArgs.Empty); } + public void EmitNextMapRequest() { NextMapRequest?.Invoke(this, EventArgs.Empty); } + public event EventHandler FormClosed; + public event EventHandler FormClosing; + public event EventHandler NextMapRequest; + } +} \ No newline at end of file diff --git a/App/Models/Forms/MainFormModel.cs b/App/Models/Forms/MainFormModel.cs new file mode 100644 index 0000000..2ac5a47 --- /dev/null +++ b/App/Models/Forms/MainFormModel.cs @@ -0,0 +1,29 @@ +using App.Interfaces; +using App.Interfaces.Forms; +using GuiComponents.Interfaces; + +namespace App.Models.Forms +{ + public class MainFormModel : IMainFormModel + { + + public MainFormModel(ICollectionEditor collectionEditor, IUserDialogs userDialogs) + { + UserDialogs = userDialogs; + CollectionEditor = collectionEditor; + } + + private ICollectionEditor CollectionEditor { get; } + public ICollectionEditor GetCollectionEditor() + { + return CollectionEditor; + } + + private IUserDialogs UserDialogs { get; } + public IUserDialogs GetUserDialogs() + { + return UserDialogs; + } + + } +} \ No newline at end of file diff --git a/App/Models/GenericMapSetterModel.cs b/App/Models/GenericMapSetterModel.cs new file mode 100644 index 0000000..5de79f0 --- /dev/null +++ b/App/Models/GenericMapSetterModel.cs @@ -0,0 +1,36 @@ +using System; +using App.Interfaces; +using CollectionManager.DataTypes; + +namespace App.Models +{ + public abstract class GenericMapSetterModel :IGenericMapSetterModel + { + public virtual void SetBeatmap(Beatmap beatmap) + { + CurrentBeatmap = beatmap; + } + + private Beatmap _currentBeatmap; + + public virtual Beatmap CurrentBeatmap + { + get + { + return _currentBeatmap; + } + set + { + _currentBeatmap = value; + OnBeatmapChanged(); + } + } + + public event EventHandler BeatmapChanged; + + protected virtual void OnBeatmapChanged() + { + BeatmapChanged?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/App/OsuDownloadManager.cs b/App/OsuDownloadManager.cs new file mode 100644 index 0000000..3ce076f --- /dev/null +++ b/App/OsuDownloadManager.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Net; +using App.Misc; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Modules.DownloadManager; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using Gui.Misc; +using GuiComponents.Interfaces; + +namespace App +{ + public sealed class OsuDownloadManager + { + public static OsuDownloadManager Instance = new OsuDownloadManager(); + //Collections for preparing downloads + private Beatmaps BeatmapsToDownload { get; } = new Beatmaps(); + private HashSet ListedMapSetIds { get; } = new HashSet(); + /// + /// Stores all requested downloads + /// + public ICollection DownloadItems { get; private set; } = new List(); + + private OsuDownloader _osuDownloader = null; + + public event EventHandler DownloadItemsChanged; + public event EventHandler> DownloadItemUpdated; + + public bool IsLoggedIn { get; private set; } = false; + public string DownloadDirectory { get; set; } = ""; + public bool DownloadDirectoryIsSet => DownloadDirectory != ""; + private long _downloadId = 0; + private const string BaseDownloadUrl = "https://osu.ppy.sh/d/{0}"; + + public bool AskUserForSaveDirectoryAndLogin(IUserDialogs userDIalogs, ILoginFormView loginForm) + { + if (!DownloadDirectoryIsSet) + { + SetDownloadDirectory(userDIalogs.SelectDirectory("Select directory for saved beatmaps", true)); + if (!DownloadDirectoryIsSet) + return false; + } + if (!IsLoggedIn) + { + LogIn(loginForm.GetLoginData()); + if (!IsLoggedIn) + return false; + } + return true; + } + public void DownloadBeatmap(Beatmap beatmap) + { + DownloadBeatmap(beatmap, true); + } + + public void PauseDownloads() + { + _osuDownloader?.StopDownloads(); + } + public void ResumeDownloads() + { + _osuDownloader.ResumeNewDownloads(); + } + + public void SetDownloadDirectory(string path) + { + if (DownloadDirectory != "") + throw new NotImplementedException("Changing of download directory while it has been set before is not supported."); + DownloadDirectory = path; + _osuDownloader = new OsuDownloader(DownloadDirectory, 3); + _osuDownloader.ProgressUpdated += OsuDownloaderOnProgressUpdated; + } + + private void OsuDownloaderOnProgressUpdated(object sender, DownloadProgressChangedEventArgs downloadProgressChangedEventArgs) + { + var item = (DownloadItem)downloadProgressChangedEventArgs.UserState; + if (item.ProgressPrecentage % 10 == 0) + DownloadItemUpdated?.Invoke(this, new EventArgs(item)); + } + + internal void DownloadBeatmaps(Beatmaps selectedBeatmaps) + { + foreach (var selectedBeatmap in selectedBeatmaps) + { + this.DownloadBeatmap(selectedBeatmap, false); + } + DownloadBeatmap(null, true); + } + + public void LogIn(LoginData loginData) + { + if (loginData!=null && loginData.isValid()) + { + IsLoggedIn = _osuDownloader.Login(loginData); + } + } + + private void DownloadBeatmap(Beatmap beatmap, bool fireUpdateEvent) + { + if (beatmap != null) + { + BeatmapsToDownload.Add((BeatmapExtension)beatmap); + var downloadItem = GetDownloadItem((BeatmapExtension)beatmap); + if (downloadItem == null) + return; + DownloadItems.Add(downloadItem); + ListedMapSetIds.Add(beatmap.MapSetId); + } + if (fireUpdateEvent) + DownloadItemsChanged?.Invoke(this, EventArgs.Empty); + } + + private DownloadItem GetDownloadItem(Beatmap beatmap) + { + if (beatmap.MapSetId < 1 || ListedMapSetIds.Contains(beatmap.MapSetId)) + return null; + long currentId = ++_downloadId; + var oszFileName = CreateFileName(beatmap); + var downloadUrl = string.Format(BaseDownloadUrl, beatmap.MapSetId); + + var downloadItem = _osuDownloader.DownloadFileAsync(downloadUrl, oszFileName, currentId); + downloadItem.Id = currentId; + return downloadItem; + } + private string CreateFileName(Beatmap map) + { + var filename = map.MapSetId + " " + map.ArtistRoman + " - " + map.TitleRoman; + return Helpers.StripInvalidCharacters(filename) + ".osz"; + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/BeatmapListingPresenter.cs b/App/Presenters/Controls/BeatmapListingPresenter.cs new file mode 100644 index 0000000..8bbf449 --- /dev/null +++ b/App/Presenters/Controls/BeatmapListingPresenter.cs @@ -0,0 +1,69 @@ +using System; +using CollectionManager.DataTypes; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class BeatmapListingPresenter: IBeatmapListingPresenter + { + readonly IBeatmapListingView _view; + readonly IBeatmapListingModel _model; + + private Beatmaps _beatmaps; + + public Beatmaps Beatmaps + { + get + { + return _beatmaps; + } + set + { + _beatmaps = value; + _view.SetBeatmaps(value); + } + } + public BeatmapListingPresenter(IBeatmapListingView view, IBeatmapListingModel model) + { + _view = view; + _view.SearchTextChanged+=ViewOnSearchTextChanged; + _view.SelectedBeatmapChanged += (s, a) => _model.SelectedBeatmap = _view.SelectedBeatmap; + _view.SelectedBeatmapsChanged += (s, a) => _model.SelectedBeatmaps = _view.SelectedBeatmaps; + _view.DeleteBeatmapsFromCollection+= (s, a) => _model.EmitDeleteBeatmapsFromCollection(); + _view.DownloadBeatmaps += (s, a) => _model.EmitDownloadBeatmaps(); + _view.DownloadBeatmapsManaged += (s, a) => _model.EmitDownloadBeatmapsManaged(); + _view.OpenBeatmapPages += (s, a) => _model.EmitOpenBeatmapPages(); + _view.BeatmapsDropped += (s, a) => _model.EmitBeatmapsDropped(s, a); + + _model = model; + _model.BeatmapsChanged += _model_BeatmapsChanged; + _model.FilteringStarted+=ModelOnFilteringStarted; + _model.FilteringFinished += _model_FilteringFinished; + _view.SetFilter(_model.GetFilter()); + Beatmaps = _model.GetBeatmaps(); + } + + private void _model_FilteringFinished(object sender, EventArgs e) + { + _view.FilteringFinished(); + } + + private void ModelOnFilteringStarted(object sender, EventArgs eventArgs) + { + _view.FilteringStarted(); + } + + private void ViewOnSearchTextChanged(object sender, EventArgs eventArgs) + { + _model.FilterBeatmaps(_view.SearchText); + } + + private void _model_BeatmapsChanged(object sender, System.EventArgs e) + { + Beatmaps = _model.GetBeatmaps(); + } + + + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/BeatmapThumbnailPresenter.cs b/App/Presenters/Controls/BeatmapThumbnailPresenter.cs new file mode 100644 index 0000000..d356f74 --- /dev/null +++ b/App/Presenters/Controls/BeatmapThumbnailPresenter.cs @@ -0,0 +1,90 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Globalization; +using System.Threading; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Utils; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class BeatmapThumbnailPresenter + { + private IBeatmapThumbnailView _view; + private readonly IBeatmapThumbnailModel _model; + private readonly object _lockingObject = new object(); + private BeatmapExtension _currentBeatmap = null; + private AutoResetEvent resetEvent = new AutoResetEvent(false); + + public BeatmapThumbnailPresenter(IBeatmapThumbnailView view, IBeatmapThumbnailModel model) + { + _view = view; + + _model = model; + _model.BeatmapChanged += _model_BeatmapChanged; + + } + + private void _model_BeatmapChanged(object sender, System.EventArgs e) + { + lock (_lockingObject) + { + _currentBeatmap = (BeatmapExtension)_model.CurrentBeatmap; + if (_currentBeatmap == null) + { + //SetViewData(null, null); + return; + } + LoadData(_currentBeatmap); + } + } + + private long changeId = 0; + private void LoadData(BeatmapExtension beatmap) + {//TODO: OPTIMIZATION: do not load same image twice(or more) in a row(Cache last loaded image location and compare?) + string imgPath = beatmap.GetImageLocation(); + BackgroundWorker bw = new BackgroundWorker(); + var currentId = Interlocked.Increment(ref changeId); + bw.DoWork += (s, e) => + { + //resetEvent.WaitOne(100); + if (beatmap.Equals(_currentBeatmap)) + { + Image img = null; + if (imgPath != "") + img = Image.FromFile(imgPath); + if (currentId != Interlocked.Read(ref changeId)) + { + img?.Dispose(); + return; + } + SetViewData(img, beatmap); + } + }; + bw.RunWorkerAsync(); + } + private void SetViewData(Image image, Beatmap beatmap) + { + _view.beatmapImage = image; + if (beatmap == null) + { + _view.AR = ""; + _view.CS = ""; + _view.OD = ""; + _view.Stars = ""; + _view.BeatmapName = ""; + } + else + { + _view.AR = Math.Round(beatmap.ApproachRate, 2).ToString(CultureInfo.InvariantCulture); + _view.CS = Math.Round(beatmap.CircleSize, 2).ToString(CultureInfo.InvariantCulture); + _view.OD = Math.Round(beatmap.OverallDifficulty, 2).ToString(CultureInfo.InvariantCulture); + _view.Stars = Math.Round(beatmap.StarsNomod, 2).ToString(CultureInfo.InvariantCulture); + _view.BeatmapName = ((BeatmapExtension)beatmap).ToString(true); + } + + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/CollectionAddRenamePresenter.cs b/App/Presenters/Controls/CollectionAddRenamePresenter.cs new file mode 100644 index 0000000..8208ecf --- /dev/null +++ b/App/Presenters/Controls/CollectionAddRenamePresenter.cs @@ -0,0 +1,50 @@ +using System; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + class CollectionAddRenamePresenter + { + private readonly ICollectionRenameView _view; + private readonly ICollectionAddRenameModel _model; + + public CollectionAddRenamePresenter(ICollectionRenameView view, ICollectionAddRenameModel model) + { + _view = view; + _model = model; + + _view.CollectionNameChanged += ViewOnCollectionNameChanged; + _view.Submited += ViewOnSubmited; + _view.Canceled += ViewOnCanceled; + } + + private void Unbind() + { + _view.CollectionNameChanged -= ViewOnCollectionNameChanged; + _view.Submited -= ViewOnSubmited; + _view.Canceled -= ViewOnCanceled; + } + private void ViewOnCanceled(object sender, EventArgs eventArgs) + { + _model.UserCanceled = true; + Unbind(); + } + + private void ViewOnSubmited(object sender, EventArgs eventArgs) + { + _model.EmitSubmited(); + Unbind(); + } + + private void ViewOnCollectionNameChanged(object sender, EventArgs eventArgs) + { + bool isValid = _model.IsCollectionNameValid(_view.NewCollectionName); + _view.CanSubmit = isValid; + _view.ErrorText = isValid ? "" : "Collection name is invalid!"; + + _model.NewCollectionNameIsValid = isValid; + _model.NewCollectionName = _view.NewCollectionName; + } + } +} diff --git a/App/Presenters/Controls/CollectionListingPresenter.cs b/App/Presenters/Controls/CollectionListingPresenter.cs new file mode 100644 index 0000000..b292991 --- /dev/null +++ b/App/Presenters/Controls/CollectionListingPresenter.cs @@ -0,0 +1,90 @@ +using System; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class CollectionListingPresenter + { + ICollectionListingView _view; + + private Collections _collections; + private ICollectionListingModel _model; + + public Collections Collections + { + get + { + return _collections; + } + set + { + _collections = value; + var selectedCollection = _view.SelectedCollection; + _view.Collections = value; + if (_collections.Contains(selectedCollection)) + _view.SelectedCollection = selectedCollection; + } + } + public CollectionListingPresenter(ICollectionListingView view, ICollectionListingModel model) + { + _view = view; + _view.RightClick += _view_RightClick; + _view.SelectedCollectionsChanged += ViewOnSelectedCollectionsChanged; + _view.BeatmapsDropped += ViewOnBeatmapsDropped; + _model = model; + _model.CollectionsChanged += ModelOnCollectionsChanged; + Collections = _model.GetCollections(); + } + + private void ViewOnBeatmapsDropped(object sender, Beatmaps args, string collectionName) + { + _model.EmitCollectionEditing(CollectionEditArgs.AddBeatmaps(collectionName, args)); + } + + + private void ViewOnSelectedCollectionsChanged(object sender, EventArgs eventArgs) + { + Collections selectedCollections = new Collections(); + foreach (var collection in _view.SelectedCollections) + { + selectedCollections.Add((Collection)collection); + } + _model.SelectedCollections = selectedCollections; + } + + private void _view_RightClick(object sender, Gui.Misc.StringEventArgs e) + { + var selectedCollections = _model.SelectedCollections; + CollectionEditArgs args; + switch (e.Value) + { + case "Delete": + args = CollectionEditArgs.RemoveCollections(selectedCollections); + break; + case "Merge": + args = CollectionEditArgs.MergeCollections(selectedCollections, selectedCollections[0].Name); + break; + case "Create": + args = CollectionEditArgs.AddCollections(null); + break; + case "Rename": + if (_view.SelectedCollection == null) + return; + args = CollectionEditArgs.RenameCollection(_view.SelectedCollection, null); + break; + default: + return; + } + _model.EmitCollectionEditing(args); + + } + + private void ModelOnCollectionsChanged(object sender, EventArgs eventArgs) + { + Collections = _model.GetCollections(); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/CollectionTextPresenter.cs b/App/Presenters/Controls/CollectionTextPresenter.cs new file mode 100644 index 0000000..9c80210 --- /dev/null +++ b/App/Presenters/Controls/CollectionTextPresenter.cs @@ -0,0 +1,36 @@ +using System; +using CollectionManagerExtensionsDll.Enums; +using CollectionManagerExtensionsDll.Modules.CollectionListGenerator; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class CollectionTextPresenter + { + private ICollectionTextView _view; + private readonly ICollectionTextModel _model; + private ListGenerator _listGenerator = new ListGenerator(); + public CollectionTextPresenter(ICollectionTextView view, ICollectionTextModel model) + { + _view = view; + _view.SetListTypes(Enum.GetValues(typeof(CollectionListSaveType))); + _view.SaveTypeChanged += _view_SaveTypeChanged; + + _model = model; + _model.CollectionChanged += ModelOnCollectionChanged; + } + + private void _view_SaveTypeChanged(object sender, EventArgs e) + { + ModelOnCollectionChanged(this, null); + } + + private void ModelOnCollectionChanged(object sender, EventArgs eventArgs) + { + CollectionListSaveType type; + Enum.TryParse(_view.SelectedSaveType, out type); + _view.GeneratedText = _listGenerator.GetAllMapsList(_model.Collections, type); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/CombinedBeatmapPreviewPresenter.cs b/App/Presenters/Controls/CombinedBeatmapPreviewPresenter.cs new file mode 100644 index 0000000..39efd9d --- /dev/null +++ b/App/Presenters/Controls/CombinedBeatmapPreviewPresenter.cs @@ -0,0 +1,34 @@ +using System; +using App.Interfaces; +using App.Models; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class CombinedBeatmapPreviewPresenter + { + private readonly ICombinedBeatmapPreviewView _view; + private readonly ICombinedBeatmapPreviewModel _model; + + private readonly IBeatmapThumbnailModel _beatmapThumbnailModel; + public readonly IMusicControlModel MusicControlModel; + public CombinedBeatmapPreviewPresenter(ICombinedBeatmapPreviewView view, ICombinedBeatmapPreviewModel model) + { + _view = view; + _model = model; + _model.BeatmapChanged+=ModelOnBeatmapChanged; + _beatmapThumbnailModel = new BeatmapThumbnailModel(); + new BeatmapThumbnailPresenter(_view.BeatmapThumbnailView, _beatmapThumbnailModel); + + MusicControlModel = new MusicControlModel(); + new MusicControlPresenter(_view.MusicControlView, MusicControlModel); + } + + private void ModelOnBeatmapChanged(object sender, EventArgs eventArgs) + { + var map = _model.CurrentBeatmap; + _beatmapThumbnailModel.SetBeatmap(map); + MusicControlModel.SetBeatmap(map); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/CombinedListingPresenter.cs b/App/Presenters/Controls/CombinedListingPresenter.cs new file mode 100644 index 0000000..4587278 --- /dev/null +++ b/App/Presenters/Controls/CombinedListingPresenter.cs @@ -0,0 +1,39 @@ +using System; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class CombinedListingPresenter + { + private readonly ICombinedListingView _view; + private readonly IBeatmapListingView _beatmapsView; + private readonly ICollectionListingView _collectionsView; + public readonly IBeatmapListingModel BeatmapListingModel; + public CombinedListingPresenter(ICombinedListingView view, ICollectionListingModel collectionListingModel,IBeatmapListingModel beatmapListingModel) + { + _view = view; + _beatmapsView = _view.beatmapListingView; + _collectionsView = _view.CollectionListingView; + + BeatmapListingModel = beatmapListingModel; + new BeatmapListingPresenter(_beatmapsView, BeatmapListingModel); + new CollectionListingPresenter(_collectionsView, collectionListingModel); + + _collectionsView.SelectedCollectionChanged += CollectionsViewOnSelectedCollectionChanged; + _collectionsView.SelectedCollectionsChanged += CollectionsViewOnSelectedCollectionsChanged; + } + + private void CollectionsViewOnSelectedCollectionsChanged(object sender, EventArgs eventArgs) + { + + } + + private void CollectionsViewOnSelectedCollectionChanged(object sender, EventArgs eventArgs) + { + var collection = _collectionsView.SelectedCollection; + BeatmapListingModel.SetCollection(collection); + _beatmapsView.ClearSelection(); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/DownloadManagerPresenter.cs b/App/Presenters/Controls/DownloadManagerPresenter.cs new file mode 100644 index 0000000..8f60f49 --- /dev/null +++ b/App/Presenters/Controls/DownloadManagerPresenter.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using App.Interfaces; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using Gui.Misc; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class DownloadManagerPresenter + { + private readonly IDownloadManagerView _view; + private readonly IDownloadManagerModel _model; + private Timer enableButtonTimer = new Timer(); + + private bool _downloadsActive = true; + + public DownloadManagerPresenter(IDownloadManagerView view, IDownloadManagerModel model) + { + _view = view; + _view.DownloadToggleClick += ViewOnDownloadToggleClick; + _view.Disposed += (s, a) => + { + _model.DownloadItemsChanged -= ModelOnDownloadItemsChanged; + _model.DownloadItemUpdated -= ModelOnDownloadItemUpdated; + }; + + enableButtonTimer.Tick += EnableButtonTimer_Tick; + enableButtonTimer.Interval = 3000; + + _model = model; + _model.DownloadItemsChanged += ModelOnDownloadItemsChanged; + _model.DownloadItemUpdated += ModelOnDownloadItemUpdated; + if (_model.DownloadItems.Count > 0) + PopulateView(_model.DownloadItems); + + } + + private void ModelOnDownloadItemUpdated(object sender, EventArgs eventArgs) + { + _view.UpdateDownloadItem(eventArgs.Value); + } + + private void ModelOnDownloadItemsChanged(object sender, EventArgs eventArgs) + { + PopulateView(_model.DownloadItems); + } + + private void EnableButtonTimer_Tick(object sender, EventArgs e) + { + enableButtonTimer.Stop(); + _view.DownloadButtonIsEnabled = true; + _model.EmitStopDownloads(); + } + + private void ToggleDownloads() + { + _downloadsActive = !_downloadsActive; + if (_downloadsActive) + _model.EmitStartDownloads(); + else + _model.EmitStopDownloads(); + } + private void SetDownloadButton(bool enabled = false, bool downloadsActive = false) + { + _view.DownloadButtonIsEnabled = enabled; + _view.DownloadButtonText = downloadsActive ? "Stop Downloads" : "Resume Downloads"; + } + + + private void PopulateView(ICollection downladItems) + { + _view.SetDownloadItems(downladItems); + } + private void ViewOnDownloadToggleClick(object sender, EventArgs eventArgs) + { + ToggleDownloads(); + SetDownloadButton(false, _downloadsActive); + enableButtonTimer.Start(); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/InfoTextPresenter.cs b/App/Presenters/Controls/InfoTextPresenter.cs new file mode 100644 index 0000000..320112a --- /dev/null +++ b/App/Presenters/Controls/InfoTextPresenter.cs @@ -0,0 +1,75 @@ +using System; +using App.Interfaces; +using GuiComponents.Interfaces; + +namespace App.Presenters.Controls +{ + public class InfoTextPresenter + { + private readonly IInfoTextView _view; + public readonly IInfoTextModel Model; + + private const string UpdateAvaliable = "Update is avaliable!({0})"; + private const string UpdateError = "Error while checking for updates."; + private const string UpdateNewestVersion = "No updates avaliable ({0})"; + + private const string LoadedBeatmaps = "Loaded {0} beatmaps"; + private const string LoadedCollections = "Loaded {0} collections"; + private const string LoadedCollectionsBeatmaps = "With {0} beatmaps"; + + + private const string MissingBeatmaps = "Missing {0} beatmaps"; + + public InfoTextPresenter(IInfoTextView view, IInfoTextModel model) + { + _view = view; + Model = model; + + Model.CountsUpdated += ModelOnCountsUpdated; + _view.UpdateTextClicked += ViewOnUpdateTextClicked; + } + + private void ViewOnUpdateTextClicked(object sender, EventArgs eventArgs) + { + var updater = Model.GetUpdater(); + updater.CheckIfUpdateIsAvaliable(); + + Model.EmitUpdateTextClicked(); + + } + + + private void ModelOnCountsUpdated(object sender, EventArgs eventArgs) + { + _view.BeatmapLoaded = string.Format(LoadedBeatmaps, Model.BeatmapCount); + _view.CollectionsLoaded = string.Format(LoadedCollections, Model.CollectionsCount); + _view.BeatmapsInCollections = string.Format(LoadedCollectionsBeatmaps, Model.BeatmapsInCollectionsCount); + _view.BeatmapsMissing = string.Format(MissingBeatmaps, Model.MissingBeatmapsCount); + SetUpdateText(); + } + + private void SetUpdateText() + { + var updater = Model.GetUpdater(); + if (updater != null) + { + if (updater.IsUpdateAvaliable()) + { + _view.UpdateText = string.Format(UpdateAvaliable, updater.newVersion); + } + else if (updater.Error) + { + _view.UpdateText = UpdateError; + } + else + { + _view.UpdateText = string.Format(UpdateNewestVersion, updater.currentVersion); + } + } + else + { + _view.UpdateText = ""; + } + } + } +} \ No newline at end of file diff --git a/App/Presenters/Controls/MusicControlPresenter.cs b/App/Presenters/Controls/MusicControlPresenter.cs new file mode 100644 index 0000000..3009642 --- /dev/null +++ b/App/Presenters/Controls/MusicControlPresenter.cs @@ -0,0 +1,148 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Timers; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Utils; +using App.Interfaces; +using Gui.Misc; +using GuiComponents.Interfaces; +using MusicPlayer; +using Timer = System.Timers.Timer; + +namespace App.Presenters.Controls +{ + public class MusicControlPresenter : IDisposable + { + private IMusicControlView _view; + private readonly IMusicControlModel _model; + private IMusicPlayer musicPlayer = new MusicPlayerManager(); + private Timer trackPositionTimer; + private AutoResetEvent resetEvent = new AutoResetEvent(false); + private string _lastAudioFileLocation = string.Empty; + public MusicControlPresenter(IMusicControlView view, IMusicControlModel model) + { + _view = view; + _view.PositionChanged += ViewOnPositionChanged; + _view.VolumeChanged += ViewOnVolumeChanged; + _view.PlayPressed += ViewOnPlayPressed; + _view.PausePressed += ViewOnPausePressed; + _view.Disposed += (s, a) => this.Dispose(); + if (!musicPlayer.IsSpeedControlAvaliable) + { + _view.disableDt(); + } + _view.CheckboxChanged += ViewOnCheckboxChanged; + + trackPositionTimer = new Timer(500); + trackPositionTimer.Elapsed += TrackPositionTimer_Elapsed; + trackPositionTimer.Start(); + + _model = model; + _model.BeatmapChanged += ModelOnBeatmapChanged; + musicPlayer.PlaybackFinished += MusicPlayer_PlaybackFinished; + } + + private void ViewOnCheckboxChanged(object sender, EventArgs eventArgs) + { + if (musicPlayer.IsSpeedControlAvaliable) + { + musicPlayer.SetSpeed(_view.IsDTEnabled ? 1.25f : 1.0f); + } + + + + } + + + public void Dispose() + { + trackPositionTimer.Stop(); + trackPositionTimer.Dispose(); + musicPlayer.Dispose(); + } + + + private void MusicPlayer_PlaybackFinished(object sender, EventArgs e) + { + if (_view.IsMusicPlayerMode) + _model.EmitNextMapRequest(); + } + + private void ViewOnPausePressed(object sender, EventArgs eventArgs) + { + musicPlayer.Pause(); + } + + private void ViewOnPlayPressed(object sender, EventArgs eventArgs) + { + if (_model.CurrentBeatmap != null) + { + if (musicPlayer.IsPaused) + musicPlayer.Resume(); + else + PlayBeatmap(_model.CurrentBeatmap); + } + } + + private void ViewOnVolumeChanged(object sender, FloatEventArgs floatEventArgs) + { + musicPlayer.SetVolume(floatEventArgs.Value); + } + + private void ViewOnPositionChanged(object sender, IntEventArgs intEventArgs) + { + double pos = (_view.Position / 100d) * musicPlayer.TotalTime; + musicPlayer.Seek(pos); + } + + private void ModelOnBeatmapChanged(object sender, EventArgs eventArgs) + { + var map = _model.CurrentBeatmap; + if (map == null || !_view.IsAutoPlayEnabled) + return; + var audioLocation = ((BeatmapExtension)map).FullAudioFileLocation(); + if ((_lastAudioFileLocation == audioLocation || string.IsNullOrWhiteSpace(audioLocation)) && _view.IsMusicPlayerMode && !musicPlayer.IsPlaying) + { + //Run as worker to avoid eventual stack overflow exception (eg. too many maps with no audio file in a row) + RunAsWorker(() => _model.EmitNextMapRequest()); + return; + } + _lastAudioFileLocation = audioLocation; + PlayBeatmap(map); + } + + private void PlayBeatmap(Beatmap map) + { + var audioLocation = ((BeatmapExtension)map).FullAudioFileLocation(); + if (audioLocation != string.Empty) + { + RunAsWorker(() => + { + resetEvent.WaitOne(250); + if (map.Equals(_model.CurrentBeatmap)) + { + musicPlayer.Play(audioLocation, + _view.IsMusicPlayerMode ? 0 : Convert.ToInt32(Math.Round(map.PreviewTime / 1000f))); + } + }); + } + } + private void RunAsWorker(Action action) + { + BackgroundWorker bw = new BackgroundWorker(); + bw.DoWork += (s, ee) => + { + action(); + }; + bw.RunWorkerAsync(); + } + private void TrackPositionTimer_Elapsed(object sender, ElapsedEventArgs e) + { + if (_view.IsUserSeeking) return; + int precentagePosition = Convert.ToInt32((musicPlayer.CurrentTime / musicPlayer.TotalTime) * 100); + _view.Position = precentagePosition; + } + + } +} \ No newline at end of file diff --git a/App/Presenters/Forms/BeatmapListingFormPresenter.cs b/App/Presenters/Forms/BeatmapListingFormPresenter.cs new file mode 100644 index 0000000..debafc7 --- /dev/null +++ b/App/Presenters/Forms/BeatmapListingFormPresenter.cs @@ -0,0 +1,31 @@ +using App.Presenters.Controls; +using App.Interfaces; +using App.Models; +using GuiComponents.Interfaces; + +namespace App.Presenters.Forms +{ + public class BeatmapListingFormPresenter + { + private readonly IBeatmapListingForm _view; + private ICombinedBeatmapPreviewModel _combinedBeatmapPreviewModel; + public readonly IBeatmapListingModel BeatmapListingModel; + public BeatmapListingFormPresenter(IBeatmapListingForm view) + { + _view = view; + //_view.BeatmapListingView.SelectedBeatmapChanged += BeatmapListingView_SelectedBeatmapChanged; + BeatmapListingModel = new BeatmapListingModel(Initalizer.LoadedBeatmaps); + BeatmapListingModel.SelectedBeatmapChanged += BeatmapListingView_SelectedBeatmapChanged; + new BeatmapListingPresenter(_view.BeatmapListingView, BeatmapListingModel); + + _combinedBeatmapPreviewModel = new CombinedBeatmapPreviewModel(); + var presenter =new CombinedBeatmapPreviewPresenter(_view.CombinedBeatmapPreviewView, _combinedBeatmapPreviewModel); + presenter.MusicControlModel.NextMapRequest += (s, a) => _view.BeatmapListingView.SelectNextOrFirst(); + } + + private void BeatmapListingView_SelectedBeatmapChanged(object sender, System.EventArgs e) + { + _combinedBeatmapPreviewModel.SetBeatmap(BeatmapListingModel.SelectedBeatmap); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Forms/CollectionAddRenameFormPresenter.cs b/App/Presenters/Forms/CollectionAddRenameFormPresenter.cs new file mode 100644 index 0000000..46d822e --- /dev/null +++ b/App/Presenters/Forms/CollectionAddRenameFormPresenter.cs @@ -0,0 +1,23 @@ +using System; +using App.Interfaces; +using App.Interfaces.Forms; +using App.Models; +using App.Presenters.Controls; +using GuiComponents.Interfaces; + +namespace App.Presenters.Forms +{ + public class CollectionAddRenameFormPresenter + { + private readonly ICollectionAddRenameForm _form; + private readonly ICollectionAddRenameModel _model; + + public CollectionAddRenameFormPresenter(ICollectionAddRenameForm form, ICollectionAddRenameModel model) + { + _form = form; + _model = model; + + new CollectionAddRenamePresenter(form.CollectionRenameView, model); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Forms/DownloadManagerFormPresenter.cs b/App/Presenters/Forms/DownloadManagerFormPresenter.cs new file mode 100644 index 0000000..7a3c8f7 --- /dev/null +++ b/App/Presenters/Forms/DownloadManagerFormPresenter.cs @@ -0,0 +1,19 @@ +using App.Interfaces; +using App.Presenters.Controls; +using GuiComponents.Interfaces; + +namespace App.Presenters.Forms +{ + public class DownloadManagerFormPresenter + { + private readonly IDownloadManagerFormView _formView; + private readonly IDownloadManagerView _downloadManagerView; + + public DownloadManagerFormPresenter(IDownloadManagerFormView view, IDownloadManagerModel downloadManagerModel) + { + _formView = view; + _downloadManagerView = _formView.DownloadManagerView; + new DownloadManagerPresenter(_downloadManagerView, downloadManagerModel); + } + } +} \ No newline at end of file diff --git a/App/Presenters/Forms/MainFormPresenter.cs b/App/Presenters/Forms/MainFormPresenter.cs new file mode 100644 index 0000000..ca357a2 --- /dev/null +++ b/App/Presenters/Forms/MainFormPresenter.cs @@ -0,0 +1,84 @@ +using System; +using App.Presenters.Controls; +using CollectionManager.Enums; +using App.Interfaces; +using App.Interfaces.Forms; +using App.Misc; +using App.Models; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using GuiComponents.Interfaces; + +namespace App.Presenters.Forms +{ + public class MainFormPresenter + { + private IMainFormView _view; + private readonly IMainFormModel _mainFormModel; + private ICollectionTextModel _collectionTextModel; + private readonly ICombinedBeatmapPreviewModel _combinedBeatmapPreviewModel; + public IInfoTextModel InfoTextModel; + public readonly IBeatmapListingModel BeatmapListingModel; + public readonly ICollectionListingModel CollectionListingModel; + + public MainFormPresenter(IMainFormView view, IMainFormModel mainFormModel, IInfoTextModel infoTextModel) + { + _view = view; + _mainFormModel = mainFormModel; + + //Combined listing (Collections+Beatmaps) + BeatmapListingModel = new BeatmapListingModel(null); + BeatmapListingModel.BeatmapsDropped += BeatmapListing_BeatmapsDropped; + BeatmapListingModel.SelectedBeatmapChanged += BeatmapListingViewOnSelectedBeatmapChanged; + CollectionListingModel = new CollectionListingModel(Initalizer.LoadedCollections); + CollectionListingModel.CollectionEditing += CollectionListing_CollectionEditing; + CollectionListingModel.SelectedCollectionsChanged += CollectionListing_SelectedCollectionsChanged; + new CombinedListingPresenter(_view.CombinedListingView, CollectionListingModel, BeatmapListingModel); + + //Beatmap preview stuff (images, beatmap info like ar,cs,stars...) + _combinedBeatmapPreviewModel = new CombinedBeatmapPreviewModel(); + var presenter = new CombinedBeatmapPreviewPresenter(_view.CombinedBeatmapPreviewView, _combinedBeatmapPreviewModel); + + presenter.MusicControlModel.NextMapRequest += (s, a) => { _view.CombinedListingView.beatmapListingView.SelectNextOrFirst(); }; + + _collectionTextModel = new CollectionTextModel(); + new CollectionTextPresenter(_view.CollectionTextView, _collectionTextModel); + + //General information (Collections loaded, update check etc.) + InfoTextModel = infoTextModel; + new InfoTextPresenter(_view.InfoTextView, InfoTextModel); + + } + + private void CollectionListing_CollectionEditing(object sender, CollectionEditArgs collectionEditArgs) + { + _mainFormModel.GetCollectionEditor()?.EditCollection(collectionEditArgs); + } + + private void CollectionListing_SelectedCollectionsChanged(object sender, EventArgs eventArgs) + { + var collections = CollectionListingModel.SelectedCollections; + if (collections != null) + { + _collectionTextModel.SetCollections(collections); + } + } + + + private void BeatmapListing_BeatmapsDropped(object sender, Beatmaps args) + { + if (CollectionListingModel.SelectedCollections?.Count == 1) + { + var collection = CollectionListingModel.SelectedCollections[0]; + CollectionListing_CollectionEditing(sender, CollectionEditArgs.AddBeatmaps(collection.Name, args)); + } + } + + + private void BeatmapListingViewOnSelectedBeatmapChanged(object sender, EventArgs eventArgs) + { + _combinedBeatmapPreviewModel.SetBeatmap(_view.CombinedListingView.beatmapListingView.SelectedBeatmap); + } + + } +} \ No newline at end of file diff --git a/App/Program.cs b/App/Program.cs new file mode 100644 index 0000000..42ba78a --- /dev/null +++ b/App/Program.cs @@ -0,0 +1,21 @@ +using System; +using System.Windows.Forms; + +namespace App +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + var app = new Initalizer(); + app.Run(); + Application.Run(app); + } + } +} diff --git a/App/Properties/AssemblyInfo.cs b/App/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6600af4 --- /dev/null +++ b/App/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("osu! Collections Manager by Piotrekol")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Gui")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f87a0f61-b6f3-4b5e-8a1a-c19c8c8feaa4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.1.*")] diff --git a/App/Properties/Resources.Designer.cs b/App/Properties/Resources.Designer.cs new file mode 100644 index 0000000..7119717 --- /dev/null +++ b/App/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace App.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("App.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/App/Properties/Resources.resx b/App/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/App/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/App/Properties/Settings.Designer.cs b/App/Properties/Settings.Designer.cs new file mode 100644 index 0000000..0706b74 --- /dev/null +++ b/App/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace App.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/App/Properties/Settings.settings b/App/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/App/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/App/SidePanelActionsHandler.cs b/App/SidePanelActionsHandler.cs new file mode 100644 index 0000000..03b7b5c --- /dev/null +++ b/App/SidePanelActionsHandler.cs @@ -0,0 +1,166 @@ +using System; +using System.Diagnostics; +using System.IO; +using App.Interfaces; +using App.Misc; +using App.Models; +using App.Presenters.Forms; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using CollectionManager.Modules.FileIO; +using GuiComponents.Interfaces; + +namespace App +{ + public class SidePanelActionsHandler + { + private readonly OsuFileIo _osuFileIo; + private readonly ICollectionEditor _collectionEditor; + private readonly IUserDialogs _userDialogs; + private readonly IMainFormView _mainForm; + + IBeatmapListingForm _beatmapListingForm; + private IDownloadManagerFormView _downloadManagerForm; + private readonly IBeatmapListingBindingProvider _beatmapListingBindingProvider; + private readonly MainFormPresenter _mainFormPresenter; + private readonly ILoginFormView _loginForm; + + public SidePanelActionsHandler(OsuFileIo osuFileIo, ICollectionEditor collectionEditor, IUserDialogs userDialogs, IMainFormView mainForm, IBeatmapListingBindingProvider beatmapListingBindingProvider, MainFormPresenter mainFormPresenter, ILoginFormView loginForm) + { + _osuFileIo = osuFileIo; + _collectionEditor = collectionEditor; + _userDialogs = userDialogs; + _mainForm = mainForm; + _beatmapListingBindingProvider = beatmapListingBindingProvider; + _mainFormPresenter = mainFormPresenter; + _loginForm = loginForm; + + BindMainFormActions(); + } + + private void BindMainFormActions() + { + _mainForm.SidePanelView.LoadCollection += (s, a) => LoadCollectionFile(); + _mainForm.SidePanelView.LoadDefaultCollection += (s, a) => LoadDefaultCollection(); + _mainForm.SidePanelView.ClearCollections += (s, a) => ClearCollections(); + _mainForm.SidePanelView.SaveCollections += (s, a) => SaveCollections(); + _mainForm.SidePanelView.SaveInvidualCollections += (s, a) => SaveInvidualCollections(); + + _mainForm.SidePanelView.ShowBeatmapListing += (s, a) => ShowBeatmapListing(); + _mainForm.SidePanelView.ShowDownloadManager += (s, a) => ShowDownloadManager(); + _mainForm.SidePanelView.DownloadAllMissing += (s, a) => DownloadAllMissing(); + + _mainFormPresenter.InfoTextModel.UpdateTextClicked += FormUpdateTextClicked; + _mainForm.Closing += FormOnClosing; + } + + private void DownloadAllMissing() + { + var downloadableBeatmaps = new Beatmaps(); + foreach (var collection in Initalizer.LoadedCollections) + { + foreach (var beatmap in collection.DownloadableBeatmaps) + { + downloadableBeatmaps.Add(beatmap); + } + } + + if (downloadableBeatmaps.Count > 0) + if (OsuDownloadManager.Instance.AskUserForSaveDirectoryAndLogin(_userDialogs, _loginForm)) + { + OsuDownloadManager.Instance.DownloadBeatmaps(downloadableBeatmaps); + ShowDownloadManager(); + } + } + + private void FormUpdateTextClicked(object sender, EventArgs args) + { + var updater = _mainFormPresenter.InfoTextModel.GetUpdater(); + if (updater.IsUpdateAvaliable()) + { + if (!string.IsNullOrWhiteSpace(updater.newVersionLink)) + Process.Start(updater.newVersionLink); + } + + } + + private void FormOnClosing(object sender, EventArgs eventArgs) + { + if (_beatmapListingForm != null && !_beatmapListingForm.IsDisposed) + { + _beatmapListingForm.Close(); + } + if (_downloadManagerForm != null && !_downloadManagerForm.IsDisposed) + { + _downloadManagerForm.Close(); + } + } + private void LoadCollectionFile() + { + var fileLocation = _userDialogs.SelectFile("", "Collection database (*.db/*.osdb)|*.db;*.osdb", + "collection.db"); + if (fileLocation == string.Empty) return; + var loadedCollections = _osuFileIo.CollectionLoader.LoadCollection(fileLocation); + _collectionEditor.EditCollection(CollectionEditArgs.AddCollections(loadedCollections)); + } + + private void LoadDefaultCollection() + { + var fileLocation = Path.Combine(Initalizer.OsuDirectory, "collection.db"); + if (File.Exists(fileLocation)) + { + var loadedCollections = _osuFileIo.CollectionLoader.LoadCollection(fileLocation); + _collectionEditor.EditCollection(CollectionEditArgs.AddCollections(loadedCollections)); + } + } + + private void ClearCollections() + { + _collectionEditor.EditCollection(CollectionEditArgs.ClearCollections()); + } + + private void SaveCollections() + { + var fileLocation = _userDialogs.SaveFile("Where collection file should be saved?", "osu! Collection database (.db)|*.db|CM database (.osdb)|*.osdb"); + if (fileLocation == string.Empty) return; + _osuFileIo.CollectionLoader.SaveCollection(Initalizer.LoadedCollections, fileLocation); + } + + private void SaveInvidualCollections() + { + var saveDirectory = _userDialogs.SelectDirectory("Where collection files should be saved?", true); + if (saveDirectory == string.Empty) return; + foreach (var collection in Initalizer.LoadedCollections) + { + var filename = Helpers.StripInvalidCharacters(collection.Name); + _osuFileIo.CollectionLoader.SaveCollection(new Collections() { collection }, saveDirectory + filename); + } + } + + private void ShowBeatmapListing() + { + if (_beatmapListingForm == null || _beatmapListingForm.IsDisposed) + { + _beatmapListingForm = GuiComponentsProvider.Instance.GetClassImplementing(); + var presenter = new BeatmapListingFormPresenter(_beatmapListingForm); + _beatmapListingBindingProvider.Bind(presenter.BeatmapListingModel); + _beatmapListingForm.Closing += (s, a) => _beatmapListingBindingProvider.UnBind(presenter.BeatmapListingModel); + } + _beatmapListingForm.Show(); + } + + private void CreateDownloadManagerForm() + { + if (_downloadManagerForm == null || _downloadManagerForm.IsDisposed) + { + _downloadManagerForm = GuiComponentsProvider.Instance.GetClassImplementing(); + new DownloadManagerFormPresenter(_downloadManagerForm, new DownloadManagerModel(OsuDownloadManager.Instance)); + } + } + private void ShowDownloadManager() + { + CreateDownloadManagerForm(); + _downloadManagerForm.Show(); + } + } +} \ No newline at end of file diff --git a/App/UpdateChecker.cs b/App/UpdateChecker.cs new file mode 100644 index 0000000..5a9c549 --- /dev/null +++ b/App/UpdateChecker.cs @@ -0,0 +1,65 @@ +using System; +using App.Interfaces; + +namespace App +{ + public class UpdateChecker : IUpdateModel + { + private const string UpdateUrl = "http://osustats.ppy.sh/api/ce/version"; + + public bool Error { get; private set; } + public string newVersion { get; private set; } + public string newVersionLink { get; private set; } + public string currentVersion { get; set; } = "???"; + + public bool IsUpdateAvaliable() + { + return CheckForUpdates(); + } + public void CheckIfUpdateIsAvaliable() + { + UpdateVersion(); + } + + private bool CheckForUpdates() + { + UpdateVersion(); + if (string.IsNullOrWhiteSpace(newVersion)) + { + Error = true; + return false; + } + Version verLocal, verOnline; + try + { + verLocal = new Version(currentVersion); + verOnline = new Version(newVersion); + } + catch + { + return true; + } + + + return verLocal.CompareTo(verOnline) < 0; + } + private void UpdateVersion() + { + try + { + string contents; + using (var wc = new System.Net.WebClient()) + contents = wc.DownloadString(UpdateUrl); + var splited = contents.Split(new[] { ',' }, 2); + + newVersionLink = splited[1]; + newVersion = splited[0]; + + } + catch (Exception) + { + } + + } + } +} \ No newline at end of file diff --git a/App/packages.config b/App/packages.config new file mode 100644 index 0000000..c14ebb9 --- /dev/null +++ b/App/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/CollectionGenerator/App.config b/CollectionGenerator/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/CollectionGenerator/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CollectionGenerator/CollectionGenerator.csproj b/CollectionGenerator/CollectionGenerator.csproj new file mode 100644 index 0000000..e4f0228 --- /dev/null +++ b/CollectionGenerator/CollectionGenerator.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4} + Exe + Properties + CollectionGenerator + CollectionGenerator + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {533ab47a-d1b5-45db-a37e-f053fa3699c4} + CollectionManagerDll + + + {2bdf5d5f-1cb0-47a6-8138-e4db961740f2} + CollectionManagerExtensionsDll + + + + + \ No newline at end of file diff --git a/CollectionGenerator/Program.cs b/CollectionGenerator/Program.cs new file mode 100644 index 0000000..194675d --- /dev/null +++ b/CollectionGenerator/Program.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using CollectionManager.DataTypes; +using CollectionManager.Modules.FileIO; +using CollectionManagerExtensionsDll.Modules.API; +using CollectionManagerExtensionsDll.Modules.API.osu; + + +namespace CollectionGenerator +{ + class Program + { + private static OsuApi _api; + private static readonly OsuFileIo OsuFileIo = new OsuFileIo(); + static void Main(string[] args) + { + Console.WriteLine("Collection Generator"); + Console.WriteLine("===================="); + Console.WriteLine("WARNING: ALL paths are hardcoded, meaning if you don't change them and recompile this program IT WILL NOT WORK."); //technically it could but eh, better safe than sorry. + Console.WriteLine("If you did recompile it with your correct paths then you may continue... (Press enter)"); + Console.WriteLine("===================="); + + Console.ReadLine(); + Console.WriteLine("Enter your osu!api key: "); + + string apiKey = Console.ReadLine(); + _api = new OsuApi(apiKey); + + //grab all maps from 2007 + Console.WriteLine("Getting beatmaps. This can take around 5 minutes."); + var maps = GetBeatmapsFromYear(2007, false); + + Console.WriteLine("Generating collections"); + OsuFileIo.CollectionLoader.SaveOsdbCollection(GenByYear(maps), @"D:\Kod\kolekcje\ByYear.osdb"); + OsuFileIo.CollectionLoader.SaveOsdbCollection(GenByGenreV2(maps), @"D:\Kod\kolekcje\ByGenre.osdb"); + + } + private static readonly Dictionary mapGenres = new Dictionary() + { + {0,"Any" }, + {1,"Unspecified" }, + {2,"Video Game" }, + {3,"Anime" }, + {4,"Rock" }, + {5,"Pop" }, + {6,"Other" }, + {7,"Novelity" }, + {8,"??" }, + {9,"Hip Hop" }, + {10,"Electronic" } + }; + private static readonly Dictionary languages = new Dictionary() + { + {0,"Any" }, + {1,"Other" }, + {2,"English" }, + {3,"Japanese" }, + {4,"Chinese" }, + {5,"Instrumental" }, + {6,"Korean" }, + {7,"French" }, + {8,"German" }, + {9,"Swedish" }, + {10,"Spanish" }, + {11,"Italian" } + }; + static Collections GenByGenre(Beatmaps maps) + { + var year = 2007; + Collections collections = new Collections(); + + //Prepare a bunch of collections for genre_id matching + for (int i = 0; i < 20; i++) + { + collections.Add(new Collection(OsuFileIo.LoadedMaps) { Name = i.ToString() }); + } + //place maps in collections according to genre_id + foreach (BeatmapExtensionEx map in maps) + { + collections[map.GenreId].AddBeatmap(map); + } + + RemoveEmptyCollections(collections); + + return collections; + } + static Collections GenByGenreV2(Beatmaps maps) + { + var year = 2007; + //Collections collections = new Collections(); + Dictionary> sortedMaps = new Dictionary>(); + + //Prepare a bunch of collections for genre and language matching + foreach (var mapGenre in mapGenres) + { + sortedMaps.Add(mapGenre.Key, new Dictionary()); + foreach (var language in languages) + { + sortedMaps[mapGenre.Key].Add(language.Key, + new Collection(OsuFileIo.LoadedMaps) { Name = mapGenre.Value + " (" + language.Value + ")" }); + } + } + //categorize maps + foreach (BeatmapExtensionEx map in maps) + { + sortedMaps[map.GenreId][map.LanguageId].AddBeatmap(map); + } + Collections collections = new Collections(); + + //Grab all non-empty collections + foreach (var dictOfCollections in sortedMaps) + { + foreach (var collection in dictOfCollections.Value) + { + if (collection.Value.NumberOfBeatmaps != 0) + collections.Add(collection.Value); + } + } + + return collections; + } + static Collections GenByYear(Beatmaps maps) + { + var year = 2007; + Collections collections = new Collections(); + Dictionary CollYears = new Dictionary(); + + + for (int i = year; i < 2050; i++) + { + var collection = new Collection(OsuFileIo.LoadedMaps) { Name = i.ToString() }; + collections.Add(collection); + CollYears.Add(i, collection); + } + foreach (BeatmapExtensionEx map in maps) + { + CollYears[map.ApprovedDate.Year].AddBeatmap(map); + } + + RemoveEmptyCollections(collections); + + return collections; + } + + static void RemoveEmptyCollections(Collections collections) + { + for (int i = collections.Count - 1; i > 0; i--) + { + if (collections[i].NumberOfBeatmaps == 0) + { + collections.Remove(collections[i]); + } + } + + } + + static Beatmaps GetBeatmapsFromYear(int year, bool grabJustOneYear = true) + { + DateTime startDate = new DateTime(year, 01, 01), endDate = new DateTime(year + (grabJustOneYear ? 1 : 50), 01, 01); + return _api.GetBeatmaps(startDate, endDate); + } + + } +} diff --git a/CollectionGenerator/Properties/AssemblyInfo.cs b/CollectionGenerator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1a69edb --- /dev/null +++ b/CollectionGenerator/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CollectionGenerator")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CollectionGenerator")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3070a7aa-7493-42a7-9e0e-74878453abb4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CollectionManager.sln b/CollectionManager.sln new file mode 100644 index 0000000..8e9acb6 --- /dev/null +++ b/CollectionManager.sln @@ -0,0 +1,81 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectionManagerDll", "CollectionManagerDll\CollectionManagerDll.csproj", "{533AB47A-D1B5-45DB-A37E-F053FA3699C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectionManagerExtensionsDll", "CollectionManagerExtensionsDll\CollectionManagerExtensionsDll.csproj", "{2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjectListView2012", "ObjectListView\ObjectListView2012.csproj", "{18FEDA0C-D147-4286-B39A-01204808106A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MusicPlayer", "MusicPlayer\MusicPlayer.csproj", "{573A1557-C916-4ABE-BD52-94760D19CC29}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GuiComponents", "GuiComponents\GuiComponents.csproj", "{9F6C4BFE-5696-4513-BB06-90DC14A56CCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{14768636-102A-4A27-AB5A-9B5D1BA316A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "App\App.csproj", "{F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectionGenerator", "CollectionGenerator\CollectionGenerator.csproj", "{3070A7AA-7493-42A7-9E0E-74878453ABB4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + Remote Debug|Any CPU = Remote Debug|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Release|Any CPU.Build.0 = Release|Any CPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Release|Any CPU.Build.0 = Release|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Release|Any CPU.Build.0 = Release|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {18FEDA0C-D147-4286-B39A-01204808106A}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Release|Any CPU.Build.0 = Release|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {573A1557-C916-4ABE-BD52-94760D19CC29}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Release|Any CPU.Build.0 = Release|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Release|Any CPU.Build.0 = Release|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Release|Any CPU.Build.0 = Release|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Remote Debug|Any CPU.ActiveCfg = Remote Debug|Any CPU + {F87A0F61-B6F3-4B5E-8A1A-C19C8C8FEAA4}.Remote Debug|Any CPU.Build.0 = Remote Debug|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Release|Any CPU.Build.0 = Release|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Remote Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3070A7AA-7493-42A7-9E0E-74878453ABB4}.Remote Debug|Any CPU.Build.0 = Debug|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/CollectionManager.sln.DotSettings b/CollectionManager.sln.DotSettings new file mode 100644 index 0000000..03328d0 --- /dev/null +++ b/CollectionManager.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/CollectionManagerDll/CollectionManagerDll.csproj b/CollectionManagerDll/CollectionManagerDll.csproj new file mode 100644 index 0000000..b83ede1 --- /dev/null +++ b/CollectionManagerDll/CollectionManagerDll.csproj @@ -0,0 +1,88 @@ + + + + + Debug + AnyCPU + {533AB47A-D1B5-45DB-A37E-F053FA3699C4} + Library + Properties + CollectionManager + CollectionManager + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + full + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CollectionManagerDll/DataTypes/Beatmap.cs b/CollectionManagerDll/DataTypes/Beatmap.cs new file mode 100644 index 0000000..51b4767 --- /dev/null +++ b/CollectionManagerDll/DataTypes/Beatmap.cs @@ -0,0 +1,420 @@ +using System; +using System.Collections.Generic; +using CollectionManager.Enums; + +namespace CollectionManager.DataTypes +{ + + public abstract class Beatmap : ICloneable + { + private string _titleUnicode; + public string TitleUnicode + { + get + { + return _titleUnicode == string.Empty ? TitleRoman : _titleUnicode; + } + set { _titleUnicode = value; } + } + private string _artistUnicode; + public string ArtistUnicode + { + get { return _artistUnicode == string.Empty ? ArtistRoman : _artistUnicode; } + set { _artistUnicode = value; } + } + public string TitleRoman { get; set; } + public string ArtistRoman { get; set; } + public string Creator { get; set; } + public string DiffName { get; set; } + public string Mp3Name { get; set; } + public string Md5 { get; set; } + public string OsuFileName { get; set; } + public string MapLink + { + get + { + if (MapId == 0) + return MapSetLink; + return @"http://osu.ppy.sh/b/" + MapId; + + } + } + public string MapSetLink + { + get + { + if (MapSetId == 0) + return string.Empty; + return @"http://osu.ppy.sh/s/" + MapSetId; + } + } + + public Dictionary ModPpStars = new Dictionary(); + public double StarsNomod + { + get + { + if (ModPpStars.ContainsKey(0)) + return ModPpStars[0]; + return 0d; + } + } + + public double MaxBpm { get; set; } + public double MinBpm { get; set; } + + + public string Tags { get; set; } + public string StateStr { get; private set; } + private byte _state; + public byte State + { + get { return _state; } + set + { + string val; + _state = value; + switch (value) + { + case 0: val = "Not updated"; break; + case 1: val = "Unsubmitted"; break; + case 2: val = "Pending"; break; + case 3: val = "??"; break; + case 4: val = "Ranked"; break; + case 5: val = "Approved"; break; + case 7: val = "Loved"; break; + default: val = "??" + value.ToString(); break; + } + StateStr = val; + } + } + public short Circles { get; set; } + public short Sliders { get; set; } + public short Spinners { get; set; } + public DateTime? EditDate { get; set; } + public float ApproachRate { get; set; } + public float CircleSize { get; set; } + public float HpDrainRate { get; set; } + public float OverallDifficulty { get; set; } + public double SliderVelocity { get; set; } + public int DrainingTime { get; set; } + public int TotalTime { get; set; } + public int PreviewTime { get; set; } + public int MapId { get; set; } + public int MapSetId { get; set; } + public int ThreadId { get; set; } + public int MapRating { get; set; } + public short Offset { get; set; } + public float StackLeniency { get; set; } + private PlayModes playMode; + public PlayModes PlayMode + { + get + { + return playMode; + } + + set + { + playMode = value; + if (Enum.IsDefined(PlayMode.GetType(), value)) + { + playMode = value; + } + else + { + playMode = PlayModes.Osu; + } + } + } + public string Source { get; set; } + public short AudioOffset { get; set; } + public string LetterBox { get; set; } + public bool Played { get; set; } + public DateTime? LastPlayed { get; set; } + public bool IsOsz2 { get; set; } + public string Dir { get; set; } + public DateTime? LastSync { get; set; } + public bool DisableHitsounds { get; set; } + public bool DisableSkin { get; set; } + public bool DisableSb { get; set; } + public short BgDim { get; set; } + public int Somestuff { get; set; } + public string VideoDir { get; set; } + + public Beatmap() + { + InitEmptyValues(); + } + + public void CloneValues(Beatmap b) + { + TitleUnicode = b.TitleUnicode; + TitleRoman = b.TitleRoman; + ArtistUnicode = b.ArtistUnicode; + ArtistRoman = b.ArtistRoman; + Creator = b.Creator; + DiffName = b.DiffName; + Mp3Name = b.Mp3Name; + Md5 = b.Md5; + OsuFileName = b.OsuFileName; + Tags = b.Tags; + Somestuff = b.Somestuff; + State = b.State; + Circles = b.Circles; + Sliders = b.Sliders; + Spinners = b.Spinners; + EditDate = b.EditDate; + ApproachRate = b.ApproachRate; + CircleSize = b.CircleSize; + HpDrainRate = b.HpDrainRate; + OverallDifficulty = b.OverallDifficulty; + SliderVelocity = b.SliderVelocity; + DrainingTime = b.DrainingTime; + TotalTime = b.TotalTime; + PreviewTime = b.PreviewTime; + MapId = b.MapId; + MapSetId = b.MapSetId; + ThreadId = b.ThreadId; + MapRating = b.MapRating; + Offset = b.Offset; + StackLeniency = b.StackLeniency; + PlayMode = b.PlayMode; + Source = b.Source; + AudioOffset = b.AudioOffset; + LetterBox = b.LetterBox; + Played = b.Played; + LastPlayed = b.LastPlayed; + IsOsz2 = b.IsOsz2; + Dir = b.Dir; + LastSync = b.LastSync; + DisableHitsounds = b.DisableHitsounds; + DisableSkin = b.DisableSkin; + DisableSb = b.DisableSb; + BgDim = b.BgDim; + ModPpStars = b.ModPpStars; + MaxBpm = b.MaxBpm; + MinBpm = b.MinBpm; + } + public Beatmap(Beatmap b) + { + TitleUnicode = b.TitleUnicode; + TitleRoman = b.TitleRoman; + ArtistUnicode = b.ArtistUnicode; + ArtistRoman = b.ArtistRoman; + Creator = b.Creator; + DiffName = b.DiffName; + Mp3Name = b.Mp3Name; + Md5 = b.Md5; + OsuFileName = b.OsuFileName; + Tags = b.Tags; + Somestuff = b.Somestuff; + State = b.State; + Circles = b.Circles; + Sliders = b.Sliders; + Spinners = b.Spinners; + EditDate = b.EditDate; + ApproachRate = b.ApproachRate; + CircleSize = b.CircleSize; + HpDrainRate = b.HpDrainRate; + OverallDifficulty = b.OverallDifficulty; + SliderVelocity = b.SliderVelocity; + DrainingTime = b.DrainingTime; + TotalTime = b.TotalTime; + PreviewTime = b.PreviewTime; + MapId = b.MapId; + MapSetId = b.MapSetId; + ThreadId = b.ThreadId; + MapRating = b.MapRating; + Offset = b.Offset; + StackLeniency = b.StackLeniency; + PlayMode = b.PlayMode; + Source = b.Source; + AudioOffset = b.AudioOffset; + LetterBox = b.LetterBox; + Played = b.Played; + LastPlayed = b.LastPlayed; + IsOsz2 = b.IsOsz2; + Dir = b.Dir; + LastSync = b.LastSync; + DisableHitsounds = b.DisableHitsounds; + DisableSkin = b.DisableSkin; + DisableSb = b.DisableSb; + BgDim = b.BgDim; + ModPpStars = b.ModPpStars; + MaxBpm = b.MaxBpm; + MinBpm = b.MinBpm; + } + public Beatmap(string artist) + { + InitEmptyValues(); + ArtistUnicode = artist; + } + public Beatmap(string md5, bool collection) + { + InitEmptyValues(); + Md5 = md5; + } + public void InitEmptyValues() + { + ModPpStars = new Dictionary(); + TitleUnicode = string.Empty; + TitleRoman = string.Empty; + ArtistUnicode = string.Empty; + ArtistRoman = string.Empty; + Creator = string.Empty; + DiffName = string.Empty; + Mp3Name = string.Empty; + Md5 = string.Empty; + OsuFileName = string.Empty; + Tags = string.Empty; + Somestuff = 0; + State = 0; + Circles = 0; + Sliders = 0; + Spinners = 0; + EditDate = null; + ApproachRate = 0; + CircleSize = 0; + HpDrainRate = 0; + OverallDifficulty = 0; + SliderVelocity = 0; + DrainingTime = 0; + TotalTime = 0; + PreviewTime = 0; + MapId = 0; + MapSetId = 0; + ThreadId = 0; + MapRating = 0; + Offset = 0; + StackLeniency = 0; + PlayMode = PlayModes.Osu; + Source = string.Empty; + AudioOffset = 0; + LetterBox = string.Empty; + Played = false; + LastPlayed = null; + IsOsz2 = false; + Dir = string.Empty; + LastSync = null; + DisableHitsounds = false; + DisableSkin = false; + DisableSb = false; + BgDim = 0; + MinBpm = 0.0f; + MaxBpm = 0.0f; + } + + + public object Clone() + { + return this.MemberwiseClone(); + } + + public override int GetHashCode() + { + return Md5.GetHashCode(); + } + + public override bool Equals(object obj) + { + var b = obj as Beatmap; + if (b == null) + return false; + if (TitleUnicode != b.TitleUnicode) + return false; + if (TitleRoman != b.TitleRoman) + return false; + if (ArtistUnicode != b.ArtistUnicode) + return false; + if (ArtistRoman != b.ArtistRoman) + return false; + if (Creator != b.Creator) + return false; + if (DiffName != b.DiffName) + return false; + if (Mp3Name != b.Mp3Name) + return false; + if (Md5 != b.Md5) + return false; + if (OsuFileName != b.OsuFileName) + return false; + if (Tags != b.Tags) + return false; + if (Somestuff != b.Somestuff) + return false; + if (State != b.State) + return false; + if (Circles != b.Circles) + return false; + if (Sliders != b.Sliders) + return false; + if (Spinners != b.Spinners) + return false; + if (EditDate != b.EditDate) + return false; + if (ApproachRate != b.ApproachRate) + return false; + if (CircleSize != b.CircleSize) + return false; + if (HpDrainRate != b.HpDrainRate) + return false; + if (OverallDifficulty != b.OverallDifficulty) + return false; + if (SliderVelocity != b.SliderVelocity) + return false; + if (DrainingTime != b.DrainingTime) + return false; + if (TotalTime != b.TotalTime) + return false; + if (PreviewTime != b.PreviewTime) + return false; + if (MapId != b.MapId) + return false; + if (MapSetId != b.MapSetId) + return false; + if (ThreadId != b.ThreadId) + return false; + if (MapRating != b.MapRating) + return false; + if (Offset != b.Offset) + return false; + if (StackLeniency != b.StackLeniency) + return false; + if (PlayMode != b.PlayMode) + return false; + if (Source != b.Source) + return false; + if (AudioOffset != b.AudioOffset) + return false; + if (LetterBox != b.LetterBox) + return false; + if (Played != b.Played) + return false; + if (LastPlayed != b.LastPlayed) + return false; + if (IsOsz2 != b.IsOsz2) + return false; + if (Dir != b.Dir) + return false; + if (LastSync != b.LastSync) + return false; + if (DisableHitsounds != b.DisableHitsounds) + return false; + if (DisableSkin != b.DisableSkin) + return false; + if (DisableSb != b.DisableSb) + return false; + if (BgDim != b.BgDim) + return false; + if (ModPpStars != b.ModPpStars) + return false; + if (MaxBpm != b.MaxBpm) + return false; + if (MinBpm != b.MinBpm) + return false; + return true; + } + } +} + diff --git a/CollectionManagerDll/DataTypes/BeatmapExtension.cs b/CollectionManagerDll/DataTypes/BeatmapExtension.cs new file mode 100644 index 0000000..9ca7b5e --- /dev/null +++ b/CollectionManagerDll/DataTypes/BeatmapExtension.cs @@ -0,0 +1,67 @@ +using System; + +namespace CollectionManager.DataTypes +{ + public class BeatmapExtension : Beatmap + { + + public override string ToString() + { + if (string.IsNullOrEmpty(Artist) && string.IsNullOrEmpty(Title)) + return Md5; + var baseStr = Artist + " - " + Title; + return baseStr; + } + public string ToString(bool withDiff) + { + if (withDiff) + { + return ToString() + " [" + DiffName + "]"; + } + else return ToString(); + } + + private string _artist; + public string Artist + { + get + { + if (_artist == null) + { + if (!string.IsNullOrEmpty(ArtistRoman)) + _artist = ArtistRoman; + else if (!string.IsNullOrEmpty(ArtistUnicode)) + _artist = ArtistUnicode; + else + _artist = ""; + } + return _artist; + } + } + private string _title; + public string Title + { + get + { + if (_title == null) + { + if (!string.IsNullOrEmpty(TitleRoman)) + _title = TitleRoman; + else if (!string.IsNullOrEmpty(TitleUnicode)) + _title = TitleUnicode; + else + _title = ""; + } + return _title; + } + } + + #region ICeBeatmapProps + public string Name { get { return this.ToString(); } } + + public bool DataDownloaded { get; set; } + public bool LocalBeatmapMissing { get; set; } + public bool LocalVersionDiffers { get; set; } + #endregion + } +} \ No newline at end of file diff --git a/CollectionManagerDll/DataTypes/Beatmaps.cs b/CollectionManagerDll/DataTypes/Beatmaps.cs new file mode 100644 index 0000000..6b90fb6 --- /dev/null +++ b/CollectionManagerDll/DataTypes/Beatmaps.cs @@ -0,0 +1,6 @@ +namespace CollectionManager.DataTypes +{ + public class Beatmaps :RangeObservableCollection + { + } +} \ No newline at end of file diff --git a/CollectionManagerDll/DataTypes/Collection.cs b/CollectionManagerDll/DataTypes/Collection.cs new file mode 100644 index 0000000..b6b7cae --- /dev/null +++ b/CollectionManagerDll/DataTypes/Collection.cs @@ -0,0 +1,206 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using CollectionManager.Annotations; +using CollectionManager.Exceptions; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManager.DataTypes +{ + public class Collection : IEnumerable + { + private MapCacher LoadedMaps = null; + + /// + /// Contains all beatmap hashes contained in this collection + /// + private HashSet _beatmapHashes = new HashSet(); + /// + /// Contains beatmaps that did not find a match in LoadedMaps + /// nor had additional data(MapSetId) + /// + public Beatmaps UnknownBeatmaps { get; } = new Beatmaps(); + /// + /// Contains beatmaps that did not find a match in LoadedMaps + /// but contain enough information(MapSetId) to be able to issue new download + /// + /// .osdb files contain this data since v2 + public Beatmaps DownloadableBeatmaps { get; }= new Beatmaps(); + /// + /// Contains beatmap with data from LoadedMaps + /// + public Beatmaps KnownBeatmaps { get; }= new Beatmaps(); + + + + /// + /// Total number of beatmaps contained in this collection + /// + public int NumberOfBeatmaps { get { return UnknownBeatmaps.Count + KnownBeatmaps.Count + DownloadableBeatmaps.Count; } } + + public int NumberOfMissingBeatmaps { get { return UnknownBeatmaps.Count + DownloadableBeatmaps.Count; } } + /// + /// Username of last person editing this collection + /// + public string LastEditorUsername { get; set; } + + public void SetLoadedMaps(MapCacher instance) + { + if (instance == null) + throw new BeatmapCacherNotInitalizedException(); + if (LoadedMaps != null) + LoadedMaps.BeatmapsModified -= LoadedMaps_BeatmapsModified; + + LoadedMaps = instance; + LoadedMaps.BeatmapsModified += LoadedMaps_BeatmapsModified; + ReprocessBeatmaps(); + } + + private void LoadedMaps_BeatmapsModified(object sender, System.EventArgs e) + { + ReprocessBeatmaps(); + } + + private void ReprocessBeatmaps() + { + if (_beatmapHashes.Count <= 0) + return; + + var tempBeatmaps = new Beatmaps(); + tempBeatmaps.AddRange(this.AllBeatmaps()); + UnknownBeatmaps.Clear(); + KnownBeatmaps.Clear(); + DownloadableBeatmaps.Clear(); + + + foreach (var beatmap in tempBeatmaps) + { + ProcessNewlyAddedMap(beatmap); + } + + } + + + public IEnumerable AllBeatmaps() + { + for (int i = 0; i < this.KnownBeatmaps.Count; i++) + { + yield return this.KnownBeatmaps[i]; + } + foreach (var beatmap in NotKnownBeatmaps()) + { + yield return beatmap; + } + + } + public IEnumerable NotKnownBeatmaps() + { + for (int i = 0; i < this.DownloadableBeatmaps.Count; i++) + { + yield return this.DownloadableBeatmaps[i]; + } + for (int i = 0; i < this.UnknownBeatmaps.Count; i++) + { + yield return this.UnknownBeatmaps[i]; + } + } + + public string Name { get; set; } = "."; + + public Collection(MapCacher instance) + { + SetLoadedMaps(instance); + } + public void AddBeatmap(Beatmap map) + { + var exMap = new BeatmapExtension(); + exMap.CloneValues(map); + AddBeatmap(exMap); + } + + + public void AddBeatmap(BeatmapExtension map) + { + if (_beatmapHashes.Contains(map.Md5)) + return; + _beatmapHashes.Add(map.Md5); + ProcessNewlyAddedMap(map); + } + + + public void AddBeatmapByHash(string hash) + { + if (_beatmapHashes.Contains(hash)) + return; + this.AddBeatmap(new BeatmapExtension() { Md5 = hash }); + } + + private void ProcessNewlyAddedMap(BeatmapExtension map) + { + lock (LoadedMaps.LockingObject) + { + if (LoadedMaps.Beatmaps.Count == 0) + { + UnknownBeatmaps.Add(map); + return; + } + if (LoadedMaps.LoadedBeatmapsMd5Dict.ContainsKey(map.Md5)) + { + KnownBeatmaps.Add(LoadedMaps.LoadedBeatmapsMd5Dict[map.Md5]); + return; + } + if (map.MapId > 10 && LoadedMaps.LoadedBeatmapsMapIdDict.ContainsKey(map.MapId)) + { + KnownBeatmaps.Add(LoadedMaps.LoadedBeatmapsMapIdDict[map.MapId]); + LoadedMaps.LoadedBeatmapsMapIdDict[map.MapId].LocalVersionDiffers = true; + return; + } + if (map.MapSetId != 0) + { + DownloadableBeatmaps.Add(map); + + } + else + { + UnknownBeatmaps.Add(map); + } + + map.LocalBeatmapMissing = true; + } + } + + public void RemoveBeatmap(string hash) + { + if (_beatmapHashes.Contains(hash)) + { + bool removed = false; + for (int i = 0; i < KnownBeatmaps.Count; i++) + { + if (KnownBeatmaps[i].Md5 == hash) + { + KnownBeatmaps.RemoveAt(i); + removed = true; + break; + } + } + if (!removed) + for (int i = 0; i < UnknownBeatmaps.Count; i++) + { + if (UnknownBeatmaps[i].Md5 == hash) + { + UnknownBeatmaps.RemoveAt(i); + removed = true; + break; + } + } + _beatmapHashes.Remove(hash); + } + + } + public IEnumerator GetEnumerator() + { + return this.AllBeatmaps().GetEnumerator(); + } + + } +} \ No newline at end of file diff --git a/CollectionManagerDll/DataTypes/Collections.cs b/CollectionManagerDll/DataTypes/Collections.cs new file mode 100644 index 0000000..d103716 --- /dev/null +++ b/CollectionManagerDll/DataTypes/Collections.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace CollectionManager.DataTypes +{ + /// + /// Contents of this collection should be only modified by CollectionManager. + /// Edits outside of it are not supported and things may break. + /// + public class Collections :RangeObservableCollection + { + public IEnumerable AllBeatmaps() + { + for (int i = 0; i < this.Count; i++) + { + foreach (var beatmap in this[i].AllBeatmaps()) + { + yield return beatmap; + } + } + } + + public override event NotifyCollectionChangedEventHandler CollectionChanged + { + add { base.CollectionChanged += value; } + remove { base.CollectionChanged -= value; } + } + + protected override event PropertyChangedEventHandler PropertyChanged + { + add { base.PropertyChanged += value; } + remove { base.PropertyChanged -= value; } + } + + } +} \ No newline at end of file diff --git a/CollectionManagerDll/DataTypes/RangeObservableCollection.cs b/CollectionManagerDll/DataTypes/RangeObservableCollection.cs new file mode 100644 index 0000000..fbfca6c --- /dev/null +++ b/CollectionManagerDll/DataTypes/RangeObservableCollection.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace CollectionManager.DataTypes +{ + public class RangeObservableCollection : ObservableCollection + { + private bool _suppressNotification = false; + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (!_suppressNotification) + { + base.OnCollectionChanged(e); + } + } + + public void SilentRemove(T item) + { + _suppressNotification = true; + Remove(item); + _suppressNotification = false; + } + + public void SilentAdd(T item) + { + _suppressNotification = true; + Add(item); + _suppressNotification = false; + } + + public void CallReset() + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + public void SuspendEvents(bool suspend = true) + { + _suppressNotification = suspend; + if (!suspend) + CallReset(); + } + public void AddRange(IEnumerable list) + { + if (list == null) + throw new ArgumentNullException("list"); + + _suppressNotification = true; + + foreach (T item in list) + { + Add(item); + } + _suppressNotification = false; + //OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + } +} diff --git a/CollectionManagerDll/Enums/CollectionEdit.cs b/CollectionManagerDll/Enums/CollectionEdit.cs new file mode 100644 index 0000000..c3ab6b0 --- /dev/null +++ b/CollectionManagerDll/Enums/CollectionEdit.cs @@ -0,0 +1,13 @@ +namespace CollectionManager.Enums +{ + public enum CollectionEdit + { + Add=0, + Remove=1, + Rename=2, + Merge=3, + Clear=4, + AddBeatmaps=5, + RemoveBeatmaps=6, + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Enums/PlayModes.cs b/CollectionManagerDll/Enums/PlayModes.cs new file mode 100644 index 0000000..f44e7af --- /dev/null +++ b/CollectionManagerDll/Enums/PlayModes.cs @@ -0,0 +1,10 @@ +namespace CollectionManager.Enums +{ + public enum PlayModes + { + Osu = 0, + Taiko = 1, + CatchTheBeat = 2, + OsuMania = 3 + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Example.cs b/CollectionManagerDll/Example.cs new file mode 100644 index 0000000..5eae720 --- /dev/null +++ b/CollectionManagerDll/Example.cs @@ -0,0 +1,114 @@ +using System; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using CollectionManager.Modules.FileIO; + +namespace CollectionManager +{ + internal class Class1 + { + internal void Run() + { + var osuFileIo = new OsuFileIo(); + + //Automatic Detection of osu! directory location + string dir = osuFileIo.OsuPathResolver.GetOsuDir(ThisPathIsCorrect, SelectDirectoryDialog); + + + + string osuPath = @"E:\osu!\"; + string osuDbFileName = "osu!.db"; + string ExampleCollectionFileLocation = @"E:\osuCollections\SomeCollectionThatExists.db"; + + + + + osuFileIo.OsuDatabase.Load(osuPath+osuDbFileName); + + //osu! configuration file is currently only used for getting a songs folder location + osuFileIo.OsuSettings.Load(osuPath); + + //Data loaded using next 2 functions are going to be automatically correlated + //with currently avalable beatmap data. + osuFileIo.CollectionLoader.LoadOsuCollection(ExampleCollectionFileLocation); + //osuFileIo.CollectionLoader.LoadOsdbCollection(""); + + + string osuSongsFolderLocation = osuFileIo.OsuSettings.CustomBeatmapDirectoryLocation; + + //Create Collection manager instance + var collectionManager = new CollectionsManagerWithCounts(osuFileIo.OsuDatabase.LoadedMaps.Beatmaps); + //or just this: + //var collectionManager = new CollectionsManager(osuFileIo.OsuDatabase.LoadedMaps.Beatmaps); + + collectionManager.LoadedCollections.CollectionChanged += LoadedCollections_CollectionChanged; + + //Create some dummy collections + Collection ourCollection = new Collection(osuFileIo.LoadedMaps) { Name = "Example collection1" }; + Collection ourSecondCollection = new Collection(osuFileIo.LoadedMaps) { Name = "Example collection2" }; + + //Add these to our manager + collectionManager.EditCollection( + CollectionEditArgs.AddCollections(new Collections() { ourCollection, ourSecondCollection }) + ); + + //Add some beatmaps to ourSecondCollection + collectionManager.EditCollection( + CollectionEditArgs.AddBeatmaps("Example collection2",new Beatmaps() + { + new BeatmapExtension() {Md5 = "'known' md5"}, + new BeatmapExtension() {Md5 = "another 'known' md5"}, + new BeatmapExtension() {Md5 = "md5 that we have no idea about"} + })); + + //Trying to issue any action on collection with unknown name will be just ignored + collectionManager.EditCollection( + CollectionEditArgs.AddBeatmaps("Collection that doesn't exist", new Beatmaps() + { + new BeatmapExtension() {Md5 = "1234567890,yes I know these aren't valid md5s"} + })); + + //Merge our collections into one. + //Note that while I do not impose a limit on collection name length, osu! does and it will be truncated upon opening collection in osu! client. + collectionManager.EditCollection( + CollectionEditArgs.MergeCollections(new Collections(){ourCollection,ourSecondCollection} + , "Collection created from 2 Example collections") + ); + + //true + bool isNameTaken = collectionManager.CollectionNameExists("Collection created from 2 Example collections"); + + Collection ourMergedCollection = collectionManager.GetCollectionByName("Collection created from 2 Example collections"); + + //These are avaliable only when using CollectionsManagerWithCounts + var TotalBeatmapCount = collectionManager.BeatmapsInCollectionsCount; + var MissingBeatmapsCount = collectionManager.MissingBeatmapCount; + + //Lets save our collections after edits + //as .osdb + osuFileIo.CollectionLoader.SaveOsdbCollection(collectionManager.LoadedCollections, ExampleCollectionFileLocation); + //or .db + osuFileIo.CollectionLoader.SaveOsuCollection(collectionManager.LoadedCollections, ExampleCollectionFileLocation); + } + + private string SelectDirectoryDialog(string s) + { + //If result of ThisPathIsCorrect was false this would be executed + //ask user to provide path themself? + return string.Empty; + } + + private bool ThisPathIsCorrect(string s) + { + //check with user that path provided in s is correct? + return true; + } + + private void LoadedCollections_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + //This gets called once per every collectionManager.EditCollection() call + + //Do some things + } + } +} diff --git a/CollectionManagerDll/Exceptions/BeatmapCacherNotInitalizedException.cs b/CollectionManagerDll/Exceptions/BeatmapCacherNotInitalizedException.cs new file mode 100644 index 0000000..5297809 --- /dev/null +++ b/CollectionManagerDll/Exceptions/BeatmapCacherNotInitalizedException.cs @@ -0,0 +1,9 @@ +using System; + +namespace CollectionManager.Exceptions +{ + [Serializable] + class BeatmapCacherNotInitalizedException :Exception + { + } +} diff --git a/CollectionManagerDll/Exceptions/NotOsuDirectoryException.cs b/CollectionManagerDll/Exceptions/NotOsuDirectoryException.cs new file mode 100644 index 0000000..fe630c7 --- /dev/null +++ b/CollectionManagerDll/Exceptions/NotOsuDirectoryException.cs @@ -0,0 +1,10 @@ +using System; + +namespace CollectionManager.Exceptions +{ + [Serializable] + public class NotOsuDirectoryException:Exception + { + + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Interfaces/ICollectionEditor.cs b/CollectionManagerDll/Interfaces/ICollectionEditor.cs new file mode 100644 index 0000000..37c2e96 --- /dev/null +++ b/CollectionManagerDll/Interfaces/ICollectionEditor.cs @@ -0,0 +1,9 @@ +using CollectionManager.Modules.CollectionsManager; + +namespace App.Interfaces +{ + public interface ICollectionEditor + { + void EditCollection(CollectionEditArgs args); + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Interfaces/ICollectionNameValidator.cs b/CollectionManagerDll/Interfaces/ICollectionNameValidator.cs new file mode 100644 index 0000000..55ca8ef --- /dev/null +++ b/CollectionManagerDll/Interfaces/ICollectionNameValidator.cs @@ -0,0 +1,7 @@ +namespace CollectionManager.Interfaces +{ + public interface ICollectionNameValidator + { + bool IsCollectionNameValid(string name); + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Interfaces/ILogger.cs b/CollectionManagerDll/Interfaces/ILogger.cs new file mode 100644 index 0000000..51309ea --- /dev/null +++ b/CollectionManagerDll/Interfaces/ILogger.cs @@ -0,0 +1,7 @@ +namespace CollectionManager.Interfaces +{ + public interface ILogger + { + void Log(string logMessage, params string[] vals); + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Interfaces/IMapDataStorer.cs b/CollectionManagerDll/Interfaces/IMapDataStorer.cs new file mode 100644 index 0000000..d036ce8 --- /dev/null +++ b/CollectionManagerDll/Interfaces/IMapDataStorer.cs @@ -0,0 +1,11 @@ +using CollectionManager.DataTypes; + +namespace CollectionManager.Interfaces +{ + public interface IMapDataStorer + { + void StartMassStoring(); + void EndMassStoring(); + void StoreBeatmap(BeatmapExtension beatmap); + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/CollectionsManager/CollectionEditArgs.cs b/CollectionManagerDll/Modules/CollectionsManager/CollectionEditArgs.cs new file mode 100644 index 0000000..821a323 --- /dev/null +++ b/CollectionManagerDll/Modules/CollectionsManager/CollectionEditArgs.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using CollectionManager.DataTypes; +using CollectionManager.Enums; + +namespace CollectionManager.Modules.CollectionsManager +{ + public class CollectionEditArgs : EventArgs + { + public CollectionEdit Action { get; private set; } + public string OrginalName { get; private set; } + public string NewName { get; set; } + public Collections Collections { get; private set; } + public Beatmaps Beatmaps { get; private set; } + public IList CollectionNames { get; private set; } + public CollectionEditArgs(CollectionEdit action) + { + Action = action; + } + #region Add Collection + public static CollectionEditArgs AddCollections(Collections collections) + { + return new CollectionEditArgs(CollectionEdit.Add) + { + Collections = collections + }; + } + #endregion + #region Remove Collection + public static CollectionEditArgs RemoveCollections(IList collectionNames) + { + return new CollectionEditArgs(CollectionEdit.Remove) + { + CollectionNames = collectionNames + }; + } + public static CollectionEditArgs RemoveCollections(Collections collections) + { + var names = new List(); + foreach (var collection in collections) + { + names.Add(collection.Name); + } + return RemoveCollections(names); + } + #endregion + #region Rename Collection + public static CollectionEditArgs RenameCollection(string oldName, string NewName) + { + return new CollectionEditArgs(CollectionEdit.Rename) + { + OrginalName = oldName, + NewName = NewName + }; + } + public static CollectionEditArgs RenameCollection(Collection collection, string newName) + { + return RenameCollection(collection.Name, newName); + } + #endregion + #region merge Collections + public static CollectionEditArgs MergeCollections(Collections collections, string newName) + { + return new CollectionEditArgs(CollectionEdit.Merge) + { + Collections = collections, + NewName = newName + }; + } + #endregion + #region Clear Collections + public static CollectionEditArgs ClearCollections() + { + return new CollectionEditArgs(CollectionEdit.Clear); + } + #endregion + #region Add Beatmaps to collection + public static CollectionEditArgs AddBeatmaps(string collectionName,Beatmaps beatmaps) + { + return new CollectionEditArgs(CollectionEdit.AddBeatmaps) + { + Beatmaps = beatmaps, + OrginalName = collectionName + }; + } + + #endregion + #region Remove beatmaps from collection + public static CollectionEditArgs RemoveBeatmaps(string collectionName, Beatmaps beatmaps) + { + return new CollectionEditArgs(CollectionEdit.RemoveBeatmaps) + { + Beatmaps = beatmaps, + OrginalName = collectionName + }; + } + #endregion + + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/CollectionsManager/CollectionsManager.cs b/CollectionManagerDll/Modules/CollectionsManager/CollectionsManager.cs new file mode 100644 index 0000000..933f395 --- /dev/null +++ b/CollectionManagerDll/Modules/CollectionsManager/CollectionsManager.cs @@ -0,0 +1,151 @@ +using App.Interfaces; +using CollectionManager.DataTypes; +using CollectionManager.Enums; +using CollectionManager.Interfaces; + +namespace CollectionManager.Modules.CollectionsManager +{ + public class CollectionsManager : ICollectionEditor, ICollectionNameValidator + { + public readonly Collections LoadedCollections = new Collections(); + public Beatmaps LoadedBeatmaps; + public CollectionsManager(Beatmaps loadedBeatmaps) + { + LoadedBeatmaps = loadedBeatmaps; + } + /* + add collection + remove collection + edit collection name + merge x collections + clear collections + add beatmaps to collection + remove beatmaps from collection + */ + + private void EditCollection(CollectionEditArgs args, bool suspendRefresh = false) + { + var action = args.Action; + if (action == CollectionEdit.Add) + { + foreach (var collection in args.Collections) + { + collection.Name = GetValidCollectionName(collection.Name); + } + LoadedCollections.AddRange(args.Collections); + } + else if (action == CollectionEdit.Remove) + { + foreach (var collectionName in args.CollectionNames) + { + var collection = GetCollectionByName(collectionName); + if (collection != null) + LoadedCollections.SilentRemove(collection); + } + } + else if (action == CollectionEdit.Merge) + { + var collections = args.Collections; + var newCollectionName = args.NewName; + if (collections.Count > 0) + { + var masterCollection = collections[0]; + for (int i = 1; i < collections.Count; i++) + { + var collectionToMerge = collections[i]; + foreach (var beatmap in collectionToMerge.AllBeatmaps()) + { + masterCollection.AddBeatmap(beatmap); + } + LoadedCollections.SilentRemove(collectionToMerge); + } + LoadedCollections.SilentRemove(masterCollection); + + masterCollection.Name = GetValidCollectionName(newCollectionName); + EditCollection(CollectionEditArgs.AddCollections(new Collections() { masterCollection }), true); + } + } + else if (action == CollectionEdit.Clear) + { + LoadedCollections.Clear(); + } + else + { + var collection = GetCollectionByName(args.OrginalName); + + if (action == CollectionEdit.Rename) + { + collection.Name = GetValidCollectionName(args.NewName); + } + else if (action == CollectionEdit.AddBeatmaps) + { + if (collection != null) + { + foreach (var beatmap in args.Beatmaps) + { + collection.AddBeatmap(beatmap); + } + } + } + else if (action == CollectionEdit.RemoveBeatmaps) + { + if (collection != null) + { + foreach (var beatmap in args.Beatmaps) + { + collection.RemoveBeatmap(beatmap.Md5); + } + } + } + } + if (!suspendRefresh) + AfterCollectionsEdit(); + } + public void EditCollection(CollectionEditArgs args) + { + EditCollection(args, false); + } + + protected virtual void AfterCollectionsEdit() + { + LoadedCollections.CallReset(); + } + public Collection GetCollectionByName(string collectionName) + { + for (int i = 0; i < LoadedCollections.Count; i++) + { + if (LoadedCollections[i].Name == collectionName) + { + return LoadedCollections[i]; + } + } + return null; + } + + private string GetValidCollectionName(string desiredName) + { + var newName = desiredName; + int c = 0; + while (CollectionNameExists(newName)) + { + newName = desiredName + "_" + c++; + } + return newName; + } + + public bool CollectionNameExists(string name) + { + foreach (var collection in LoadedCollections) + { + if (collection.Name == name) + return true; + } + return false; + } + + public bool IsCollectionNameValid(string name) + { + return !CollectionNameExists(name); + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/CollectionsManager/CollectionsManagerWithCounts.cs b/CollectionManagerDll/Modules/CollectionsManager/CollectionsManagerWithCounts.cs new file mode 100644 index 0000000..8e57fcf --- /dev/null +++ b/CollectionManagerDll/Modules/CollectionsManager/CollectionsManagerWithCounts.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using CollectionManager.DataTypes; + +namespace CollectionManager.Modules.CollectionsManager +{ + public class CollectionsManagerWithCounts : CollectionsManager + { + private readonly HashSet _missingBeatmapHashes = new HashSet(); + private readonly HashSet _downloadableMapSetIds = new HashSet(); + public int MissingBeatmapCount => _missingBeatmapHashes.Count + DownloadableBeatmapsCount; + public int DownloadableBeatmapsCount => _downloadableMapSetIds.Count; + public int BeatmapsInCollectionsCount { get; private set; } + public int CollectionsCount => LoadedCollections.Count; + + public CollectionsManagerWithCounts(Beatmaps loadedBeatmaps) : base(loadedBeatmaps) + { + } + + + protected override void AfterCollectionsEdit() + { + var beatmapCount = 0; + _missingBeatmapHashes.Clear(); + _downloadableMapSetIds.Clear(); + foreach (var collection in LoadedCollections) + { + foreach (var partialBeatmap in collection.UnknownBeatmaps) + { + if (!_missingBeatmapHashes.Contains(partialBeatmap.Md5)) + _missingBeatmapHashes.Add(partialBeatmap.Md5); + } + foreach (var map in collection.DownloadableBeatmaps) + { + if (!_downloadableMapSetIds.Contains(map.MapSetId)) + _downloadableMapSetIds.Add(map.MapSetId); + } + beatmapCount += collection.NumberOfBeatmaps; + } + BeatmapsInCollectionsCount = beatmapCount; + + base.AfterCollectionsEdit(); + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/FileIO/FileCollections/CollectionLoader.cs b/CollectionManagerDll/Modules/FileIO/FileCollections/CollectionLoader.cs new file mode 100644 index 0000000..796c07c --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/FileCollections/CollectionLoader.cs @@ -0,0 +1,67 @@ +using System.IO; +using CollectionManager.DataTypes; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManager.Modules.FileIO.FileCollections +{ + public class CollectionLoader + { + private readonly MapCacher _mapCacher; + private OsdbCollectionHandler OsdbCollectionHandler = new OsdbCollectionHandler(null); + private OsuCollectionHandler OsuCollectionHandler = new OsuCollectionHandler(null); + + public CollectionLoader(MapCacher mapCacher) + { + _mapCacher = mapCacher; + } + + public Collections LoadOsuCollection(string fileLocation) + { + return OsuCollectionHandler.LoadCollections(fileLocation, _mapCacher); + } + + public Collections LoadOsdbCollections(string fileLocation) + { + return OsdbCollectionHandler.ReadOsdb(fileLocation, _mapCacher); + } + + public void SaveOsuCollection(Collections collections, string saveLocation) + { + OsuCollectionHandler.SaveCollections(collections, saveLocation); + } + public void SaveOsdbCollection(Collections collections, string saveLocation, string editorUsername = "N/A") + { + OsdbCollectionHandler.WriteOsdb(collections, saveLocation, editorUsername); + } + + public Collections LoadCollection(string fileLocation) + { + var ext = Path.GetExtension(fileLocation); + switch (ext.ToLower()) + { + case ".db": + return LoadOsuCollection(fileLocation); + case ".osdb": + return LoadOsdbCollections(fileLocation); + default: + return null; + } + } + + public void SaveCollection(Collections collections,string fileLocation) + { + var ext = Path.GetExtension(fileLocation); + switch (ext.ToLower()) + { + case ".db": + SaveOsuCollection(collections,fileLocation); + break; + case ".osdb": + SaveOsdbCollection(collections,fileLocation); + break; + default: + return; + } + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/FileIO/FileCollections/OsdbCollectionHandler.cs b/CollectionManagerDll/Modules/FileIO/FileCollections/OsdbCollectionHandler.cs new file mode 100644 index 0000000..0a8593f --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/FileCollections/OsdbCollectionHandler.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CollectionManager.DataTypes; +using CollectionManager.Interfaces; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManager.Modules.FileIO.FileCollections +{ + public class OsdbCollectionHandler + { + //TODO: Make lastEditor per-collection variable, not file-wide. + //TODO: Better way to do .osdb versioning while allowing all versions to be loaded/saved + + private BinaryReader _binReader; + private BinaryWriter _binWriter; + private FileStream _fileStream; + private ILogger _logger; + private MemoryStream _memStream; + public OsdbCollectionHandler(ILogger logger) + { + _logger = logger; + } + + protected virtual void Error(string message) + { + //Helpers.Error(message); + } + protected virtual void Info(string message) + { + //Helpers.Info(message); + } + public void WriteOsdb(Collections collections, string fullFileDir, string editor, bool skipMissing = false) + { + OpenFile(fullFileDir, true); + //header + _binWriter.Write("o!dm3"); + //save date + _binWriter.Write(DateTime.Now.ToOADate()); + //who saved given osdb + _binWriter.Write(editor); + //number of collections stored in osdb + _binWriter.Write(collections.Count); + //bool ignoreMissingMaps = false; + foreach (var collection in collections) + { + List beatmapsPossibleToSave = new List(); + HashSet beatmapWithHashOnly = new HashSet(); + + foreach (var beatmap in collection.KnownBeatmaps) + { + beatmapsPossibleToSave.Add(beatmap); + } + foreach (var beatmap in collection.DownloadableBeatmaps) + { + beatmapsPossibleToSave.Add(beatmap); + } + + foreach (var partialBeatmap in collection.UnknownBeatmaps) + { + if (partialBeatmap.TitleRoman != "" || partialBeatmap.MapSetId > 0) + { + beatmapsPossibleToSave.Add(partialBeatmap); + } + else + { + beatmapWithHashOnly.Add(partialBeatmap.Md5); + } + } + _binWriter.Write(collection.Name); + _binWriter.Write(beatmapsPossibleToSave.Count); + //Save beatmaps + foreach (var beatmap in beatmapsPossibleToSave) + { + _binWriter.Write(beatmap.MapId); + _binWriter.Write(beatmap.MapSetId); + _binWriter.Write(beatmap.ArtistRoman); + _binWriter.Write(beatmap.TitleRoman); + _binWriter.Write(beatmap.DiffName); + _binWriter.Write(beatmap.Md5); + } + _binWriter.Write(beatmapWithHashOnly.Count); + foreach (var beatmapHash in beatmapWithHashOnly) + { + _binWriter.Write(beatmapHash); + } + + } + + _binWriter.Write("By Piotrekol"); + CloseFile(true); + } + + public Collections ReadOsdb(string fullFileDir,MapCacher mapCacher) + { + int fileVersion = -1; + DateTime fileDate = DateTime.Now; + var collections = new Collections(); + OpenFile(fullFileDir, false); + _binReader.BaseStream.Seek(0, SeekOrigin.Begin); + string versionString = _binReader.ReadString(); + //check header + switch (versionString) + { + case "o!dm": + fileVersion = 1; + break; + + case "o!dm2": + fileVersion = 2; + break; + + case "o!dm3": + fileVersion = 3; + break; + default: + Error("Something went wrong while loading this file"); + break; + } + if (fileVersion != -1) + { + _logger?.Log("Starting file load"); + fileDate = DateTime.FromOADate(_binReader.ReadDouble()); + _logger?.Log(">Date: " + fileDate); + string lastEditor = _binReader.ReadString(); + _logger?.Log(">LastEditor: " + lastEditor); + int numberOfCollections = _binReader.ReadInt32(); + _logger?.Log(">Collections: " + numberOfCollections); + for (int i = 0; i < numberOfCollections; i++) + { + var name = _binReader.ReadString(); + var numberOfBeatmaps = _binReader.ReadInt32(); + _logger?.Log(">Number of maps in collection {0}: {1} named:{2}", i.ToString(), numberOfBeatmaps.ToString(), name); + var collection = new Collection(mapCacher) { Name = name, LastEditorUsername = lastEditor}; + for (int j = 0; j < numberOfBeatmaps; j++) + { + var map = new BeatmapExtension(); + map.MapId = _binReader.ReadInt32(); + if (fileVersion >= 2) + map.MapSetId = _binReader.ReadInt32(); + map.ArtistRoman = _binReader.ReadString(); + map.TitleRoman = _binReader.ReadString(); + map.DiffName = _binReader.ReadString(); + map.Md5 = _binReader.ReadString(); + collection.AddBeatmap(map); + } + + if (fileVersion >= 3) + { + var numberOfMapHashes = _binReader.ReadInt32(); + for (int j = 0; j < numberOfMapHashes; j++) + { + string hash = _binReader.ReadString(); + collection.AddBeatmapByHash(hash); + } + } + + + collections.Add(collection); + } + } + if (_binReader.ReadString() != "By Piotrekol") + { + Error("Something went wrong while loading this file"); + //collections = new List(); + } + + + CloseFile(false); + + + collections = IssuseVersionRelevantProcedures(fileVersion, fileDate, collections); + + return collections; + } + + private Collections IssuseVersionRelevantProcedures(int fileVersion, DateTime fileDate, Collections collections) + { + if (fileVersion != -1) + { + if (fileVersion < 3) + { + Info("This collection was generated using an older version of Collection Manager." + Environment.NewLine + + "All download links in this collection will not work." + Environment.NewLine + + "File version: " + fileVersion + Environment.NewLine + + "Date: " + fileDate); + } + } + return collections; + } + + + private void OpenFile(string fileDir, bool forWriting = false) + { + if (forWriting) + { + _fileStream = new FileStream(fileDir, FileMode.Create, FileAccess.ReadWrite); + _binWriter = new BinaryWriter(_fileStream); + + } + else + { + _fileStream = new FileStream(fileDir, FileMode.Open, FileAccess.Read); + _memStream = new MemoryStream(); + _fileStream.CopyTo(_memStream); + _fileStream.Close(); + _binReader = new BinaryReader(_memStream); + + } + } + + private void CloseFile(bool forWriting = false) + { + try + { + if (forWriting) + { + _binWriter.Close(); + } + else + { + _binReader.Close(); + } + } + catch { } + } + } +} diff --git a/CollectionManagerDll/Modules/FileIO/FileCollections/OsuCollectionHandler.cs b/CollectionManagerDll/Modules/FileIO/FileCollections/OsuCollectionHandler.cs new file mode 100644 index 0000000..17f9795 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/FileCollections/OsuCollectionHandler.cs @@ -0,0 +1,131 @@ +using System.IO; +using CollectionManager.DataTypes; +using CollectionManager.Interfaces; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManager.Modules.FileIO.FileCollections +{ + public class OsuCollectionHandler + { + private BinaryReader BinReader; + private BinaryWriter BinWriter; + private FileStream FileStream; + + private ILogger logger; + public int LastfileDate; + + public OsuCollectionHandler(ILogger logger) + { + this.logger = logger; + } + public int readInt32() + { + return this.BinReader.ReadInt32(); + } + public string readString() + { + try + { + if (this.BinReader.ReadByte() == 11) + { + return BinReader.ReadString(); + } + return ""; + } + catch { throw new System.IO.IOException("This isn't valid osu! collection!"); } + } + + private void openFile(string fileDir, bool forWriting = false) + { + if (forWriting) + { + this.FileStream = new FileStream(fileDir, FileMode.Create, FileAccess.ReadWrite); + this.BinWriter = new BinaryWriter(this.FileStream); + + } + else + { + this.FileStream = new FileStream(fileDir, FileMode.Open, FileAccess.Read); + this.BinReader = new BinaryReader(this.FileStream); + + } + } + + private void closeFile(bool forWriting = false) + { + try + { + if (forWriting) + this.BinWriter.Close(); + else + this.BinReader.Close(); + } + catch { } + } + public Collections LoadCollections(string fullFileDir, MapCacher mapCacher) + { + openFile(fullFileDir); + + + LastfileDate = readInt32(); + int numOfCollections = readInt32(); + logger?.Log("Reading " + numOfCollections + " Collections"); + Collections loadedCollections = new Collections(); + + try + { + for (int i = 1; i <= numOfCollections; i++) + { + var collectionName = readString(); + var numberOfDiffs = readInt32(); + + var collection = new Collection(mapCacher) { Name = collectionName }; + var j = 0; + for (j = 0; j < numberOfDiffs; j++)//j=1 <= + { + var md5 = readString(); + collection.AddBeatmapByHash(md5); + } + loadedCollections.Add(collection); + logger?.Log(">Number of maps in collection {0}: {1} named:{2}", i.ToString(), numberOfDiffs.ToString(), collection.Name); + } + } + catch (System.IO.IOException) { throw new System.IO.IOException("This isn't valid osu! collection!"); } + closeFile(); + return loadedCollections; + } + + public void SaveCollections(Collections collections, string fullFileDir) + { + openFile(fullFileDir, true); + + //last edit time + //BinWriter.Write((int)DateTime.Now.Ticks); + BinWriter.Write((int)this.LastfileDate); + //collection count + BinWriter.Write(collections.Count); + + /*collections + * repeated: + * 0x0b + * (string) collection name + * (int) number of beatmaps in collection + * repeated: + * 0x0b + * (string) beatmap hash + */ + foreach (var collection in collections) + { + BinWriter.Write((byte)0x0b); + BinWriter.Write(collection.Name); + BinWriter.Write(collection.NumberOfBeatmaps); + foreach (var beatmap in collection.AllBeatmaps()) + { + BinWriter.Write((byte)0x0b); + BinWriter.Write(beatmap.Md5); + } + } + closeFile(true); + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/FileIO/OsuDb/LOsuDatabaseLoader.cs b/CollectionManagerDll/Modules/FileIO/OsuDb/LOsuDatabaseLoader.cs new file mode 100644 index 0000000..3bbf8d7 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuDb/LOsuDatabaseLoader.cs @@ -0,0 +1,43 @@ +using System; +using CollectionManager.Interfaces; + +namespace CollectionManager.Modules.FileIO.OsuDb +{ + public class LOsuDatabaseLoader : OsuDatabaseLoader + { + private ILogger logger; + public string status { get; private set; } + public LOsuDatabaseLoader(ILogger logger, IMapDataStorer mapDataStorer) : base(logger, mapDataStorer) + { + } + + protected override bool FileExists(string fullPath) + { + var result = base.FileExists(fullPath); + if (!result) + logger?.Log($"File \"{fullPath}\" doesn't exist!"); + return result; + } + + public override void LoadDatabase(string fullOsuDbPath) + { + status = "Loading database"; + base.LoadDatabase(fullOsuDbPath); + status = $"Loaded {NumberOfLoadedBeatmaps} beatmaps"; + } + protected override void ReadDatabaseEntries() + { + try + { + base.ReadDatabaseEntries(); + } + catch (Exception e) + { + logger?.Log("Something went wrong while processing beatmaps(database is corrupt or its format changed)"); + logger?.Log("Try restarting your osu! first before reporting this problem."); + logger?.Log("Exception: {0},{1}", e.Message, e.StackTrace); + status = "Failed with exception " + $"Exception: {e.Message},{e.StackTrace}"; + } + } + } +} diff --git a/CollectionManagerDll/Modules/FileIO/OsuDb/MapCacher.cs b/CollectionManagerDll/Modules/FileIO/OsuDb/MapCacher.cs new file mode 100644 index 0000000..4c79ff1 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuDb/MapCacher.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using CollectionManager.DataTypes; +using CollectionManager.Interfaces; + +namespace CollectionManager.Modules.FileIO.OsuDb +{ + public class MapCacher : IMapDataStorer + { + public readonly object LockingObject = new object(); + public readonly Beatmaps Beatmaps = new Beatmaps(); + public HashSet BeatmapHashes = new HashSet(); + public Dictionary LoadedBeatmapsMd5Dict = new Dictionary(); + public Dictionary LoadedBeatmapsMapIdDict = new Dictionary(); + public event EventHandler BeatmapsModified; + private bool _massStoring = false; + public MapCacher() + { + + } + + public void UnloadBeatmaps() + { + Beatmaps.Clear(); + BeatmapHashes.Clear(); + EndMassStoring(); + } + private void UpdateLookupDicts(BeatmapExtension map, bool recalculate = false) + { + lock (LockingObject) + { + if (recalculate) + { + + LoadedBeatmapsMd5Dict.Clear(); + LoadedBeatmapsMapIdDict.Clear(); + foreach (var beatmap in Beatmaps) + { + UpdateLookupDicts(beatmap); + } + return; + + } + + if (!LoadedBeatmapsMd5Dict.ContainsKey(map.Md5)) + LoadedBeatmapsMd5Dict.Add(map.Md5, map); + + if (!LoadedBeatmapsMapIdDict.ContainsKey(map.MapId)) + LoadedBeatmapsMapIdDict.Add(map.MapId, map); + } + } + public void StartMassStoring() + { + _massStoring = true; + } + + public void EndMassStoring() + { + _massStoring = false; + UpdateLookupDicts(null, true); + OnBeatmapsModified(); + } + + public void StoreBeatmap(BeatmapExtension beatmap) + { + if (!BeatmapHashes.Contains(beatmap.Md5)) + { + this.BeatmapHashes.Add(beatmap.Md5); + this.Beatmaps.Add((BeatmapExtension)beatmap.Clone()); + if (!_massStoring) + { + UpdateLookupDicts(beatmap); + OnBeatmapsModified(); + } + } + } + + public bool BeatmapExistsInDatabase(string md5) + { + return BeatmapHashes.Contains(md5); + } + + private void OnBeatmapsModified() + { + BeatmapsModified?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabase.cs b/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabase.cs new file mode 100644 index 0000000..b954347 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabase.cs @@ -0,0 +1,28 @@ +namespace CollectionManager.Modules.FileIO.OsuDb +{ + public class OsuDatabase + { + public MapCacher LoadedMaps = new MapCacher(); + private readonly OsuDatabaseLoader _databaseLoader; + public string OsuFileLocation { get; private set; } + public string SongsFolderLocation { get; set; } + public int NumberOfBeatmapsWithoutId { get; set; } + + public bool DatabaseIsLoaded => _databaseLoader.LoadedSuccessfully; + public string Status => ((LOsuDatabaseLoader)_databaseLoader).status; + public int NumberOfBeatmaps => LoadedMaps.Beatmaps.Count; + public string Username => _databaseLoader.Username; + + public OsuDatabase() + { + _databaseLoader = new LOsuDatabaseLoader(null, LoadedMaps); + } + + public void Load(string fileDir) + { + OsuFileLocation = fileDir; + _databaseLoader.LoadDatabase(fileDir); + } + + } +} diff --git a/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabaseReader.cs b/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabaseReader.cs new file mode 100644 index 0000000..449f03f --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabaseReader.cs @@ -0,0 +1,373 @@ +#define GetStarsCombinations +using System; +using System.IO; +using CollectionManager.DataTypes; +using CollectionManager.Enums; +using CollectionManager.Interfaces; + +namespace CollectionManager.Modules.FileIO.OsuDb +{ + //TODO: refactor to allow for read/write access. + public class OsuDatabaseLoader + { + private readonly ILogger _logger; + private readonly IMapDataStorer _mapDataStorer; + private FileStream _fileStream; + private BinaryReader _binaryReader; + private Exception _exception; + private readonly BeatmapExtension _tempBeatmap = new BeatmapExtension(); + private bool _stopProcessing; + + public bool LoadedSuccessfully + { + get + { + if (ExpectedNumOfBeatmaps != -1) + return !_stopProcessing; + return false; + } + } + + public int MapsWithNoId { get; private set; } + public string Username { get; private set; } + public int FileDate { get; private set; } + public int ExpectedNumberOfMapSets { get; private set; } + public int ExpectedNumOfBeatmaps { get; private set; } = -1; + public int NumberOfLoadedBeatmaps { get; private set; } + + public OsuDatabaseLoader(ILogger logger, IMapDataStorer mapDataStorer) + { + _mapDataStorer = mapDataStorer; + _logger = logger; + } + + public virtual void LoadDatabase(string fullOsuDbPath) + { + if (FileExists(fullOsuDbPath)) + { + _fileStream = new FileStream(fullOsuDbPath, FileMode.Open, FileAccess.Read); + _binaryReader = new BinaryReader(_fileStream); + if (DatabaseContainsData()) + { + ReadDatabaseEntries(); + } + DestoryReader(); + } + GC.Collect(); + } + + protected virtual void ReadDatabaseEntries() + { + _mapDataStorer.StartMassStoring(); + for (NumberOfLoadedBeatmaps = 0; NumberOfLoadedBeatmaps < ExpectedNumOfBeatmaps; NumberOfLoadedBeatmaps++) + { + //TODO: check if it is safe to remove all try/catch _stopProcessing stuff + if (_stopProcessing) + { + throw _exception; + } + ReadNextBeatmap(); + } + _mapDataStorer.EndMassStoring(); + } + private void ReadNextBeatmap() + { + _tempBeatmap.InitEmptyValues(); + try + { + ReadMapHeader(_tempBeatmap); + ReadMapInfo(_tempBeatmap); + ReadTimingPoints(_tempBeatmap); + ReadMapMetaData(_tempBeatmap); + } + catch (Exception e) + { + _exception = e; + _stopProcessing = true; + return; + } + _mapDataStorer.StoreBeatmap(_tempBeatmap); + } + + private void ReadMapMetaData(Beatmap beatmap) + { + beatmap.MapId = Math.Abs(_binaryReader.ReadInt32()); + if (beatmap.MapId == 0) MapsWithNoId++; + + beatmap.MapSetId = Math.Abs(_binaryReader.ReadInt32()); + beatmap.ThreadId = Math.Abs(_binaryReader.ReadInt32()); + beatmap.MapRating = _binaryReader.ReadInt32(); + beatmap.Offset = _binaryReader.ReadInt16(); + beatmap.StackLeniency = _binaryReader.ReadSingle(); + beatmap.PlayMode = (PlayModes)_binaryReader.ReadByte(); + beatmap.Source = ReadString(); + beatmap.Tags = ReadString(); + beatmap.AudioOffset = _binaryReader.ReadInt16(); + beatmap.LetterBox = ReadString(); + beatmap.Played = !_binaryReader.ReadBoolean(); + beatmap.LastPlayed = GetDate(); + beatmap.IsOsz2 = _binaryReader.ReadBoolean(); + beatmap.Dir = ReadString(); + beatmap.LastSync = GetDate(); + beatmap.DisableHitsounds = _binaryReader.ReadBoolean(); + beatmap.DisableSkin = _binaryReader.ReadBoolean(); + beatmap.DisableSb = _binaryReader.ReadBoolean(); + _binaryReader.ReadBoolean(); + beatmap.BgDim = _binaryReader.ReadInt16(); + //bytes not analysed. + if (FileDate <= 20160403) + _binaryReader.BaseStream.Seek(4, SeekOrigin.Current); + else + _binaryReader.BaseStream.Seek(8, SeekOrigin.Current); + } + private void ReadTimingPoints(Beatmap beatmap) + { + int amountOfTimingPoints = _binaryReader.ReadInt32(); + double minBpm = double.MaxValue, + maxBpm = double.MinValue; + double bpmDelay, time; + bool InheritsBPM; + for (int i = 0; i < amountOfTimingPoints; i++) + { + bpmDelay = _binaryReader.ReadDouble(); + time = _binaryReader.ReadDouble(); + InheritsBPM = _binaryReader.ReadBoolean(); + if (InheritsBPM) + { + if (60000 / bpmDelay < minBpm) + minBpm = 60000 / bpmDelay; + if (60000 / bpmDelay > maxBpm) + maxBpm = 60000 / bpmDelay; + } + } + beatmap.MaxBpm = maxBpm; + beatmap.MinBpm = minBpm; + } + private void ReadMapInfo(Beatmap beatmap) + { + beatmap.State = _binaryReader.ReadByte(); + beatmap.Circles = _binaryReader.ReadInt16(); + beatmap.Sliders = _binaryReader.ReadInt16(); + beatmap.Spinners = _binaryReader.ReadInt16(); + beatmap.EditDate = GetDate(); + beatmap.ApproachRate = _binaryReader.ReadSingle(); + beatmap.CircleSize = _binaryReader.ReadSingle(); + beatmap.HpDrainRate = _binaryReader.ReadSingle(); + beatmap.OverallDifficulty = _binaryReader.ReadSingle(); + beatmap.SliderVelocity = _binaryReader.ReadDouble(); + + for (int j = 0; j < 4; j++) + { + ReadStarsData(beatmap); + } + beatmap.DrainingTime = _binaryReader.ReadInt32(); + beatmap.TotalTime = _binaryReader.ReadInt32(); + beatmap.PreviewTime = _binaryReader.ReadInt32(); + } + + private void ReadStarsData(Beatmap beatmap) + { + int num = _binaryReader.ReadInt32(); + if (num < 0) + { + return; + } + for (int j = 0; j < num; j++) + { + int modEnum = (int)ConditionalRead(); + Double stars = (Double)ConditionalRead(); + if (!beatmap.ModPpStars.ContainsKey(modEnum)) + { + beatmap.ModPpStars.Add(modEnum, Math.Round(stars, 2)); + } + else + { + var star = Math.Round(stars, 2); + if (beatmap.ModPpStars[modEnum] < star) + beatmap.ModPpStars[modEnum] = star; + } + + } + + } + private object ConditionalRead() + { + switch (_binaryReader.ReadByte()) + { + case 1: + { + return _binaryReader.ReadBoolean(); + } + case 2: + { + return _binaryReader.ReadByte(); + } + case 3: + { + return _binaryReader.ReadUInt16(); + } + case 4: + { + return _binaryReader.ReadUInt32(); + } + case 5: + { + return _binaryReader.ReadUInt64(); + } + case 6: + { + return _binaryReader.ReadSByte(); + } + case 7: + { + return _binaryReader.ReadInt16(); + } + case 8: + { + return _binaryReader.ReadInt32(); + } + case 9: + { + return _binaryReader.ReadInt64(); + } + case 10: + { + return _binaryReader.ReadChar(); + } + case 11: + { + return _binaryReader.ReadString(); + } + case 12: + { + return _binaryReader.ReadSingle(); + } + case 13: + { + return _binaryReader.ReadDouble(); + } + case 14: + { + return _binaryReader.ReadDecimal(); + } + case 15: + { + return GetDate(); + } + case 16: + { + int num = _binaryReader.ReadInt32(); + if (num > 0) + { + return _binaryReader.ReadBytes(num); + } + if (num < 0) + { + return null; + } + return new byte[0]; + + } + case 17: + { + int num = _binaryReader.ReadInt32(); + if (num > 0) + { + return _binaryReader.ReadChars(num); + } + if (num < 0) + { + return null; + } + return new char[0]; + } + case 18: + { + throw new NotImplementedException(); + } + default: + { + return null; + } + } + } + private DateTime GetDate() + { + long ticks = _binaryReader.ReadInt64(); + if (ticks < 0L) + { + return new DateTime(); + } + try + { + return new DateTime(ticks, DateTimeKind.Utc); + } + catch (Exception e) + { + _exception = e; + _stopProcessing = true; + return new DateTime(); + } + } + private void ReadMapHeader(Beatmap beatmap) + { + beatmap.ArtistRoman = ReadString().Trim(); + beatmap.ArtistUnicode = ReadString().Trim(); + beatmap.TitleRoman = ReadString().Trim(); + beatmap.TitleUnicode = ReadString().Trim(); + beatmap.Creator = ReadString().Trim(); + beatmap.DiffName = ReadString().Trim(); + beatmap.Mp3Name = ReadString().Trim(); + beatmap.Md5 = ReadString().Trim(); + beatmap.OsuFileName = ReadString().Trim(); + + } + private string ReadString() + { + try + { + if (_binaryReader.ReadByte() == 11) + { + return _binaryReader.ReadString(); + } + return ""; + } + catch { _stopProcessing = true; return ""; } + } + private bool DatabaseContainsData() + { + FileDate = _binaryReader.ReadInt32(); + ExpectedNumberOfMapSets = _binaryReader.ReadInt32(); + _logger?.Log(string.Format("Expected number of mapSets: {0}", ExpectedNumberOfMapSets)); + try + { + bool something = _binaryReader.ReadBoolean(); + DateTime a = GetDate().ToLocalTime(); + _binaryReader.BaseStream.Seek(1, SeekOrigin.Current); + Username = _binaryReader.ReadString(); + ExpectedNumOfBeatmaps = _binaryReader.ReadInt32(); + if (FileDate > 20160403) + _binaryReader.BaseStream.Seek(4, SeekOrigin.Current); + _logger?.Log(string.Format("Expected number of beatmaps: {0}", ExpectedNumOfBeatmaps)); + + if (ExpectedNumOfBeatmaps < 0) + { + return false; + } + } + catch { return false; } + return true; + } + private void DestoryReader() + { + _fileStream.Close(); + _binaryReader.Close(); + _fileStream.Dispose(); + _binaryReader.Dispose(); + } + protected virtual bool FileExists(string fullPath) + { + return !string.IsNullOrEmpty(fullPath) && File.Exists(fullPath); + } + } +} diff --git a/CollectionManagerDll/Modules/FileIO/OsuFileIo.cs b/CollectionManagerDll/Modules/FileIO/OsuFileIo.cs new file mode 100644 index 0000000..e4a6195 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuFileIo.cs @@ -0,0 +1,19 @@ +using CollectionManager.Modules.FileIO.FileCollections; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManager.Modules.FileIO +{ + public class OsuFileIo + { + public OsuDatabase OsuDatabase = new OsuDatabase(); + public OsuSettingsLoader OsuSettings = new OsuSettingsLoader(); + public CollectionLoader CollectionLoader; + public OsuPathResolver OsuPathResolver => OsuPathResolver.Instance; + public MapCacher LoadedMaps => OsuDatabase.LoadedMaps; + + public OsuFileIo() + { + CollectionLoader = new CollectionLoader(OsuDatabase.LoadedMaps); + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/FileIO/OsuPathResolver.cs b/CollectionManagerDll/Modules/FileIO/OsuPathResolver.cs new file mode 100644 index 0000000..3a9d898 --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuPathResolver.cs @@ -0,0 +1,111 @@ +using System; +using System.Diagnostics; +using System.IO; +using CollectionManager.Exceptions; +using Microsoft.Win32; + +namespace CollectionManager.Modules.FileIO +{ + public sealed class OsuPathResolver + { + public static OsuPathResolver Instance = new OsuPathResolver(); + private Process[] _processes; + + public bool OsuIsRunning + { + get + { + try + { + _processes = Process.GetProcessesByName("osu!"); + return _processes.Length > 0; + } + catch + { + return false; + } + } + } + + + private void Log(string text, params string[] vals) + { + // + } + + public string GetOsuDir(Func thisPathIsCorrect, Func selectDirectoryDialog) + { + var dir = _getRunningOsuDir(); + if (dir != string.Empty) + { + var result = thisPathIsCorrect(dir); + if (result) + return dir; + else + return GetManualOsuDir(selectDirectoryDialog); + } + return GetManualOsuDir(selectDirectoryDialog); + } + private string _getRunningOsuDir() + { + if (OsuIsRunning) + { + try + { + string dir = _processes[0].Modules[0].FileName; + dir = dir.Remove(dir.LastIndexOf('\\')); + return dir; + } + catch (Exception e) //Access denied + { + Log("ERROR: could not get directory from running osu! | {0}", e.Message); + } + } + else + { + try + { + using (RegistryKey osureg = Registry.ClassesRoot.OpenSubKey("osu\\DefaultIcon")) + { + if (osureg != null) + { + string osukey = osureg.GetValue(null).ToString(); + var osupath = osukey.Remove(0, 1); + osupath = osupath.Remove(osupath.Length - 11); + return osupath; + } + } + } + catch (Exception e) + { + Log("ERROR: could not get directory from registry key | {0}", e.Message); + } + + } + return string.Empty; + } + public string GetManualOsuDir(Func selectDirectoryDialog) + { + var directory = selectDirectoryDialog("Where is your osu! folder located at?"); + if (!File.Exists(directory + @"\osu!.db")) + directory = string.Empty; + + return directory; + } + + public string SelectDirectory(string text, bool showNewFolder = false) + { + //FolderBrowserDialog dialog = new FolderBrowserDialog(); + ////set description and base folder for browsing + + //dialog.ShowNewFolderButton = true; + //dialog.Description = text; + //dialog.RootFolder = Environment.SpecialFolder.MyComputer; + //if (dialog.ShowDialog() == DialogResult.OK && Directory.Exists((dialog.SelectedPath))) + //{ + // return dialog.SelectedPath; + //} + return string.Empty; + } + } +} \ No newline at end of file diff --git a/CollectionManagerDll/Modules/FileIO/OsuSettingsLoader.cs b/CollectionManagerDll/Modules/FileIO/OsuSettingsLoader.cs new file mode 100644 index 0000000..d56bd3c --- /dev/null +++ b/CollectionManagerDll/Modules/FileIO/OsuSettingsLoader.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; + +namespace CollectionManager.Modules.FileIO +{ + public sealed class OsuSettingsLoader + { + public string CustomBeatmapDirectoryLocation { get; private set; }= "Songs"; + + public OsuSettingsLoader() + { + + } + public void Load(string osuDirectory) + { + string FilePath = GetConfigFilePath(osuDirectory); + if(File.Exists(FilePath)) + ReadSettings(FilePath); + + if (CustomBeatmapDirectoryLocation == "Songs") + CustomBeatmapDirectoryLocation = Path.Combine(osuDirectory, "Songs\\"); + + } + private string GetConfigFilePath(string osuDirectory) + { + string filename = string.Format("osu!.{0}.cfg", StripInvalidCharacters(Environment.UserName)); + return Path.Combine(osuDirectory, filename); + } + + private string StripInvalidCharacters(string name) + { + foreach (var invalidChar in Path.GetInvalidFileNameChars()) + { + name = name.Replace(invalidChar.ToString(), string.Empty); + } + return name.Replace(".", string.Empty); + } + private void ReadSettings(string configPath) + { + foreach (var cfgLine in File.ReadLines(configPath)) + { + if (cfgLine.StartsWith("BeatmapDirectory")) + { + var splitedLines = cfgLine.Split(new[] { '=' }, 2); + var songDirectory = splitedLines[1].Trim(' '); + + if (songDirectory != "Songs") + CustomBeatmapDirectoryLocation = songDirectory; + } + } + } + } +} diff --git a/CollectionManagerDll/Properties/Annotations.cs b/CollectionManagerDll/Properties/Annotations.cs new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/CollectionManagerDll/Properties/Annotations.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/CollectionManagerDll/Properties/Annotations1.cs b/CollectionManagerDll/Properties/Annotations1.cs new file mode 100644 index 0000000..697801c --- /dev/null +++ b/CollectionManagerDll/Properties/Annotations1.cs @@ -0,0 +1,1039 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.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; + +#pragma warning disable 1591 +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable InconsistentNaming + +namespace CollectionManager.Annotations +{ + /// + /// Indicates that the value of the marked element could be null sometimes, + /// so the check for null is necessary before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element could never be null. + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] + public sealed class NotNullAttribute : Attribute { } + + /// + /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can never be null. + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be appplied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property + /// or of the Lazy.Value property can be null. + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] + public sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Implicitly apply [NotNull]/[ItemNotNull] annotation to all the of type members and parameters + /// in particular scope where this annotation is used (type declaration or whole assembly). + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface | AttributeTargets.Assembly)] + public sealed class ImplicitNotNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds string by format pattern and (optional) arguments. + /// Parameter, which contains format string, should be given in constructor. The format string + /// should be in -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] + public sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as format-string + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; private set; } + } + + /// + /// For a parameter that is expected to be one of the limited set of values. + /// Specify fields of which type should be used as values for this parameter. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; private set; } + } + + /// + /// Indicates that the function argument should be string literal and match one + /// of the parameters of the caller function. For example, ReSharper annotates + /// the parameter of . + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; private set; } + } + + /// + /// Describes dependency between method input and output. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If method has single input parameter, it's name could be omitted.
+ /// Using halt (or void/nothing, which is the same) + /// for method output means that the methos doesn't return normally.
+ /// canbenull annotation is only applicable for output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, + /// or use single attribute with rows separated by semicolon.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("halt <= condition: false")] + /// public void Assert(bool condition, string text) // regular assertion method + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("s:null=>false; =>true,result:notnull; =>false, result:null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; private set; } + public bool ForceFullStates { get; private set; } + } + + /// + /// Indicates that marked element should be localized or not. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; private set; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using '==' or '!=' operators and Equals() + /// should be used instead. However, using '==' or '!=' for comparison + /// with null is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] + public sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] + public sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; private set; } + } + + /// + /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), + /// so this symbol will not be marked as unused (as well as by other usage inspections). + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; private set; } + public ImplicitUseTargetFlags TargetFlags { get; private set; } + } + + /// + /// Should be used on attributes and causes ReSharper to not mark symbols marked with such attributes + /// as unused (as well as by other usage inspections) + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter)] + public sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; private set; } + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; private set; } + } + + [Flags] + public enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters won't be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specify what is considered used implicitly when marked + /// with or . + /// + [Flags] + public enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of entity marked with attribute are considered used. + Members = 2, + /// Entity marked with attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available API + /// which should not be removed and so is treated as used. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + public sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; private set; } + } + + /// + /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class InstantHandleAttribute : Attribute { } + + /// + /// Indicates that a method does not make any observable state changes. + /// The same as System.Diagnostics.Contracts.PureAttribute. + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Waring: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of method invocation must be used. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; private set; } + } + + /// + /// Indicates the type member or parameter of some type, that should be used instead of all other ways + /// to get the value that type. This annotation is useful when you have some "context" value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] + public sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; private set; } + } + + /// + /// An extension method marked with this attribute is processed by ReSharper code completion + /// as a 'Source Template'. When extension method is completed over some expression, it's source code + /// is automatically expanded like a template at call site. + /// + /// + /// Template method body can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// + /// + /// In this example, the 'forEach' method is a source template available over all values + /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class SourceTemplateAttribute : Attribute { } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression + /// is defined in the property. When applied on a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value = -1. + /// + /// + /// Applying the attribute on a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute on a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] + public sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + public string Expression { get; set; } + + /// + /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// > + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied on a template method. + /// + public string Target { get; set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute(string format) + { + Format = format; + } + + public string Format { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; private set; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; private set; } + } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] + public sealed class AspMvcActionSelectorAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] + public sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; private set; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] + public sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; private set; } + } + + /// + /// Razor attribute. Indicates that a parameter or a method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] + public sealed class RazorSectionAttribute : Attribute { } + + /// + /// Indicates how method, constructor invocation or property access + /// over collection type affects content of the collection. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] + public sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; private set; } + } + + [Flags] + public enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is assertion method, i.e. it halts control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; private set; } + } + + /// + /// Specifies assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// + public enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] + public sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, + /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that IEnumerable, passed as parameter, is not enumerated. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that parameter is regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RegexPatternAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the type that has ItemsSource property and should be treated + /// as ItemsControl-derived type, to enable inner items DataContext type resolve. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of ItemsControl-derived type. This annotation will + /// enable the DataContext type resolve for XAML bindings for such properties. + /// + /// + /// Property should have the tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; private set; } + [NotNull] public Type ControlType { get; private set; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; private set; } + } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; private set; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; private set; } + [NotNull] public string FieldName { get; private set; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; private set; } + } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] + public sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] + public sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class RazorWriteMethodParameterAttribute : Attribute { } + + /// + /// Prevents the Member Reordering feature from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns + /// + [AttributeUsage(AttributeTargets.All)] + public sealed class NoReorder : Attribute { } +} \ No newline at end of file diff --git a/CollectionManagerDll/Properties/AssemblyInfo.cs b/CollectionManagerDll/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9f7c4d6 --- /dev/null +++ b/CollectionManagerDll/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CollectionManager")] +[assembly: AssemblyDescription("Implements read/write access to db/osdb files, along with ability to edit these in a reasonable manner")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CollectionManager")] +[assembly: AssemblyCopyright("Copyright Piotrekol© 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("533ab47a-d1b5-45db-a37e-f053fa3699c4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CollectionManagerExtensionsDll/Class1.cs b/CollectionManagerExtensionsDll/Class1.cs new file mode 100644 index 0000000..c1650a2 --- /dev/null +++ b/CollectionManagerExtensionsDll/Class1.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CollectionManagerExtensionsDll +{ + public class Class1 + { + } +} diff --git a/CollectionManagerExtensionsDll/CollectionManagerExtensionsDll.csproj b/CollectionManagerExtensionsDll/CollectionManagerExtensionsDll.csproj new file mode 100644 index 0000000..33c71c7 --- /dev/null +++ b/CollectionManagerExtensionsDll/CollectionManagerExtensionsDll.csproj @@ -0,0 +1,98 @@ + + + + + Debug + AnyCPU + {2BDF5D5F-1CB0-47A6-8138-E4DB961740F2} + Library + Properties + CollectionManagerExtensionsDll + CollectionManagerExtensionsDll + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + full + true + + + + ..\packages\Newtonsoft.Json.10.0.2\lib\net40\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + Component + + + + + + + + + + + + + + + + {533ab47a-d1b5-45db-a37e-f053fa3699c4} + CollectionManagerDll + + + + + + + + + + + \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Enums/CollectionListSaveType.cs b/CollectionManagerExtensionsDll/Enums/CollectionListSaveType.cs new file mode 100644 index 0000000..3a6bad3 --- /dev/null +++ b/CollectionManagerExtensionsDll/Enums/CollectionListSaveType.cs @@ -0,0 +1,10 @@ +namespace CollectionManagerExtensionsDll.Enums +{ + public enum CollectionListSaveType + { + Txt, + Html, + osuBBCode, + RedditCode + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/API/BeatmapExtensionEx.cs b/CollectionManagerExtensionsDll/Modules/API/BeatmapExtensionEx.cs new file mode 100644 index 0000000..bda480e --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/API/BeatmapExtensionEx.cs @@ -0,0 +1,12 @@ +using System; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.API +{ + public class BeatmapExtensionEx : BeatmapExtension + { + public DateTime ApprovedDate { get; set; } + public int GenreId { get; set; } + public int LanguageId { get; set; } + } +} diff --git a/CollectionManagerExtensionsDll/Modules/API/osu/OsuApi.cs b/CollectionManagerExtensionsDll/Modules/API/osu/OsuApi.cs new file mode 100644 index 0000000..bd2f6be --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/API/osu/OsuApi.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using CollectionManager.DataTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace CollectionManagerExtensionsDll.Modules.API.osu +{ + public class OsuApi + { + private readonly WebClient _client = new WebClient(); + private string _apiKey; + + private const string ApiUrl = "https://osu.ppy.sh/api/"; + private const string GetBeatmapsURL = ApiUrl + "get_beatmaps"; + private const string GetUserURL = ApiUrl + "get_user"; + private const string GetScoresURL = ApiUrl + "get_scores"; + private const string GetUserBestURL = ApiUrl + "get_user_best"; + private const string GetUserRecentURL = ApiUrl + "get_user_recent"; + private const string GetMatchURL = ApiUrl + "get_match"; + public OsuApi(string apiKey) + { + _apiKey = apiKey; + } + + public Beatmaps GetBeatmaps(DateTime fromDate, DateTime toDate) + { + var resultBeatmaps = new Beatmaps(); + + DateTime currentDate = fromDate; + + while (currentDate < toDate) + { + var newBeatmaps = GetBeatmaps(string.Format(GetBeatmapsURL + "?k={0}&since={1}", _apiKey, currentDate.ToString("yyyy-MM-dd HH:mm:ss"))); + + if (newBeatmaps.Count < 500) + currentDate = toDate; + foreach (var newBeatmap in newBeatmaps) + { + if (newBeatmap.ApprovedDate < toDate) + { + resultBeatmaps.Add(newBeatmap); + } + + if (currentDate < newBeatmap.ApprovedDate) + { + currentDate = newBeatmap.ApprovedDate; + } + } + } + return resultBeatmaps; + } + + private RangeObservableCollection GetBeatmaps(string url) + { + var beatmaps = new RangeObservableCollection(); + + var jsonResponse = _client.DownloadString(url); + if (jsonResponse == "Please provide a valid API key.") + throw new Exception("Invalid osu!Api key"); + //jsonResponse = jsonResponse.Trim(']', '['); + if (jsonResponse.Trim(' ') == string.Empty) + return null; + var jsonArray = JArray.Parse(jsonResponse); + + foreach (var json in jsonArray) + { + var beatmap = new BeatmapExtensionEx(); + beatmap.MapSetId = int.Parse(json["beatmapset_id"].ToString()); + beatmap.MapId = int.Parse(json["beatmap_id"].ToString()); + beatmap.DiffName = json["version"].ToString(); + beatmap.Md5 = json["file_md5"].ToString(); + beatmap.ArtistRoman = json["artist"].ToString(); + beatmap.TitleRoman = json["title"].ToString(); + beatmap.Creator = json["creator"].ToString(); + beatmap.ApprovedDate = DateTime.ParseExact(json["approved_date"].ToString(), "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + beatmap.ModPpStars.Add(0, Math.Round(double.Parse(json["difficultyrating"].ToString(), CultureInfo.InvariantCulture), 2)); + beatmap.GenreId = Int32.Parse(json["genre_id"].ToString()); + beatmap.LanguageId = Int32.Parse(json["language_id"].ToString()); + beatmap.DataDownloaded = true; + + beatmaps.Add(beatmap); + } + + + + + + + return beatmaps; + } + + public BeatmapExtension GetBeatmap(int beatmapId) + { + return GetBeatmapResult(GetBeatmapsURL + "?k=" + _apiKey + "&b=" + beatmapId); + } + public BeatmapExtension GetBeatmap(string hash) + { + return GetBeatmapResult(GetBeatmapsURL + "?k=" + _apiKey + "&h=" + hash); + } + + private BeatmapExtension GetBeatmapResult(string url) + { + var jsonResponse = _client.DownloadString(url); + if (jsonResponse == "Please provide a valid API key.") + throw new Exception("Invalid osu!Api key"); + jsonResponse = jsonResponse.Trim(']', '['); + if (jsonResponse.Trim(' ') == string.Empty) + return null; + var json = JObject.Parse(jsonResponse); + var beatmap = new BeatmapExtension(); + //var a = json.Count; + beatmap.MapSetId = int.Parse(json["beatmapset_id"].ToString()); + beatmap.MapId = int.Parse(json["beatmap_id"].ToString()); + beatmap.DiffName = json["version"].ToString(); + beatmap.Md5 = json["file_md5"].ToString(); + beatmap.ArtistRoman = json["artist"].ToString(); + beatmap.TitleRoman = json["title"].ToString(); + beatmap.Creator = json["creator"].ToString(); + beatmap.ModPpStars.Add(0, Math.Round(double.Parse(json["difficultyrating"].ToString(), CultureInfo.InvariantCulture), 2)); + //beatmap.OverallDifficulty = float.Parse(json["difficultyrating"].ToString(), ); + beatmap.DataDownloaded = true; + + return beatmap; + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/BeatmapFilter/BeatmapFilter.cs b/CollectionManagerExtensionsDll/Modules/BeatmapFilter/BeatmapFilter.cs new file mode 100644 index 0000000..c33f091 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/BeatmapFilter/BeatmapFilter.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using CollectionManager.DataTypes; +using CollectionManager.Enums; +using CollectionManager.Modules.FileIO.OsuDb; + +namespace CollectionManagerExtensionsDll.Modules.BeatmapFilter +{ + public class BeatmapFilter + { + public const string SpaceReplacement = "!._!"; + private Beatmaps _beatmaps; + public Dictionary BeatmapHashHidden = new Dictionary(); + + private delegate bool searchFilter(Beatmap m); + internal static readonly NumberFormatInfo nfi = new CultureInfo(@"en-US", false).NumberFormat; + + private static Regex regComparison = new Regex(@"^(\w*)([<>=]=?|!=)(.*)$"); + private static Regex regNumber = new Regex + (@"(?x) + ^ + [+-]? + (?: + (?>\d+) \.? + | + \. \d + ) + \d* + (?: e [+-]? \d+ )? + $ + "); + + public BeatmapFilter(Beatmaps beatmaps) + { + SetBeatmaps(beatmaps); + } + + public void SetBeatmaps(Beatmaps beatmaps) + { + _beatmaps = beatmaps; + } + public void UpdateSearch(string searchString) + { + searchString = searchString.ToLower(); + string[] words = searchString.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + lock (_beatmaps) + { + foreach (var beatmap in _beatmaps) + { + BeatmapHashHidden[beatmap.Md5] = false; + } + foreach (string w in words) + { + searchFilter filter = GetSearchFilter(w); + + foreach (var b in _beatmaps) + { + if (!BeatmapHashHidden[b.Md5] && !filter(b)) + { + BeatmapHashHidden[b.Md5] = true; + } + } + } + } + } + /// + /// Returns beatmapFilter delegate for specified searchWord. + /// Unimplemented: key/keys/speed/played/unplayed + /// + /// + /// + private searchFilter GetSearchFilter(string searchWord) + { + Match match = regComparison.Match(searchWord); + if (match.Success) + { + string key = match.Groups[1].Value.ToLower(); + string op = match.Groups[2].Value; + string val = match.Groups[3].Value.ToLower(); + if (op == @"=") op = @"=="; + + double num; + + Match matchNum = regNumber.Match(val); + if (matchNum.Success) + { + num = Double.Parse(matchNum.Groups[0].Value, nfi); + switch (key) + { + + case "star": + case "stars": + return delegate (Beatmap b) { return isPatternMatch(Math.Round(b.StarsNomod, 2), op, num); }; + + case "cs": + return delegate (Beatmap b) { return isPatternMatch(Math.Round((double)b.CircleSize, 1), op, num) && b.PlayMode != PlayModes.OsuMania && b.PlayMode != PlayModes.Taiko; }; + case "hp": + return delegate (Beatmap b) { return isPatternMatch(Math.Round((double)b.HpDrainRate, 1), op, num); }; + case "od": + return delegate (Beatmap b) { return isPatternMatch(Math.Round((double)b.OverallDifficulty, 1), op, num); }; + case "ar": + return delegate (Beatmap b) { return isPatternMatch(Math.Round((double)b.ApproachRate, 1), op, num) && b.PlayMode != PlayModes.OsuMania && b.PlayMode != PlayModes.Taiko; }; + + case "key": + case "keys": + return delegate (Beatmap b) { return RetFalse(); }; + + case "speed": + return delegate (Beatmap b) { return RetFalse(); }; + + case "bpm": + return delegate (Beatmap b) { return isPatternMatch(Math.Round(b.MinBpm), op, num); }; + case "length": + return delegate (Beatmap b) { return isPatternMatch(b.TotalTime / 1000, op, num); }; + case "drain": + return delegate (Beatmap b) { return isPatternMatch(b.DrainingTime, op, num); }; + + case "played": + break; + } + } + + switch (key) + { + case "artist": + var artist = val.Replace(SpaceReplacement, " "); + return delegate (Beatmap b) { return isArtistMatch(b, artist); }; + case "title": + var title = val.Replace(SpaceReplacement, " "); + return delegate (Beatmap b) { return isTitleMatch(b, title); }; + case "unplayed": + if (String.IsNullOrEmpty(val)) + return delegate { return RetFalse(); }; + break; + case "mode": + num = descriptorToNum(val, ModePairs); + return delegate (Beatmap b) { return isPatternMatch((double)b.PlayMode, op, num); }; + case "status": + num = descriptorToNum(val, StatusPairs); + + return delegate (Beatmap b) { return isPatternMatch((double)b.State, op, num); }; + } + } + int id; + if (Int32.TryParse(searchWord, out id)) + { + return delegate (Beatmap b) + { + //match mapid and mapset id while input is numbers. + if (b.MapId == id) return true; + if (b.MapSetId == id) return true; + if (b.ThreadId == id) return true; + return isWordMatch((BeatmapExtension)b, searchWord); + }; + } + return delegate (Beatmap b) { return isWordMatch((BeatmapExtension)b, searchWord); }; + } + + private static readonly KeyValuePair[] StatusPairs = new KeyValuePair[] + { + new KeyValuePair ((double)SubmissionStatus.Unknown, "unknown"), + new KeyValuePair ((double)SubmissionStatus.NotSubmitted, "notsubmitted"), + new KeyValuePair ((double)SubmissionStatus.Pending, "pending"), + new KeyValuePair ((double)SubmissionStatus.Ranked, "ranked"), + new KeyValuePair ((double)SubmissionStatus.Approved, "approved"), + new KeyValuePair ((double)SubmissionStatus.Qualified, "qualified"), + new KeyValuePair ((double)SubmissionStatus.Loved, "loved"), + }; + private static readonly KeyValuePair[] ModePairs = new KeyValuePair[] + { + new KeyValuePair ((double)PlayModes.Osu, "osu!"), + new KeyValuePair ((double)PlayModes.Taiko, "taiko"), + new KeyValuePair ((double)PlayModes.CatchTheBeat, "catchthebeat"), + new KeyValuePair ((double)PlayModes.CatchTheBeat, "ctb"), + new KeyValuePair ((double)PlayModes.OsuMania, "osu!mania"), + new KeyValuePair ((double)PlayModes.OsuMania, "osumania"), + new KeyValuePair ((double)PlayModes.OsuMania, "mania"), + new KeyValuePair ((double)PlayModes.OsuMania, "o!m"), + }; + + + + public enum SubmissionStatus + { + Unknown, + NotSubmitted, + Pending, + EditableCutoff, + Ranked, + Approved, + Qualified, + Loved + } + private static double descriptorToNum(string got, KeyValuePair[] pairs) + { + if (got.Length == 0) + return Double.NaN; + + foreach (KeyValuePair want in pairs) + { + if (want.Value.StartsWith(got)) + return want.Key; + } + + return Double.NaN; + } + + private bool RetFalse() + { + return false; + } + private bool isWordMatch(BeatmapExtension b, string word) + { + if (b.ToString(true).IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.Creator.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.Tags.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.Source.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.ArtistUnicode != null && b.ArtistUnicode.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.TitleUnicode != null && b.TitleUnicode.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + return false; + } + + private bool isArtistMatch(Beatmap b, string word) + { + if (b.ArtistUnicode != null && b.ArtistUnicode.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.ArtistRoman != null && b.ArtistRoman.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + return false; + } + private bool isTitleMatch(Beatmap b, string word) + { + if (b.TitleUnicode != null && b.TitleUnicode.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + if (b.TitleRoman != null && b.TitleRoman.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) >= 0) return true; + return false; + } + private bool isPatternMatch(T left, string op, T right) where T : IComparable + { + int cmp = left.CompareTo(right); + switch (op) + { + case @"<": + return cmp < 0; + case @">": + return cmp > 0; + case "==": + return cmp == 0; + case ">=": + return cmp >= 0; + case "<=": + return cmp <= 0; + case "!=": + return cmp != 0; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/BeatmapType.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/BeatmapType.cs new file mode 100644 index 0000000..593782e --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/BeatmapType.cs @@ -0,0 +1,7 @@ +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator +{ + public enum BeatmapListType + { + All, Known, NotKnown, Downloadable + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/IListGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/IListGenerator.cs new file mode 100644 index 0000000..f622eab --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/IListGenerator.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator +{ + public interface IListGenerator + { + void StartGenerating(); + void EndGenerating(); + string GetListHeader(Collections collections); + string GetCollectionBody(Collection collection, Dictionary mapSets, int collectionNumber); + string GetListFooter(Collections collections); + } +} diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListGenerator.cs new file mode 100644 index 0000000..11feb85 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListGenerator.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Enums; +using CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes; +using CollectionManagerExtensionsDll.Utils; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator +{ + public class ListGenerator + { + private readonly StringBuilder _stringBuilder = new StringBuilder(); + private Dictionary _listGenerators; + + public ListGenerator() + { + _listGenerators = new Dictionary(); + + _listGenerators.Add(CollectionListSaveType.Txt, new TxtListGenerator()); + _listGenerators.Add(CollectionListSaveType.Html, new HtmlListGenerator()); + _listGenerators.Add(CollectionListSaveType.osuBBCode, new OsuBbCodeGenerator()); + _listGenerators.Add(CollectionListSaveType.RedditCode, new RedditCodeGenerator()); + } + public string GetMissingMapsList(Collections collections, + CollectionListSaveType listType = CollectionListSaveType.Txt) + { + return GenerateList(collections, listType, BeatmapListType.NotKnown); + } + + public string GetAllMapsList(Collections collections, + CollectionListSaveType listType = CollectionListSaveType.Txt) + { + return GenerateList(collections, listType, BeatmapListType.All); + } + + private string GenerateList(Collections collections, + CollectionListSaveType listType = CollectionListSaveType.Txt, + BeatmapListType beatmapListType = BeatmapListType.All) + { + if (collections == null) return ""; + _listGenerators[listType].StartGenerating(); + + _stringBuilder.Clear(); + _stringBuilder.Append(_listGenerators[listType].GetListHeader(collections)); + for (int i = 0; i < collections.Count; i++) + { + var mapSets = collections[i].GetMapSets(beatmapListType); + _stringBuilder.Append( + _listGenerators[listType].GetCollectionBody(collections[i], mapSets, i) + ); + + } + _stringBuilder.Append(_listGenerators[listType].GetListFooter(collections)); + + _listGenerators[listType].EndGenerating(); + + + return _stringBuilder.ToString(); + } + + + + + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/GenericGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/GenericGenerator.cs new file mode 100644 index 0000000..6ffd95b --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/GenericGenerator.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Text; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + internal abstract class GenericGenerator : IListGenerator + { + private StringBuilder _mainStringBuilder = new StringBuilder(); + private StringBuilder _md5Output = new StringBuilder(); + + protected abstract string MainHeader { get; } + protected abstract string MainFooter { get; } + + /*0 - mapLink + * 1- full map string with diff + * 2- map creator + * 3- map diff + * 4- nomodStars + * 5- full map string without diff*/ + protected abstract string CollectionBodyFormat { get; } + + protected abstract string CollectionFooter { get; } + + /*0 - collection name + *1 - collection map count*/ + protected abstract string CollectionHeaderTemplate { get; } + public void StartGenerating() + { + + } + + public void EndGenerating() + { + + } + + public string GetListHeader(Collections collections) + { + return MainHeader; + } + + public string GetCollectionBody(Collection collection, Dictionary mapSets, int collectionNumber) + { + _mainStringBuilder.Clear(); + _md5Output.Clear(); + + _mainStringBuilder.AppendFormat(CollectionHeaderTemplate, collection.Name, collection.NumberOfBeatmaps); + foreach (var mapSet in mapSets) + { + GetMapSetList(mapSet.Key, mapSet.Value, ref _mainStringBuilder); + } + _mainStringBuilder.Append(CollectionFooter); + return _mainStringBuilder.ToString(); + } + + public string GetListFooter(Collections collections) + { + return MainFooter; + } + protected virtual void GetMapSetList(int mapSetId, Beatmaps beatmaps, ref StringBuilder sb) + { + + if (mapSetId == -1) + { + foreach (var map in beatmaps) + { + if (map.MapId > 0) + { + + sb.AppendFormat(CollectionBodyFormat, map.MapLink, map.ToString(true), map.Creator, map.DiffName, map.StarsNomod, map.ToString()); + } + else + { + _md5Output.AppendFormat(CollectionBodyFormat, "http://osu.ppy.sh/b/0", map.Md5, "","","",map.Md5); + } + } + } + else + { + foreach (var map in beatmaps) + { + sb.AppendFormat(CollectionBodyFormat, map.MapLink, map.ToString(true), map.Creator, map.DiffName, map.StarsNomod, map.ToString()); + } + } + sb.Append(_md5Output.ToString()); + _md5Output.Clear(); + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/HtmlListGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/HtmlListGenerator.cs new file mode 100644 index 0000000..edb5452 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/HtmlListGenerator.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Text; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + public class HtmlListGenerator : IListGenerator + { + private StringBuilder _mainStringBuilder = new StringBuilder(); + private StringBuilder _md5Output = new StringBuilder(); + + /*{0} - username of creator + * {1} - number of collections in list + * {2} - Collections + * + * + */ + private string _htmlOutputHeader = @" + + + + + + + + +

List of maps in collections
+Generated by: ""{0}""
+Number of collections listed: {1}

"; + private string _htmlOutputFooter = ""; + /*{0}-collection number + * {1}-collection name + * {2}-number of maps in collection(diffs) + * + */ + string CollectionHeaderTemplate = @"

Collection {0}: {1} ( {2} diffs )

"; + /* {0} - map link + * {1} - artist + * {2} - title + */ + string CollectionBeatmapTemplateFull = @" +{1} - {2} "; + //{0} - md5 + string CollectionBeatmapTemplateMd5 = @"No Data {0}
+"; + public void StartGenerating() + { + _mainStringBuilder.Clear(); + } + + public void EndGenerating() + { + _mainStringBuilder.Clear(); + } + + public string GetListHeader(Collections collections) + { + return string.Format(_htmlOutputHeader, "N/A", collections.Count); + } + + public string GetCollectionBody(Collection collection, Dictionary mapSets, int collectionNumber) + { + _mainStringBuilder.Clear(); + + _mainStringBuilder.AppendFormat(CollectionHeaderTemplate, collectionNumber, + collection.Name, collection.NumberOfBeatmaps); + + foreach (var mapSet in mapSets) + { + GetMapSetList(mapSet.Key, mapSet.Value, ref _mainStringBuilder); + } + + return _mainStringBuilder.ToString(); + } + + public string GetListFooter(Collections collections) + { + return _htmlOutputFooter; + } + private void GetMapSetList(int mapSetId, Beatmaps beatmaps, ref StringBuilder sb) + { + + if (mapSetId == -1) + { + foreach (var map in beatmaps) + { + if (map.MapId > 0) + { + sb.AppendFormat(CollectionBeatmapTemplateFull, map.MapLink, map.ArtistRoman, + map.TitleRoman); + if (!string.IsNullOrWhiteSpace(map.DiffName)) + sb.AppendFormat("[{1}]{2}★ ", + map.MapId, map.DiffName, map.StarsNomod); + } + else + { + _md5Output.AppendFormat(CollectionBeatmapTemplateMd5, map.Md5); + } + } + } + else + { + sb.AppendFormat(CollectionBeatmapTemplateFull, beatmaps[0].MapLink, beatmaps[0].ArtistRoman, + beatmaps[0].TitleRoman); + foreach (var map in beatmaps) + { + sb.AppendFormat("[{1}]{2}★ ", + map.MapId, map.DiffName, map.StarsNomod); + + } + sb.Append("
"); + } + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/OsuBbCodeGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/OsuBbCodeGenerator.cs new file mode 100644 index 0000000..eca644d --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/OsuBbCodeGenerator.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + internal class OsuBbCodeGenerator : GenericGenerator + { + protected override string CollectionBodyFormat { get; } = Environment.NewLine + "[*][url={0}]{1} ({2})[/url]"; + + protected override string CollectionFooter { get; } = "[/list]"; + + protected override string CollectionHeaderTemplate { get; } = Environment.NewLine + Environment.NewLine + "[centre][size=150][b]{0}[/b][/size][/centre][list]"; + + protected override string MainFooter { get; } = "[/notice]"; + + protected override string MainHeader { get; } = "[notice]"; + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/RedditCodeGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/RedditCodeGenerator.cs new file mode 100644 index 0000000..3b867c6 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/RedditCodeGenerator.cs @@ -0,0 +1,13 @@ +using System; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + internal class RedditCodeGenerator : GenericGenerator + { + protected override string MainHeader { get; } = "|Name|Difficulty|BeatmapId|★|" + Environment.NewLine + "|---|---|---|:-:|"; + protected override string MainFooter { get; } = ""; + protected override string CollectionBodyFormat { get; } = Environment.NewLine + "{5}|{3}|{0}|{4}★"; + protected override string CollectionFooter { get; } = ""; + protected override string CollectionHeaderTemplate { get; } = ""; + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/TxtListGenerator.cs b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/TxtListGenerator.cs new file mode 100644 index 0000000..c9403e0 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/CollectionListGenerator/ListTypes/TxtListGenerator.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Text; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + internal class TxtListGenerator: IListGenerator + { + internal string Lb = "\r\n"; + private StringBuilder _stringBuilder = new StringBuilder(); + + public string GetListHeader(Collections collections) + { + return ""; + } + + + public string GetCollectionBody(Collection collection, Dictionary mapSets,int collectionNumber) + { + _stringBuilder.Clear(); + + _stringBuilder.AppendFormat("{0}{1}{0}{0}", Lb, string.Format("Collection {0}: {1}", collectionNumber, collection.Name)); + //collection content(beatmaps) + + foreach (var mapSet in mapSets) + { + GetMapSetList(mapSet.Key, mapSet.Value, ref _stringBuilder); + } + + return _stringBuilder.ToString(); + } + + public string GetListFooter(Collections collections) + { + return ""; + } + + public void StartGenerating() + { + _stringBuilder.Clear(); + } + + public void EndGenerating() + { + _stringBuilder.Clear(); + } + + private void GetMapSetList(int mapSetId, Beatmaps beatmaps, ref StringBuilder sb) + { + + if (mapSetId == -1) + { + foreach (var map in beatmaps) + { + if (map.MapId > 0) + { + sb.AppendFormat("{0} {1} - {2}", map.MapLink, map.ArtistRoman, map.TitleRoman); + if (!string.IsNullOrWhiteSpace(map.DiffName)) + sb.AppendFormat(" [{0}]{1}", map.DiffName, Lb); + } + else + { + sb.AppendFormat("No data - {0}{1}", map.Md5, Lb); + } + + } + } + else + { + sb.AppendFormat("{0} {1} - {2}", beatmaps[0].MapLink, beatmaps[0].ArtistRoman, beatmaps[0].TitleRoman); + foreach (var map in beatmaps) + { + sb.AppendFormat(" [{0}] {1}★", map.DiffName, map.StarsNomod); + } + sb.Append(Lb); + } + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/CookieAwareWebClient.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/CookieAwareWebClient.cs new file mode 100644 index 0000000..5c166ae --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/CookieAwareWebClient.cs @@ -0,0 +1,63 @@ +using System.IO; + +namespace System.Net +{ + using System.Text; + using System.Collections.Specialized; + + public class CookieAwareWebClient : WebClient + { + public int ClientId = -1; + public string Login(string loginPageAddress, string loginData) + { + CookieContainer container; + + var request = (HttpWebRequest)WebRequest.Create(loginPageAddress); + + request.Method = "POST"; + request.ContentType = "application/x-www-form-urlencoded"; + var data = loginData.ToString(); + var buffer = Encoding.ASCII.GetBytes(loginData.ToString()); + request.ContentLength = buffer.Length; + var requestStream = request.GetRequestStream(); + requestStream.Write(buffer, 0, buffer.Length); + requestStream.Close(); + + container = request.CookieContainer = new CookieContainer(); + + var response = request.GetResponse(); + string ResponseText = ""; + using (StreamReader sr = new StreamReader(response.GetResponseStream())) + { + ResponseText = sr.ReadToEnd(); + } + response.Close(); + CookieContainer = container; + return ResponseText; + } + + public CookieAwareWebClient(CookieContainer container) + { + CookieContainer = container; + } + + + public CookieAwareWebClient() + : this(new CookieContainer()) + { } + + public CookieContainer CookieContainer { get; private set; } + + public void SetCustomCookieContainer(CookieContainer CookieContainer) + { + this.CookieContainer = CookieContainer; + } + protected override WebRequest GetWebRequest(Uri address) + { + var request = (HttpWebRequest)base.GetWebRequest(address); + request.CookieContainer = CookieContainer; + request.Timeout = 5 * 1000; + return request; + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadItem.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadItem.cs new file mode 100644 index 0000000..870a026 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadItem.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager.API +{ + public class DownloadItem + { + public EventHandler DownloadUpdated; + private void OnDownloadUpdated() { DownloadUpdated?.Invoke(this, EventArgs.Empty); } + public long Id { get; set; } + public string Url { get; set; } + public string FileName { get; set; } + public string Name { get { return FileName; } } + + public string Progress + { + get + { + if (OtherError) + return Error; + if (DownloadAborted) + return "Download cancelled"; + if (FileAlreadyExists) + return "File already exists"; + return string.Format("{0}/{1}MB {2}%", ((BytesRecived / 1024f) / 1024f).ToString("F"), + ((TotalBytes / 1024f) / 1024f).ToString("F"), ProgressPrecentage); + } + } + public long BytesRecived { get; set; } + public long TotalBytes { get; set; } + private int _progressPrecentage; + public int ProgressPrecentage + { + get { return _progressPrecentage; } + set + { + _progressPrecentage = value; + OnDownloadUpdated(); + } + } + + public bool DownloadAborted { get; set; } + public bool FileAlreadyExists { get; set; } + public bool OtherError { get; set; } + public string Error { get; set; } + public CookieAwareWebClient WebClient { get; set; } + public int lastShownDlState { get; set; } = -1; + public object UserToken { get; set; } + + public void ResetErrorState() + { + this.Error = ""; + this.OtherError = false; + this.DownloadAborted = false; + this.FileAlreadyExists = false; + } + public override string ToString() + { + return "DLitem: " + this.Url + " ; " + this.FileName; + } + } +} diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadManager.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadManager.cs new file mode 100644 index 0000000..3967f59 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadManager.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Threading; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager.API +{ + + public abstract class DownloadManager + { + protected Queue Clients = new Queue(); + readonly LinkedList _urlsToDownload = new LinkedList(); + ConcurrentQueue FileOperations = new ConcurrentQueue(); + ConcurrentBag CurrentlyProcessedUrls = new ConcurrentBag(); + private Timer _urlWatcher; + private Timer _ProgressWatcher; + private string _saveLocation; + private bool _stopDownloads { get; set; } + Dictionary downloadCheck = new Dictionary(); + public event EventHandler ProgressUpdated; + private static object _lockingObject = ""; + public DownloadManager(string saveLocation,int downloadThreads) + { + _saveLocation = saveLocation; + + for (int i = 0; i < downloadThreads; i++) + { + var webClient = new CookieAwareWebClient(); + webClient.ClientId = i; + webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged); + webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed); + Clients.Enqueue(webClient); + downloadCheck.Add(i, new DownloadProgress()); + } + + //Run callback every 500ms with null as state + _urlWatcher = new Timer(Callback, null, 0, 250); + _ProgressWatcher = new Timer(ProgressWatcher, null, 0, 5000); + + } + + public void ChangeDefaultConnectionPolicy(int maxConnectionsToSameServer) + { + ServicePointManager.DefaultConnectionLimit = maxConnectionsToSameServer; + } + public void StopDownloads() + { + lock (_lockingObject) + _stopDownloads = true; + } + + public void ResumeNewDownloads() + { + lock (_lockingObject) + _stopDownloads = false; + } + + private void ProgressWatcher(object state) + { + lock (_lockingObject) + { + foreach (var dlItemCheck in downloadCheck) + { + if (dlItemCheck.Value.IsStalled()) + { + dlItemCheck.Value.downloadItem.WebClient.CancelAsync(); + } + dlItemCheck.Value.Process(); + } + } + while (FileOperations.Count > 0) + { + FileWorkerArgs args; + if (FileOperations.TryDequeue(out args)) + { + switch (args.action) + { + case "removeTemp": + try + { + if (File.Exists(args.orginalLocation)) + File.Delete(args.orginalLocation); + } + catch (IOException) { } + break; + case "moveTemp": + if (File.Exists(args.orginalLocation)) + { + if (!File.Exists(args.desiredLocation)) + { + File.Move(args.orginalLocation, args.desiredLocation); + } + } + break; + } + } + else + { + break; + } + } + } + private void Callback(object state) + { + //Main async download loop + lock (_lockingObject) + { + if (_stopDownloads) + { + foreach (var dlItemCheck in downloadCheck) + { + if (dlItemCheck.Value.downloadItem != null) + { + dlItemCheck.Value.downloadItem.WebClient.CancelAsync(); + dlItemCheck.Value.Reset(); + } + } + } + else + lock (_urlsToDownload) + if (_urlsToDownload.Count > 0) + { + if (Clients.Count > 0) + { + var client = Clients.Dequeue(); + var downloadItem = _urlsToDownload.First.Value; + _urlsToDownload.RemoveFirst(); + downloadItem.WebClient = client; + DownloadFile(downloadItem); + } + } + } + + } + + private void DownloadFile(DownloadItem downloadItem) + { + lock (_lockingObject) + { + string filePath = Path.Combine(_saveLocation, downloadItem.FileName); + if (File.Exists(filePath)) + { + downloadItem.FileAlreadyExists=true;// = "File already exists"; + Clients.Enqueue(downloadItem.WebClient); + return; + } + downloadCheck[downloadItem.WebClient.ClientId].Reset(); + downloadItem.ResetErrorState(); + downloadCheck[downloadItem.WebClient.ClientId].downloadItem = downloadItem; + downloadItem.WebClient.DownloadFileAsync(new Uri(downloadItem.Url), + GetFullTempLocation(downloadItem.FileName), downloadItem); + } + } + + internal class FileWorkerArgs + { + public string action { get; set; } + public string orginalLocation { get; set; } + public string desiredLocation { get; set; } + } + private void Completed(object sender, AsyncCompletedEventArgs e) + { + lock (_lockingObject) + { + var url = (DownloadItem) e.UserState; + bool error = false; + if (e.Cancelled) + { + url.DownloadAborted = true;//Progress = "download cancelled"; + error = true; + lock (_urlsToDownload) + _urlsToDownload.AddFirst(url); + } + else if (e.Error != null) + { + url.OtherError = true; + url.Error = "Error: " + e.ToString(); + error = true; + } + if (error) + { + string tempFileLocation = GetFullTempLocation(url.FileName); + FileOperations.Enqueue(new FileWorkerArgs() + { + action = "removeTemp", + orginalLocation = tempFileLocation + }); + } + else + { + string tempFileLocation = GetFullTempLocation(url.FileName); + string fileLocation = GetFullLocation(url.FileName); + FileOperations.Enqueue(new FileWorkerArgs() + { + action = "moveTemp", + orginalLocation = tempFileLocation, + desiredLocation = fileLocation + }); + + } + downloadCheck[url.WebClient.ClientId].Reset(); + Clients.Enqueue(url.WebClient); + } + } + + private string GetFullLocation(string filename) + { + return Path.Combine(_saveLocation, filename); + } + private string GetFullTempLocation(string filename) + { + return Path.Combine(_saveLocation, GetTempFilename(filename)); + } + private string GetTempFilename(string path) + { + return path + ".tmp"; + } + + private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e) + { + int progress = e.ProgressPercentage; + + var DlItem = (DownloadItem)e.UserState; + downloadCheck[DlItem.WebClient.ClientId].bytesRecived = e.BytesReceived; + if (DlItem.lastShownDlState != progress) + { + DlItem.lastShownDlState = progress; + OnProgressUpdated(e); + } + } + public DownloadItem DownloadFileAsync(string url, string filename, object token) + { + var dlItem = new DownloadItem() { FileName = filename, Url = url, UserToken = token }; + lock (_urlsToDownload) + _urlsToDownload.AddLast(dlItem); + return dlItem; + } + + protected virtual void OnProgressUpdated(DownloadProgressChangedEventArgs e) + { + var dlItem = (DownloadItem) e.UserState; + dlItem.BytesRecived = e.BytesReceived; + dlItem.TotalBytes = e.TotalBytesToReceive; + dlItem.ProgressPrecentage = e.ProgressPercentage; + ProgressUpdated?.Invoke(this, e); + } + } +} diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgress.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgress.cs new file mode 100644 index 0000000..9c8108f --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgress.cs @@ -0,0 +1,37 @@ +using System; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager.API +{ + internal class DownloadProgress + { + public long SameValue = 0; + public long LastBytesRecived { get; set; } = -2; + public long bytesRecived { get; set; } = -1; + public DownloadItem downloadItem { get; set; } = null; + public bool IsStalled() + { + if (downloadItem == null) + return false; + if (bytesRecived == LastBytesRecived) + { + SameValue++; + return SameValue > 1; + } + SameValue = 0; + return false; + } + + public void Process() + { + LastBytesRecived = bytesRecived; + } + + public void Reset() + { + LastBytesRecived = -2; + bytesRecived = -1; + SameValue = 0; + downloadItem = null; + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgressReportEventArgs.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgressReportEventArgs.cs new file mode 100644 index 0000000..bcb9190 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/DownloadProgressReportEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager.API +{ + public class DownloadProgressReportEventArgs :EventArgs + { + public DownloadProgressReportEventArgs(long id) + { + this.Id = id; + } + public string Url { get; set; } + public string FileName { get; set; } + public long Id { get; private set; } + } +} diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/API/LoginData.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/LoginData.cs new file mode 100644 index 0000000..6e0ec20 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/API/LoginData.cs @@ -0,0 +1,15 @@ +namespace CollectionManagerExtensionsDll.Modules.DownloadManager.API +{ + public class LoginData + { + public string Username { get; set; } + public string Password { get; set; } + + public bool isValid() + { + if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password)) + return false; + return (Username.Length > 2 && Password.Length > 5); + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/ILoginForm.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/ILoginForm.cs new file mode 100644 index 0000000..d2be296 --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/ILoginForm.cs @@ -0,0 +1,9 @@ +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager +{ + public interface ILoginForm + { + LoginData GetLoginData(); + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/Modules/DownloadManager/OsuDownloader.cs b/CollectionManagerExtensionsDll/Modules/DownloadManager/OsuDownloader.cs new file mode 100644 index 0000000..f4e9dfe --- /dev/null +++ b/CollectionManagerExtensionsDll/Modules/DownloadManager/OsuDownloader.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Net; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; + +namespace CollectionManagerExtensionsDll.Modules.DownloadManager +{ + public class OsuDownloader : API.DownloadManager + { + public OsuDownloader(string saveLocation, int downloadThreads) : base(saveLocation, downloadThreads) + { + } + + public bool Login(LoginData loginData) + { + var loginAddress = @"https://osu.ppy.sh/forum/ucp.php?mode=login"; + string loginDataStr = string.Format("username={0}&password={1}&login=login&sid=", loginData.Username, loginData.Password); + + + CookieContainer cookies = null; + //Take all webClients and login/set correct cookies + List webClients = new List(); + var clientCount = this.Clients.Count; + for (int i = clientCount; i > 0; i--) + { + var client = this.Clients.Dequeue(); + if (i == clientCount) + { + var response = client.Login(loginAddress, loginDataStr); + if (response.IndexOf("Welcome, guest!", StringComparison.InvariantCultureIgnoreCase) > 0) + return false; + cookies = client.CookieContainer; + } + else + { + client.SetCustomCookieContainer(cookies); + } + webClients.Add(client); + } + //Add webClients to Queue again + foreach (var client in webClients) + { + this.Clients.Enqueue(client); + } + return true; + } + } +} diff --git a/CollectionManagerExtensionsDll/Properties/AssemblyInfo.cs b/CollectionManagerExtensionsDll/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..94bd151 --- /dev/null +++ b/CollectionManagerExtensionsDll/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CollectionManagerExtensionsDll")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CollectionManagerExtensionsDll")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2bdf5d5f-1cb0-47a6-8138-e4db961740f2")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CollectionManagerExtensionsDll/Utils/BeatmapUtils.cs b/CollectionManagerExtensionsDll/Utils/BeatmapUtils.cs new file mode 100644 index 0000000..642dd74 --- /dev/null +++ b/CollectionManagerExtensionsDll/Utils/BeatmapUtils.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.IO; +using CollectionManager.DataTypes; +using CollectionManagerExtensionsDll.Modules.CollectionListGenerator; +using CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes; + +namespace CollectionManagerExtensionsDll.Utils +{ + public static class BeatmapUtils + { + internal static Dictionary GetMapSets(this Collection collection, BeatmapListType beatmapListType) + { + var mapSets = new Dictionary(); + switch (beatmapListType) + { + case BeatmapListType.All: + mapSets = collection.GetBeatmapSets(); + break; + case BeatmapListType.NotKnown: + mapSets = CollectionUtils.GetBeatmapSets(collection.NotKnownBeatmaps()); + break; + case BeatmapListType.Known: + mapSets = CollectionUtils.GetBeatmapSets(collection.KnownBeatmaps); + break; + case BeatmapListType.Downloadable: + mapSets = CollectionUtils.GetBeatmapSets(collection.DownloadableBeatmaps); + break; + } + return mapSets; + } + + public static HashSet GetUniqueMapSetIds(this Beatmaps beatmaps, bool filterInvalidIds = true) + { + var mapIds = new HashSet(); + if (beatmaps?.Count > 0) + foreach (var beatmap in beatmaps) + { + if (!mapIds.Contains(beatmap.MapSetId)) + { + if (filterInvalidIds && beatmap.MapSetId < 2) + continue; + mapIds.Add(beatmap.MapSetId); + } + } + return mapIds; + } + + public static string OsuSongsDirectory = ""; + public static string GetImageLocation(this BeatmapExtension beatmap) + { + if (beatmap.LocalBeatmapMissing) + return string.Empty; + var osuFileLocation = beatmap.FullOsuFileLocation(); + string ImageLocation = string.Empty; + using (StreamReader file = new StreamReader(osuFileLocation)) + { + string line; + while ((line = file.ReadLine()) != null) + { + if (line.ToLower().Contains(".jpg") || line.ToLower().Contains(".png")) + { + var splited = line.Split(','); + ImageLocation = Path.Combine(beatmap.BeatmapDirectory(), splited[2].Trim('"')); + if (!File.Exists(ImageLocation)) + { + return string.Empty; + } + break; + } + } + } + return ImageLocation; + } + public static string BeatmapDirectory(this BeatmapExtension beatmap) + { + return Path.Combine(OsuSongsDirectory, beatmap.Dir); + } + public static string FullOsuFileLocation(this BeatmapExtension beatmap) + { + return Path.Combine(beatmap.BeatmapDirectory(), beatmap.OsuFileName); + } + public static string FullAudioFileLocation(this BeatmapExtension beatmap) + { + if (beatmap.LocalBeatmapMissing) + return string.Empty; + return Path.Combine(beatmap.BeatmapDirectory(), beatmap.Mp3Name); + + } + } +} diff --git a/CollectionManagerExtensionsDll/Utils/ListGeneratorUtils.cs b/CollectionManagerExtensionsDll/Utils/ListGeneratorUtils.cs new file mode 100644 index 0000000..e8211cf --- /dev/null +++ b/CollectionManagerExtensionsDll/Utils/ListGeneratorUtils.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using CollectionManager.DataTypes; + +namespace CollectionManagerExtensionsDll.Modules.CollectionListGenerator.ListTypes +{ + internal static class CollectionUtils + { + /// + /// Returns grouped beatmaps using MapSetId + /// + /// + /// Dictionary containing MapSetId as key and corresponding beatmaps in value. + /// beatmaps with invalid MapSetId (less than 1) are placed under key "-1" + /// + internal static Dictionary GetBeatmapSets(this Collection collection) + { + return GetBeatmapSets(collection.AllBeatmaps()); + } + + + internal static Dictionary GetBeatmapSets(IEnumerable collection) + { + var beatmapSets = new Dictionary(); + beatmapSets.Add(-1, new Beatmaps()); + + foreach (var map in collection) + { + if (map.MapSetId < 1) + { + beatmapSets[-1].Add(map); + } + else if (beatmapSets.ContainsKey(map.MapSetId)) + { + beatmapSets[map.MapSetId].Add(map); + } + else + { + beatmapSets.Add(map.MapSetId, new Beatmaps() { map }); + } + } + return beatmapSets; + } + } +} \ No newline at end of file diff --git a/CollectionManagerExtensionsDll/packages.config b/CollectionManagerExtensionsDll/packages.config new file mode 100644 index 0000000..4415b0d --- /dev/null +++ b/CollectionManagerExtensionsDll/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Common/Common.csproj b/Common/Common.csproj new file mode 100644 index 0000000..5ded26e --- /dev/null +++ b/Common/Common.csproj @@ -0,0 +1,96 @@ + + + + + Debug + AnyCPU + {14768636-102A-4A27-AB5A-9B5D1BA316A6} + Library + Properties + Common + Common + v4.0 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {533ab47a-d1b5-45db-a37e-f053fa3699c4} + CollectionManagerDll + + + {2bdf5d5f-1cb0-47a6-8138-e4db961740f2} + CollectionManagerExtensionsDll + + + {18feda0c-d147-4286-b39a-01204808106a} + ObjectListView2012 + + + + + \ No newline at end of file diff --git a/Common/EventArgs/EventArgs.cs b/Common/EventArgs/EventArgs.cs new file mode 100644 index 0000000..3432b63 --- /dev/null +++ b/Common/EventArgs/EventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace Gui.Misc +{ + public class EventArgs :EventArgs + { + public T Value { get; } + + public EventArgs(T value) + { + Value = value; + } + + } +} \ No newline at end of file diff --git a/Common/EventArgs/FloatEventArgs.cs b/Common/EventArgs/FloatEventArgs.cs new file mode 100644 index 0000000..63d1ef8 --- /dev/null +++ b/Common/EventArgs/FloatEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Gui.Misc +{ + public class FloatEventArgs : EventArgs + { + public FloatEventArgs(float value) + { + Value = value; + } + + public float Value { get; set; } + } +} \ No newline at end of file diff --git a/Common/EventArgs/IntEventArgs.cs b/Common/EventArgs/IntEventArgs.cs new file mode 100644 index 0000000..33515c0 --- /dev/null +++ b/Common/EventArgs/IntEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Gui.Misc +{ + public class IntEventArgs : EventArgs + { + public IntEventArgs(int value) + { + Value = value; + } + + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/Common/EventArgs/StringEventArgs.cs b/Common/EventArgs/StringEventArgs.cs new file mode 100644 index 0000000..2ba3a13 --- /dev/null +++ b/Common/EventArgs/StringEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Gui.Misc +{ + public class StringEventArgs : EventArgs + { + public StringEventArgs(string value) + { + Value = value; + } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/Common/GuiHelpers.cs b/Common/GuiHelpers.cs new file mode 100644 index 0000000..a734562 --- /dev/null +++ b/Common/GuiHelpers.cs @@ -0,0 +1,10 @@ +using CollectionManager.DataTypes; + +namespace Gui.Misc +{ + public static class GuiHelpers + { + public delegate void BeatmapsEventArgs(object sender, Beatmaps args); + public delegate void CollectionBeatmapsEventArgs(object sender, Beatmaps args, string collectionName); + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IBeatmapListingView.cs b/Common/Interfaces/Controls/IBeatmapListingView.cs new file mode 100644 index 0000000..e487813 --- /dev/null +++ b/Common/Interfaces/Controls/IBeatmapListingView.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using Gui.Misc; + +namespace GuiComponents.Interfaces +{ + public interface IBeatmapListingView + { + string SearchText { get; } + string ResultText { get; set; } + Beatmap SelectedBeatmap { get; } + Beatmaps SelectedBeatmaps { get; } + + event EventHandler SearchTextChanged; + event EventHandler SelectedBeatmapChanged; + event EventHandler SelectedBeatmapsChanged; + event EventHandler OpenBeatmapPages; + event EventHandler DownloadBeatmaps; + event EventHandler DownloadBeatmapsManaged; + event EventHandler DeleteBeatmapsFromCollection; + event GuiHelpers.BeatmapsEventArgs BeatmapsDropped; + + void SetBeatmaps(IEnumerable beatmaps); + void SetFilter(IModelFilter filter); + void FilteringStarted(); + void FilteringFinished(); + void ClearSelection(); + void SelectNextOrFirst(); + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IBeatmapThumbnailView.cs b/Common/Interfaces/Controls/IBeatmapThumbnailView.cs new file mode 100644 index 0000000..098979c --- /dev/null +++ b/Common/Interfaces/Controls/IBeatmapThumbnailView.cs @@ -0,0 +1,15 @@ +using System.Drawing; + +namespace GuiComponents.Interfaces +{ + public interface IBeatmapThumbnailView + { + Image beatmapImage { set; } + string AR { set; } + string CS { set; } + string OD { set; } + string Stars { set; } + string BeatmapName { set; } + + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICollectionAddView.cs b/Common/Interfaces/Controls/ICollectionAddView.cs new file mode 100644 index 0000000..fa8b876 --- /dev/null +++ b/Common/Interfaces/Controls/ICollectionAddView.cs @@ -0,0 +1,14 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface ICollectionAddView + { + event EventHandler CollectionNameChanged; + event EventHandler Submited; + event EventHandler Canceled; + string NewCollectionName { get; } + string ErrorText { set; } + bool CanSubmit { set; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICollectionListingView.cs b/Common/Interfaces/Controls/ICollectionListingView.cs new file mode 100644 index 0000000..c198bcd --- /dev/null +++ b/Common/Interfaces/Controls/ICollectionListingView.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using CollectionManager.Modules.CollectionsManager; +using Gui.Misc; + +namespace GuiComponents.Interfaces +{ + public interface ICollectionListingView + { + string SearchText { get; } + + Collections Collections { set; } + Collection SelectedCollection { get; set; } + ArrayList SelectedCollections { get; } + + event EventHandler SearchTextChanged; + event EventHandler SelectedCollectionChanged; + event EventHandler SelectedCollectionsChanged; + event GuiHelpers.CollectionBeatmapsEventArgs BeatmapsDropped; + event EventHandler RightClick; + + void SetFilter(IModelFilter filter); + void FilteringStarted(); + void FilteringFinished(); + //void OnCollectionEditing(CollectionEditArgs e); + + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICollectionRenameView.cs b/Common/Interfaces/Controls/ICollectionRenameView.cs new file mode 100644 index 0000000..04b8c42 --- /dev/null +++ b/Common/Interfaces/Controls/ICollectionRenameView.cs @@ -0,0 +1,9 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface ICollectionRenameView : ICollectionAddView + { + string OrginalCollectionName { get; set; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICollectionTextView.cs b/Common/Interfaces/Controls/ICollectionTextView.cs new file mode 100644 index 0000000..a070d62 --- /dev/null +++ b/Common/Interfaces/Controls/ICollectionTextView.cs @@ -0,0 +1,12 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface ICollectionTextView + { + string GeneratedText { set; } + string SelectedSaveType { get; } + event EventHandler SaveTypeChanged; + void SetListTypes(Array types); + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICombinedBeatmapPreviewView.cs b/Common/Interfaces/Controls/ICombinedBeatmapPreviewView.cs new file mode 100644 index 0000000..0bf02a1 --- /dev/null +++ b/Common/Interfaces/Controls/ICombinedBeatmapPreviewView.cs @@ -0,0 +1,8 @@ +namespace GuiComponents.Interfaces +{ + public interface ICombinedBeatmapPreviewView + { + IBeatmapThumbnailView BeatmapThumbnailView { get; } + IMusicControlView MusicControlView { get; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/ICombinedListingView.cs b/Common/Interfaces/Controls/ICombinedListingView.cs new file mode 100644 index 0000000..5f96ca9 --- /dev/null +++ b/Common/Interfaces/Controls/ICombinedListingView.cs @@ -0,0 +1,8 @@ +namespace GuiComponents.Interfaces +{ + public interface ICombinedListingView + { + IBeatmapListingView beatmapListingView { get; } + ICollectionListingView CollectionListingView { get; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IDownloadManagerView.cs b/Common/Interfaces/Controls/IDownloadManagerView.cs new file mode 100644 index 0000000..f6ae087 --- /dev/null +++ b/Common/Interfaces/Controls/IDownloadManagerView.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; + +namespace GuiComponents.Interfaces +{ + public interface IDownloadManagerView + { + event EventHandler DownloadToggleClick; + event EventHandler Disposed; + + bool DownloadButtonIsEnabled { set; } + string DownloadButtonText { set; } + void SetDownloadItems(ICollection downloadItems); + void UpdateDownloadItem(DownloadItem downloadItem); + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IInfoTextView.cs b/Common/Interfaces/Controls/IInfoTextView.cs new file mode 100644 index 0000000..7531dba --- /dev/null +++ b/Common/Interfaces/Controls/IInfoTextView.cs @@ -0,0 +1,16 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface IInfoTextView + { + bool UpdateTextIsClickable { set; } + string UpdateText { set; } + string BeatmapLoaded { set; } + string CollectionsLoaded { set; } + string BeatmapsInCollections { set; } + string BeatmapsMissing { set; } + + event EventHandler UpdateTextClicked; + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IMainSidePanelView.cs b/Common/Interfaces/Controls/IMainSidePanelView.cs new file mode 100644 index 0000000..9442fda --- /dev/null +++ b/Common/Interfaces/Controls/IMainSidePanelView.cs @@ -0,0 +1,19 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface IMainSidePanelView + { + event EventHandler LoadCollection; + event EventHandler LoadDefaultCollection; + event EventHandler ClearCollections; + event EventHandler SaveCollections; + event EventHandler SaveInvidualCollections; + event EventHandler ListAllMaps; + event EventHandler ListMissingMaps; + event EventHandler ShowBeatmapListing; + event EventHandler ShowDownloadManager; + event EventHandler DownloadAllMissing; + + } +} \ No newline at end of file diff --git a/Common/Interfaces/Controls/IMusicControlView.cs b/Common/Interfaces/Controls/IMusicControlView.cs new file mode 100644 index 0000000..08ae7d7 --- /dev/null +++ b/Common/Interfaces/Controls/IMusicControlView.cs @@ -0,0 +1,25 @@ +using System; +using Gui.Misc; + +namespace GuiComponents.Interfaces +{ + public interface IMusicControlView + { + event EventHandler PositionChanged; + event EventHandler CheckboxChanged; + event EventHandler PlayPressed; + event EventHandler PausePressed; + event EventHandler VolumeChanged; + event EventHandler Disposed; + + float Volume { get; } + int Position { get; set; } + bool IsMusicPlayerMode { get; } + bool IsAutoPlayEnabled { get; } + bool IsDTEnabled { get; } + bool IsUserSeeking { get; } + + void disableDt(); + + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/IBeatmapListingForm.cs b/Common/Interfaces/Forms/IBeatmapListingForm.cs new file mode 100644 index 0000000..0616690 --- /dev/null +++ b/Common/Interfaces/Forms/IBeatmapListingForm.cs @@ -0,0 +1,8 @@ +namespace GuiComponents.Interfaces +{ + public interface IBeatmapListingForm : IForm + { + IBeatmapListingView BeatmapListingView { get; } + ICombinedBeatmapPreviewView CombinedBeatmapPreviewView { get; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/ICollectionAddRenameForm.cs b/Common/Interfaces/Forms/ICollectionAddRenameForm.cs new file mode 100644 index 0000000..d98f9bb --- /dev/null +++ b/Common/Interfaces/Forms/ICollectionAddRenameForm.cs @@ -0,0 +1,8 @@ +namespace GuiComponents.Interfaces +{ + public interface ICollectionAddRenameForm :IForm + { + ICollectionRenameView CollectionRenameView { get; } + bool IsRenameForm { get; set; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/IDownloadManagerFormView.cs b/Common/Interfaces/Forms/IDownloadManagerFormView.cs new file mode 100644 index 0000000..7fef7b2 --- /dev/null +++ b/Common/Interfaces/Forms/IDownloadManagerFormView.cs @@ -0,0 +1,7 @@ +namespace GuiComponents.Interfaces +{ + public interface IDownloadManagerFormView : IForm + { + IDownloadManagerView DownloadManagerView { get; } + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/IForm.cs b/Common/Interfaces/Forms/IForm.cs new file mode 100644 index 0000000..94f52be --- /dev/null +++ b/Common/Interfaces/Forms/IForm.cs @@ -0,0 +1,14 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface IForm + { + bool IsDisposed { get; } + void ShowAndBlock(); + void Show(); + void Close(); + event EventHandler Disposed; + event EventHandler Closing; + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/ILoginForm.cs b/Common/Interfaces/Forms/ILoginForm.cs new file mode 100644 index 0000000..4796e96 --- /dev/null +++ b/Common/Interfaces/Forms/ILoginForm.cs @@ -0,0 +1,13 @@ +using System; + +namespace GuiComponents.Interfaces +{ + public interface ILoginFormView :IForm + { + string Login { get; } + string Password { get; } + bool ClickedLogin { get; } + event EventHandler LoginClick; + event EventHandler CancelClick; + } +} \ No newline at end of file diff --git a/Common/Interfaces/Forms/IMainFormView.cs b/Common/Interfaces/Forms/IMainFormView.cs new file mode 100644 index 0000000..27bd62f --- /dev/null +++ b/Common/Interfaces/Forms/IMainFormView.cs @@ -0,0 +1,12 @@ +namespace GuiComponents.Interfaces +{ + public interface IMainFormView :IForm + { + ICombinedListingView CombinedListingView { get; } + ICombinedBeatmapPreviewView CombinedBeatmapPreviewView { get; } + IMainSidePanelView SidePanelView { get; } + ICollectionTextView CollectionTextView { get; } + IInfoTextView InfoTextView { get; } + + } +} \ No newline at end of file diff --git a/Common/Interfaces/IUserDialogs.cs b/Common/Interfaces/IUserDialogs.cs new file mode 100644 index 0000000..42ed9c7 --- /dev/null +++ b/Common/Interfaces/IUserDialogs.cs @@ -0,0 +1,15 @@ +using Common; + +namespace GuiComponents.Interfaces +{ + public interface IUserDialogs + { + bool IsThisPathCorrect(string path); + string SelectDirectory(string text); + string SelectDirectory(string text, bool showNewFolder = false); + string SelectFile(string text, string types = "", string filename = ""); + string SaveFile(string title, string types = "all|*.*"); + bool YesNoMessageBox(string text, string caption, MessageBoxType messageBoxType = MessageBoxType.Info); + void OkMessageBox(string text, string caption, MessageBoxType messageBoxType = MessageBoxType.Info); + } +} \ No newline at end of file diff --git a/Common/MessageBoxType.cs b/Common/MessageBoxType.cs new file mode 100644 index 0000000..94f0f2d --- /dev/null +++ b/Common/MessageBoxType.cs @@ -0,0 +1,7 @@ +namespace Common +{ + public enum MessageBoxType + { + Info, Question, Error, Success + } +} \ No newline at end of file diff --git a/Common/Properties/AssemblyInfo.cs b/Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5416fb4 --- /dev/null +++ b/Common/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Common")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("14768636-102a-4a27-ab5a-9b5d1ba316a6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GuiComponents/Controls/BeatmapListingView.Designer.cs b/GuiComponents/Controls/BeatmapListingView.Designer.cs new file mode 100644 index 0000000..1a9316a --- /dev/null +++ b/GuiComponents/Controls/BeatmapListingView.Designer.cs @@ -0,0 +1,333 @@ +namespace GuiComponents.Controls +{ + partial class BeatmapListingView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.ListViewBeatmaps = new BrightIdeasSoftware.FastDataListView(); + this.olvColumn2 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn4 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn1 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn5 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn6 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn7 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn8 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn9 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.label_resultsCount = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.textBox_beatmapSearch = new System.Windows.Forms.TextBox(); + this.BeatmapsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.OpenDlMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.OpenBeatmapPageMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.OpenBeatmapDownloadMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.DownloadMapManagedMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.DownloadMapInBrowserMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.DeleteMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.searchToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SearchMapsetMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.SearchArtistMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.SearchTitleMapMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.olvColumn3 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn10 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + ((System.ComponentModel.ISupportInitialize)(this.ListViewBeatmaps)).BeginInit(); + this.BeatmapsContextMenuStrip.SuspendLayout(); + this.SuspendLayout(); + // + // ListViewBeatmaps + // + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn2); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn4); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn3); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn10); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn1); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn5); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn6); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn7); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn8); + this.ListViewBeatmaps.AllColumns.Add(this.olvColumn9); + this.ListViewBeatmaps.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ListViewBeatmaps.AutoGenerateColumns = false; + this.ListViewBeatmaps.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.olvColumn2, + this.olvColumn4, + this.olvColumn3, + this.olvColumn10, + this.olvColumn1, + this.olvColumn5, + this.olvColumn6, + this.olvColumn7, + this.olvColumn8, + this.olvColumn9}); + this.ListViewBeatmaps.DataSource = null; + this.ListViewBeatmaps.EmptyListMsg = "No collection selected"; + this.ListViewBeatmaps.HideSelection = false; + this.ListViewBeatmaps.IsSimpleDragSource = true; + this.ListViewBeatmaps.IsSimpleDropSink = true; + this.ListViewBeatmaps.Location = new System.Drawing.Point(3, 41); + this.ListViewBeatmaps.Name = "ListViewBeatmaps"; + this.ListViewBeatmaps.ShowGroups = false; + this.ListViewBeatmaps.Size = new System.Drawing.Size(631, 461); + this.ListViewBeatmaps.TabIndex = 14; + this.ListViewBeatmaps.UnfocusedHighlightBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); + this.ListViewBeatmaps.UseCompatibleStateImageBehavior = false; + this.ListViewBeatmaps.UseCustomSelectionColors = true; + this.ListViewBeatmaps.View = System.Windows.Forms.View.Details; + this.ListViewBeatmaps.VirtualMode = true; + // + // olvColumn2 + // + this.olvColumn2.AspectName = "Name"; + this.olvColumn2.AspectToStringFormat = ""; + this.olvColumn2.Text = "Name"; + this.olvColumn2.Width = 200; + // + // olvColumn4 + // + this.olvColumn4.AspectName = "DiffName"; + this.olvColumn4.Text = "Difficulty"; + this.olvColumn4.TextCopyFormat = "[{0}]"; + this.olvColumn4.Width = 100; + // + // olvColumn1 + // + this.olvColumn1.AspectName = "StarsNomod"; + this.olvColumn1.Text = "★"; + this.olvColumn1.TextCopyFormat = "{0}★"; + this.olvColumn1.Width = 30; + // + // olvColumn5 + // + this.olvColumn5.AspectName = "StateStr"; + this.olvColumn5.Text = "State"; + // + // olvColumn6 + // + this.olvColumn6.AspectName = "ApproachRate"; + this.olvColumn6.Text = "AR"; + this.olvColumn6.Width = 30; + // + // olvColumn7 + // + this.olvColumn7.AspectName = "CircleSize"; + this.olvColumn7.Text = "CS"; + this.olvColumn7.Width = 30; + // + // olvColumn8 + // + this.olvColumn8.AspectName = "HpDrainRate"; + this.olvColumn8.Text = "HP"; + this.olvColumn8.Width = 30; + // + // olvColumn9 + // + this.olvColumn9.AspectName = "OverallDifficulty"; + this.olvColumn9.Text = "OD"; + this.olvColumn9.Width = 30; + // + // label_resultsCount + // + this.label_resultsCount.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.label_resultsCount.AutoSize = true; + this.label_resultsCount.Location = new System.Drawing.Point(524, 0); + this.label_resultsCount.Name = "label_resultsCount"; + this.label_resultsCount.Size = new System.Drawing.Size(41, 13); + this.label_resultsCount.TabIndex = 17; + this.label_resultsCount.Text = "0 maps"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, -1); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(151, 13); + this.label1.TabIndex = 16; + this.label1.Text = "Search just like you do in osu!:"; + // + // textBox_beatmapSearch + // + this.textBox_beatmapSearch.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBox_beatmapSearch.Location = new System.Drawing.Point(3, 15); + this.textBox_beatmapSearch.Name = "textBox_beatmapSearch"; + this.textBox_beatmapSearch.Size = new System.Drawing.Size(631, 20); + this.textBox_beatmapSearch.TabIndex = 15; + // + // BeatmapsContextMenuStrip + // + this.BeatmapsContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.OpenDlMapMenuStrip, + this.DeleteMapMenuStrip, + this.searchToolStripMenuItem}); + this.BeatmapsContextMenuStrip.Name = "CollectionContextMenuStrip"; + this.BeatmapsContextMenuStrip.Size = new System.Drawing.Size(132, 70); + // + // OpenDlMapMenuStrip + // + this.OpenDlMapMenuStrip.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.OpenBeatmapPageMapMenuStrip, + this.OpenBeatmapDownloadMapMenuStrip}); + this.OpenDlMapMenuStrip.Name = "OpenDlMapMenuStrip"; + this.OpenDlMapMenuStrip.Size = new System.Drawing.Size(131, 22); + this.OpenDlMapMenuStrip.Text = "Open"; + // + // OpenBeatmapPageMapMenuStrip + // + this.OpenBeatmapPageMapMenuStrip.Name = "OpenBeatmapPageMapMenuStrip"; + this.OpenBeatmapPageMapMenuStrip.Size = new System.Drawing.Size(191, 22); + this.OpenBeatmapPageMapMenuStrip.Text = "Beatmap page(s)"; + this.OpenBeatmapPageMapMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // OpenBeatmapDownloadMapMenuStrip + // + this.OpenBeatmapDownloadMapMenuStrip.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.DownloadMapManagedMenuStrip, + this.DownloadMapInBrowserMenuStrip}); + this.OpenBeatmapDownloadMapMenuStrip.Name = "OpenBeatmapDownloadMapMenuStrip"; + this.OpenBeatmapDownloadMapMenuStrip.Size = new System.Drawing.Size(191, 22); + this.OpenBeatmapDownloadMapMenuStrip.Text = "Download beatmap(s)"; + // + // DownloadMapManagedMenuStrip + // + this.DownloadMapManagedMenuStrip.Name = "DownloadMapManagedMenuStrip"; + this.DownloadMapManagedMenuStrip.Size = new System.Drawing.Size(129, 22); + this.DownloadMapManagedMenuStrip.Text = "Managed"; + this.DownloadMapManagedMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // DownloadMapInBrowserMenuStrip + // + this.DownloadMapInBrowserMenuStrip.Name = "DownloadMapInBrowserMenuStrip"; + this.DownloadMapInBrowserMenuStrip.Size = new System.Drawing.Size(129, 22); + this.DownloadMapInBrowserMenuStrip.Text = "In browser"; + this.DownloadMapInBrowserMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // DeleteMapMenuStrip + // + this.DeleteMapMenuStrip.Enabled = false; + this.DeleteMapMenuStrip.Name = "DeleteMapMenuStrip"; + this.DeleteMapMenuStrip.ShortcutKeys = System.Windows.Forms.Keys.Delete; + this.DeleteMapMenuStrip.Size = new System.Drawing.Size(131, 22); + this.DeleteMapMenuStrip.Text = "Delete"; + this.DeleteMapMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // searchToolStripMenuItem + // + this.searchToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.SearchMapsetMapMenuStrip, + this.SearchArtistMapMenuStrip, + this.SearchTitleMapMenuStrip}); + this.searchToolStripMenuItem.Enabled = false; + this.searchToolStripMenuItem.Name = "searchToolStripMenuItem"; + this.searchToolStripMenuItem.Size = new System.Drawing.Size(131, 22); + this.searchToolStripMenuItem.Text = "Search"; + // + // SearchMapsetMapMenuStrip + // + this.SearchMapsetMapMenuStrip.Name = "SearchMapsetMapMenuStrip"; + this.SearchMapsetMapMenuStrip.Size = new System.Drawing.Size(113, 22); + this.SearchMapsetMapMenuStrip.Text = "mapset"; + // + // SearchArtistMapMenuStrip + // + this.SearchArtistMapMenuStrip.Name = "SearchArtistMapMenuStrip"; + this.SearchArtistMapMenuStrip.Size = new System.Drawing.Size(113, 22); + this.SearchArtistMapMenuStrip.Text = "artist"; + // + // SearchTitleMapMenuStrip + // + this.SearchTitleMapMenuStrip.Name = "SearchTitleMapMenuStrip"; + this.SearchTitleMapMenuStrip.Size = new System.Drawing.Size(113, 22); + this.SearchTitleMapMenuStrip.Text = "title"; + // + // olvColumn3 + // + this.olvColumn3.AspectName = "LocalBeatmapMissing"; + this.olvColumn3.CheckBoxes = true; + this.olvColumn3.IsEditable = false; + this.olvColumn3.MaximumWidth = 35; + this.olvColumn3.MinimumWidth = 35; + this.olvColumn3.Text = "N/A"; + this.olvColumn3.Width = 35; + // + // olvColumn10 + // + this.olvColumn10.AspectName = "LocalVersionDiffers"; + this.olvColumn10.CheckBoxes = true; + this.olvColumn10.IsEditable = false; + this.olvColumn10.MaximumWidth = 35; + this.olvColumn10.MinimumWidth = 35; + this.olvColumn10.Text = "N/U"; + this.olvColumn10.Width = 35; + // + // BeatmapListingView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.ListViewBeatmaps); + this.Controls.Add(this.label_resultsCount); + this.Controls.Add(this.label1); + this.Controls.Add(this.textBox_beatmapSearch); + this.Name = "BeatmapListingView"; + this.Size = new System.Drawing.Size(634, 502); + ((System.ComponentModel.ISupportInitialize)(this.ListViewBeatmaps)).EndInit(); + this.BeatmapsContextMenuStrip.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + public BrightIdeasSoftware.FastDataListView ListViewBeatmaps; + private BrightIdeasSoftware.OLVColumn olvColumn2; + private BrightIdeasSoftware.OLVColumn olvColumn4; + private BrightIdeasSoftware.OLVColumn olvColumn1; + private BrightIdeasSoftware.OLVColumn olvColumn5; + private BrightIdeasSoftware.OLVColumn olvColumn6; + private BrightIdeasSoftware.OLVColumn olvColumn7; + private BrightIdeasSoftware.OLVColumn olvColumn8; + private BrightIdeasSoftware.OLVColumn olvColumn9; + public System.Windows.Forms.Label label_resultsCount; + private System.Windows.Forms.Label label1; + public System.Windows.Forms.TextBox textBox_beatmapSearch; + private System.Windows.Forms.ContextMenuStrip BeatmapsContextMenuStrip; + private System.Windows.Forms.ToolStripMenuItem OpenDlMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem OpenBeatmapPageMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem OpenBeatmapDownloadMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem DownloadMapManagedMenuStrip; + private System.Windows.Forms.ToolStripMenuItem DownloadMapInBrowserMenuStrip; + private System.Windows.Forms.ToolStripMenuItem DeleteMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem searchToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem SearchMapsetMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem SearchArtistMapMenuStrip; + private System.Windows.Forms.ToolStripMenuItem SearchTitleMapMenuStrip; + private BrightIdeasSoftware.OLVColumn olvColumn3; + private BrightIdeasSoftware.OLVColumn olvColumn10; + } +} diff --git a/GuiComponents/Controls/BeatmapListingView.cs b/GuiComponents/Controls/BeatmapListingView.cs new file mode 100644 index 0000000..f25fdd5 --- /dev/null +++ b/GuiComponents/Controls/BeatmapListingView.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using GuiComponents.Interfaces; +using Gui.Misc; + +namespace GuiComponents.Controls +{ + public partial class BeatmapListingView : UserControl, IBeatmapListingView + { + public event EventHandler SearchTextChanged; + public event EventHandler SelectedBeatmapChanged; + public event EventHandler SelectedBeatmapsChanged; + + public event EventHandler OpenBeatmapPages; + public event EventHandler DownloadBeatmaps; + public event EventHandler DownloadBeatmapsManaged; + public event EventHandler DeleteBeatmapsFromCollection; + public event GuiHelpers.BeatmapsEventArgs BeatmapsDropped; + + public string SearchText => textBox_beatmapSearch.Text; + public string ResultText { get; set; } + + private bool _allowForDeletion = false; + + [Description("Should user be able to delete beatmaps from the list?"), Category("Layout")] + public bool AllowForDeletion + { + get { return _allowForDeletion; } + set + { + _allowForDeletion = value; + DeleteMapMenuStrip.Enabled = value; + } + } + + + public void SetBeatmaps(IEnumerable beatmaps) + { + ListViewBeatmaps.SetObjects(beatmaps); + UpdateResultsCount(); + } + public Beatmap SelectedBeatmap => (Beatmap)ListViewBeatmaps.SelectedObject; + + public Beatmaps SelectedBeatmaps + { + get + { + var beatmaps = new Beatmaps(); + if (ListViewBeatmaps.SelectedObjects.Count > 0) + foreach (var o in ListViewBeatmaps.SelectedObjects) + { + beatmaps.Add((BeatmapExtension)o); + } + return beatmaps; + } + } + + public BeatmapListingView() + { + InitializeComponent(); + InitListView(); + Bind(); + } + private void Bind() + { + textBox_beatmapSearch.TextChanged += delegate + { + OnSearchTextChanged(); + }; + ListViewBeatmaps.SelectionChanged += delegate + { + OnSelectedBeatmapChanged(); + OnSelectedBeatmapsChanged(); + }; + ListViewBeatmaps.CellRightClick += delegate (object s, CellRightClickEventArgs args) + { + args.MenuStrip = BeatmapsContextMenuStrip; + }; + } + private void UpdateResultsCount() + { + int count = 0; + foreach (var b in ListViewBeatmaps.FilteredObjects) + { + count++; + } + label_resultsCount.Text = string.Format("{0} {1}", count, count == 1 ? "map" : "maps"); + } + private void InitListView() + { + //listview + ListViewBeatmaps.FullRowSelect = true; + ListViewBeatmaps.AllowColumnReorder = true; + ListViewBeatmaps.Sorting = SortOrder.Descending; + ListViewBeatmaps.UseHotItem = true; + ListViewBeatmaps.UseTranslucentHotItem = true; + ListViewBeatmaps.UseFiltering = true; + ListViewBeatmaps.UseNotifyPropertyChanged = true; + ListViewBeatmaps.ShowItemCountOnGroups = true; + + var dropsink = new RearrangingDropSink(); + dropsink.CanDropBetween = false; + dropsink.CanDropOnItem = false; + dropsink.CanDropOnSubItem = false; + dropsink.CanDropOnBackground = true; + dropsink.ModelDropped += DropsinkOnModelDropped; + this.ListViewBeatmaps.DropSink = dropsink; + + } + + + private void DropsinkOnModelDropped(object sender, ModelDropEventArgs modelDropEventArgs) + { + modelDropEventArgs.Handled = true; + var beatmaps = new Beatmaps(); + foreach (var sourceModel in modelDropEventArgs.SourceModels) + { + beatmaps.Add((BeatmapExtension)sourceModel); + } + BeatmapsDropped?.Invoke(this, beatmaps); + } + + public void SetFilter(IModelFilter filter) + { + ListViewBeatmaps.AdditionalFilter = filter; + } + + public void FilteringStarted() + { + ListViewBeatmaps.BeginUpdate(); + } + + public void FilteringFinished() + { + ListViewBeatmaps.UpdateColumnFiltering(); + ListViewBeatmaps.EndUpdate(); + } + + public void ClearSelection() + { + ListViewBeatmaps.SelectedIndex = -1; + } + + public void SelectNextOrFirst() + { + ListViewBeatmaps.SelectNextOrFirst(); + } + + protected virtual void OnSearchTextChanged() + { + SearchTextChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSelectedBeatmapsChanged() + { + SelectedBeatmapsChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSelectedBeatmapChanged() + { + SelectedBeatmapChanged?.Invoke(this, EventArgs.Empty); + } + + private void MenuStripClick(object sender, EventArgs e) + { + if (sender == DeleteMapMenuStrip) + DeleteBeatmapsFromCollection?.Invoke(this, EventArgs.Empty); + else if (sender == DownloadMapInBrowserMenuStrip) + DownloadBeatmaps?.Invoke(this, EventArgs.Empty); + else if (sender == DownloadMapManagedMenuStrip) + DownloadBeatmapsManaged?.Invoke(this, EventArgs.Empty); + else if (sender == OpenBeatmapPageMapMenuStrip) + OpenBeatmapPages?.Invoke(this, EventArgs.Empty); + } + } + + +} diff --git a/GuiComponents/Controls/BeatmapListingView.resx b/GuiComponents/Controls/BeatmapListingView.resx new file mode 100644 index 0000000..94e1f7e --- /dev/null +++ b/GuiComponents/Controls/BeatmapListingView.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/GuiComponents/Controls/BeatmapThumbnailView.Designer.cs b/GuiComponents/Controls/BeatmapThumbnailView.Designer.cs new file mode 100644 index 0000000..b6a9608 --- /dev/null +++ b/GuiComponents/Controls/BeatmapThumbnailView.Designer.cs @@ -0,0 +1,196 @@ +namespace GuiComponents.Controls +{ + partial class BeatmapThumbnailView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.label3 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.label_Stars = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.label_OD = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.label_CS = new System.Windows.Forms.Label(); + this.label_AR = new System.Windows.Forms.Label(); + this.panel1 = new System.Windows.Forms.Panel(); + this.label_BeatmapName = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + this.panel1.SuspendLayout(); + this.SuspendLayout(); + // + // pictureBox1 + // + this.pictureBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.pictureBox1.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch; + this.pictureBox1.Location = new System.Drawing.Point(3, 0); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(281, 183); + this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.pictureBox1.TabIndex = 13; + this.pictureBox1.TabStop = false; + // + // label3 + // + this.label3.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(3, 50); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(26, 13); + this.label3.TabIndex = 3; + this.label3.Text = "OD:"; + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, 27); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(28, 13); + this.label1.TabIndex = 1; + this.label1.Text = "AR: "; + // + // label_Stars + // + this.label_Stars.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label_Stars.AutoSize = true; + this.label_Stars.Location = new System.Drawing.Point(120, 50); + this.label_Stars.Name = "label_Stars"; + this.label_Stars.Size = new System.Drawing.Size(31, 13); + this.label_Stars.TabIndex = 8; + this.label_Stars.Text = " "; + // + // label2 + // + this.label2.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(90, 27); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(24, 13); + this.label2.TabIndex = 2; + this.label2.Text = "CS:"; + // + // label_OD + // + this.label_OD.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label_OD.AutoSize = true; + this.label_OD.Location = new System.Drawing.Point(35, 50); + this.label_OD.Name = "label_OD"; + this.label_OD.Size = new System.Drawing.Size(31, 13); + this.label_OD.TabIndex = 7; + this.label_OD.Text = " "; + // + // label4 + // + this.label4.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(90, 50); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(34, 13); + this.label4.TabIndex = 4; + this.label4.Text = "Stars:"; + // + // label_CS + // + this.label_CS.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label_CS.AutoSize = true; + this.label_CS.Location = new System.Drawing.Point(120, 27); + this.label_CS.Name = "label_CS"; + this.label_CS.Size = new System.Drawing.Size(31, 13); + this.label_CS.TabIndex = 6; + this.label_CS.Text = " "; + // + // label_AR + // + this.label_AR.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label_AR.AutoSize = true; + this.label_AR.Location = new System.Drawing.Point(37, 27); + this.label_AR.Name = "label_AR"; + this.label_AR.Size = new System.Drawing.Size(28, 13); + this.label_AR.TabIndex = 5; + this.label_AR.Text = " "; + // + // panel1 + // + this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel1.Controls.Add(this.label_BeatmapName); + this.panel1.Controls.Add(this.label3); + this.panel1.Controls.Add(this.label1); + this.panel1.Controls.Add(this.label_Stars); + this.panel1.Controls.Add(this.label2); + this.panel1.Controls.Add(this.label_OD); + this.panel1.Controls.Add(this.label4); + this.panel1.Controls.Add(this.label_CS); + this.panel1.Controls.Add(this.label_AR); + this.panel1.Location = new System.Drawing.Point(3, 183); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(281, 65); + this.panel1.TabIndex = 14; + // + // label_BeatmapName + // + this.label_BeatmapName.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.label_BeatmapName.AutoSize = true; + this.label_BeatmapName.Location = new System.Drawing.Point(3, 6); + this.label_BeatmapName.Name = "label_BeatmapName"; + this.label_BeatmapName.Size = new System.Drawing.Size(100, 13); + this.label_BeatmapName.TabIndex = 9; + this.label_BeatmapName.Text = " "; + // + // BeatmapThumbnailView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.pictureBox1); + this.Controls.Add(this.panel1); + this.Name = "BeatmapThumbnailView"; + this.Size = new System.Drawing.Size(284, 248); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label_Stars; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label_OD; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label_CS; + private System.Windows.Forms.Label label_AR; + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.Label label_BeatmapName; + } +} diff --git a/GuiComponents/Controls/BeatmapThumbnailView.cs b/GuiComponents/Controls/BeatmapThumbnailView.cs new file mode 100644 index 0000000..64521cc --- /dev/null +++ b/GuiComponents/Controls/BeatmapThumbnailView.cs @@ -0,0 +1,30 @@ +using System.Drawing; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class BeatmapThumbnailView : UserControl, IBeatmapThumbnailView + { + public BeatmapThumbnailView() + { + InitializeComponent(); + } + + public Image beatmapImage + { + set + { + var oldImage = pictureBox1.Image; + pictureBox1.Image = value; + oldImage?.Dispose(); + } + } + + public string AR { set { Invoke((MethodInvoker)(() => { label_AR.Text = value; })); } } + public string CS { set { Invoke((MethodInvoker)(() => { label_CS.Text = value; })); } } + public string OD { set { Invoke((MethodInvoker)(() => { label_OD.Text = value; })); } } + public string Stars { set { Invoke((MethodInvoker)(() => { label_Stars.Text = value; })); } } + public string BeatmapName { set { Invoke((MethodInvoker)(() => { label_BeatmapName.Text = value; })); } } + } +} diff --git a/GuiComponents/Controls/BeatmapThumbnailView.resx b/GuiComponents/Controls/BeatmapThumbnailView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/BeatmapThumbnailView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/CollectionListingView.Designer.cs b/GuiComponents/Controls/CollectionListingView.Designer.cs new file mode 100644 index 0000000..0c1368a --- /dev/null +++ b/GuiComponents/Controls/CollectionListingView.Designer.cs @@ -0,0 +1,192 @@ +namespace GuiComponents.Controls +{ + partial class CollectionListingView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.panel1 = new System.Windows.Forms.Panel(); + this.label1 = new System.Windows.Forms.Label(); + this.ListViewCollections = new BrightIdeasSoftware.FastObjectListView(); + this.olvColumn1 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.Total = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn2 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.textBox_collectionNameSearch = new System.Windows.Forms.TextBox(); + this.CollectionContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.renameCollectionMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.deleteCollectionMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.mergeWithMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.CreateMenuStrip = new System.Windows.Forms.ToolStripMenuItem(); + this.panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.ListViewCollections)).BeginInit(); + this.CollectionContextMenuStrip.SuspendLayout(); + this.SuspendLayout(); + // + // panel1 + // + this.panel1.Controls.Add(this.label1); + this.panel1.Controls.Add(this.ListViewCollections); + this.panel1.Controls.Add(this.textBox_collectionNameSearch); + this.panel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.panel1.Location = new System.Drawing.Point(0, 0); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(464, 384); + this.panel1.TabIndex = 8; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(-2, 0); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(154, 13); + this.label1.TabIndex = 2; + this.label1.Text = "Search using collection names:"; + // + // ListViewCollections + // + this.ListViewCollections.AllColumns.Add(this.olvColumn1); + this.ListViewCollections.AllColumns.Add(this.Total); + this.ListViewCollections.AllColumns.Add(this.olvColumn2); + this.ListViewCollections.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ListViewCollections.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.olvColumn1, + this.Total, + this.olvColumn2}); + this.ListViewCollections.EmptyListMsg = "No collections loaded"; + this.ListViewCollections.Location = new System.Drawing.Point(1, 39); + this.ListViewCollections.Name = "ListViewCollections"; + this.ListViewCollections.ShowGroups = false; + this.ListViewCollections.Size = new System.Drawing.Size(463, 345); + this.ListViewCollections.TabIndex = 5; + this.ListViewCollections.UnfocusedHighlightBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); + this.ListViewCollections.UseCompatibleStateImageBehavior = false; + this.ListViewCollections.UseCustomSelectionColors = true; + this.ListViewCollections.View = System.Windows.Forms.View.Details; + this.ListViewCollections.VirtualMode = true; + // + // olvColumn1 + // + this.olvColumn1.AspectName = "Name"; + this.olvColumn1.MaximumWidth = 400; + this.olvColumn1.MinimumWidth = 20; + this.olvColumn1.Text = "Name"; + this.olvColumn1.Width = 100; + // + // Total + // + this.Total.AspectName = "NumberOfBeatmaps"; + this.Total.Text = "Count"; + // + // olvColumn2 + // + this.olvColumn2.AspectName = "NumberOfMissingBeatmaps"; + this.olvColumn2.Text = "Missing"; + // + // textBox_collectionNameSearch + // + this.textBox_collectionNameSearch.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBox_collectionNameSearch.Location = new System.Drawing.Point(1, 16); + this.textBox_collectionNameSearch.Name = "textBox_collectionNameSearch"; + this.textBox_collectionNameSearch.Size = new System.Drawing.Size(463, 20); + this.textBox_collectionNameSearch.TabIndex = 1; + // + // CollectionContextMenuStrip + // + this.CollectionContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.renameCollectionMenuStrip, + this.deleteCollectionMenuStrip, + this.mergeWithMenuStrip, + this.CreateMenuStrip}); + this.CollectionContextMenuStrip.Name = "CollectionContextMenuStrip"; + this.CollectionContextMenuStrip.Size = new System.Drawing.Size(155, 114); + // + // renameCollectionMenuStrip + // + this.renameCollectionMenuStrip.Name = "renameCollectionMenuStrip"; + this.renameCollectionMenuStrip.Size = new System.Drawing.Size(154, 22); + this.renameCollectionMenuStrip.Tag = "Rename"; + this.renameCollectionMenuStrip.Text = "Rename"; + this.renameCollectionMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // deleteCollectionMenuStrip + // + this.deleteCollectionMenuStrip.Name = "deleteCollectionMenuStrip"; + this.deleteCollectionMenuStrip.Size = new System.Drawing.Size(154, 22); + this.deleteCollectionMenuStrip.Tag = "Delete"; + this.deleteCollectionMenuStrip.Text = "Delete"; + this.deleteCollectionMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // mergeWithMenuStrip + // + this.mergeWithMenuStrip.Name = "mergeWithMenuStrip"; + this.mergeWithMenuStrip.Size = new System.Drawing.Size(154, 22); + this.mergeWithMenuStrip.Tag = "Merge"; + this.mergeWithMenuStrip.Text = "Merge selected"; + this.mergeWithMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // CreateMenuStrip + // + this.CreateMenuStrip.Name = "CreateMenuStrip"; + this.CreateMenuStrip.Size = new System.Drawing.Size(154, 22); + this.CreateMenuStrip.Tag = "Create"; + this.CreateMenuStrip.Text = "Create"; + this.CreateMenuStrip.Click += new System.EventHandler(this.MenuStripClick); + // + // CollectionListingView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.panel1); + this.Name = "CollectionListingView"; + this.Size = new System.Drawing.Size(464, 384); + this.panel1.ResumeLayout(false); + this.panel1.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.ListViewCollections)).EndInit(); + this.CollectionContextMenuStrip.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Panel panel1; + private System.Windows.Forms.Label label1; + private BrightIdeasSoftware.FastObjectListView ListViewCollections; + private BrightIdeasSoftware.OLVColumn olvColumn1; + private BrightIdeasSoftware.OLVColumn olvColumn2; + private System.Windows.Forms.TextBox textBox_collectionNameSearch; + private System.Windows.Forms.ContextMenuStrip CollectionContextMenuStrip; + private System.Windows.Forms.ToolStripMenuItem renameCollectionMenuStrip; + private System.Windows.Forms.ToolStripMenuItem deleteCollectionMenuStrip; + private System.Windows.Forms.ToolStripMenuItem mergeWithMenuStrip; + private System.Windows.Forms.ToolStripMenuItem CreateMenuStrip; + private BrightIdeasSoftware.OLVColumn Total; + } +} diff --git a/GuiComponents/Controls/CollectionListingView.cs b/GuiComponents/Controls/CollectionListingView.cs new file mode 100644 index 0000000..e7bbc06 --- /dev/null +++ b/GuiComponents/Controls/CollectionListingView.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections; +using System.Windows.Forms; +using BrightIdeasSoftware; +using CollectionManager.DataTypes; +using Gui.Misc; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class CollectionListingView : UserControl, ICollectionListingView + { + public string SearchText => textBox_collectionNameSearch.Text; + public Collections Collections { set { ListViewCollections.SetObjects(value); } } + + public Collection SelectedCollection + { + get { return (Collection) ListViewCollections.SelectedObject; } + set + { + ListViewCollections.SelectedObject = null; + ListViewCollections.SelectedObject = value; + } + } + public ArrayList SelectedCollections => (ArrayList)ListViewCollections.SelectedObjects; + + public event EventHandler SearchTextChanged; + public event EventHandler SelectedCollectionChanged; + public event EventHandler SelectedCollectionsChanged; + public event GuiHelpers.CollectionBeatmapsEventArgs BeatmapsDropped; + public event EventHandler RightClick; + + public CollectionListingView() + { + InitializeComponent(); + init(); + Bind(); + } + + private void Bind() + { + this.textBox_collectionNameSearch.TextChanged += delegate + { + ListViewCollections.AdditionalFilter = TextMatchFilter.Contains(ListViewCollections, SearchText); + }; + + this.ListViewCollections.SelectionChanged += delegate + { + OnSelectedCollectionChanged(); + OnSelectedCollectionsChanged(); + }; + + } + + private void init() + { + //ListViewCollections.SelectedIndexChanged += ListViewCollectionsSelectedIndexChanged; + ListViewCollections.UseFiltering = true; + ListViewCollections.FullRowSelect = true; + ListViewCollections.HideSelection = false; + + var dropsink = new RearrangingDropSink(); + dropsink.CanDropBetween = false; + dropsink.CanDropOnItem = true; + dropsink.CanDropOnSubItem = false; + dropsink.CanDropOnBackground = false; + ListViewCollections.DropSink = dropsink; + ListViewCollections.ModelDropped += ListViewCollections_ModelDropped; + + ListViewCollections.CellRightClick += ListViewCollectionsOnCellRightClick; + } + + private void ListViewCollectionsOnCellRightClick(object sender, CellRightClickEventArgs cellRightClickEventArgs) + { + cellRightClickEventArgs.MenuStrip = CollectionContextMenuStrip; + } + + public void SetFilter(IModelFilter filter) + { + ListViewCollections.AdditionalFilter = filter; + } + + public void FilteringStarted() + { + ListViewCollections.BeginUpdate(); + } + + public void FilteringFinished() + { + ListViewCollections.UpdateColumnFiltering(); + ListViewCollections.EndUpdate(); + } + + protected virtual void OnSelectedCollectionChanged() + { + SelectedCollectionChanged?.Invoke(this, EventArgs.Empty); + } + protected virtual void OnSelectedCollectionsChanged() + { + SelectedCollectionsChanged?.Invoke(this, EventArgs.Empty); + } + private void ListViewCollections_ModelDropped(object sender, ModelDropEventArgs e) + { + e.Handled = true; + var collection = (Collection)e.TargetModel; + if (collection == null) return; + var beatmaps = new Beatmaps(); + foreach (var b in e.SourceModels) + { + beatmaps.Add((BeatmapExtension)b); + } + + BeatmapsDropped?.Invoke(this, beatmaps, collection.Name); + } + + protected virtual void OnRightClick(StringEventArgs e) + { + RightClick?.Invoke(this, e); + } + + private void MenuStripClick(object sender, EventArgs e) + { + var menuItem = (ToolStripMenuItem)sender; + OnRightClick(new StringEventArgs((string)menuItem.Tag)); + } + + } +} diff --git a/GuiComponents/Controls/CollectionListingView.resx b/GuiComponents/Controls/CollectionListingView.resx new file mode 100644 index 0000000..412c310 --- /dev/null +++ b/GuiComponents/Controls/CollectionListingView.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file diff --git a/GuiComponents/Controls/CollectionRenameView.Designer.cs b/GuiComponents/Controls/CollectionRenameView.Designer.cs new file mode 100644 index 0000000..5e85ed2 --- /dev/null +++ b/GuiComponents/Controls/CollectionRenameView.Designer.cs @@ -0,0 +1,160 @@ +namespace GuiComponents.Controls +{ + partial class CollectionRenameView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label_Error = new System.Windows.Forms.Label(); + this.panel_bottom = new System.Windows.Forms.Panel(); + this.button_cancel = new System.Windows.Forms.Button(); + this.button_rename = new System.Windows.Forms.Button(); + this.panel_Top = new System.Windows.Forms.Panel(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.label_orginalCollectionName = new System.Windows.Forms.Label(); + this.textBox_newCollectionName = new System.Windows.Forms.TextBox(); + this.panel_bottom.SuspendLayout(); + this.panel_Top.SuspendLayout(); + this.SuspendLayout(); + // + // label_Error + // + this.label_Error.AutoSize = true; + this.label_Error.Location = new System.Drawing.Point(32, 4); + this.label_Error.Name = "label_Error"; + this.label_Error.Size = new System.Drawing.Size(125, 13); + this.label_Error.TabIndex = 11; + this.label_Error.Text = "This name already exists!"; + this.label_Error.Visible = false; + // + // panel_bottom + // + this.panel_bottom.Anchor = System.Windows.Forms.AnchorStyles.None; + this.panel_bottom.Controls.Add(this.label_Error); + this.panel_bottom.Controls.Add(this.button_cancel); + this.panel_bottom.Controls.Add(this.button_rename); + this.panel_bottom.Location = new System.Drawing.Point(31, 66); + this.panel_bottom.Name = "panel_bottom"; + this.panel_bottom.Size = new System.Drawing.Size(188, 46); + this.panel_bottom.TabIndex = 10; + // + // button_cancel + // + this.button_cancel.Location = new System.Drawing.Point(110, 20); + this.button_cancel.Name = "button_cancel"; + this.button_cancel.Size = new System.Drawing.Size(75, 23); + this.button_cancel.TabIndex = 5; + this.button_cancel.Text = "Cancel"; + this.button_cancel.UseVisualStyleBackColor = true; + // + // button_rename + // + this.button_rename.DialogResult = System.Windows.Forms.DialogResult.OK; + this.button_rename.Enabled = false; + this.button_rename.Location = new System.Drawing.Point(5, 20); + this.button_rename.Name = "button_rename"; + this.button_rename.Size = new System.Drawing.Size(75, 23); + this.button_rename.TabIndex = 4; + this.button_rename.Text = "Rename"; + this.button_rename.UseVisualStyleBackColor = true; + // + // panel_Top + // + this.panel_Top.Anchor = System.Windows.Forms.AnchorStyles.None; + this.panel_Top.Controls.Add(this.label2); + this.panel_Top.Controls.Add(this.label1); + this.panel_Top.Controls.Add(this.label_orginalCollectionName); + this.panel_Top.Controls.Add(this.textBox_newCollectionName); + this.panel_Top.Location = new System.Drawing.Point(0, 0); + this.panel_Top.MaximumSize = new System.Drawing.Size(254, 63); + this.panel_Top.Name = "panel_Top"; + this.panel_Top.Size = new System.Drawing.Size(254, 63); + this.panel_Top.TabIndex = 9; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(17, 37); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(61, 13); + this.label2.TabIndex = 1; + this.label2.Text = "New name:"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(6, 7); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(72, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Orginal name:"; + // + // label_orginalCollectionName + // + this.label_orginalCollectionName.AutoSize = true; + this.label_orginalCollectionName.Location = new System.Drawing.Point(81, 7); + this.label_orginalCollectionName.Name = "label_orginalCollectionName"; + this.label_orginalCollectionName.Size = new System.Drawing.Size(27, 13); + this.label_orginalCollectionName.TabIndex = 2; + this.label_orginalCollectionName.Text = "from"; + // + // textBox_newCollectionName + // + this.textBox_newCollectionName.Location = new System.Drawing.Point(84, 34); + this.textBox_newCollectionName.Name = "textBox_newCollectionName"; + this.textBox_newCollectionName.Size = new System.Drawing.Size(150, 20); + this.textBox_newCollectionName.TabIndex = 3; + // + // CollectionRenameView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.panel_bottom); + this.Controls.Add(this.panel_Top); + this.Name = "CollectionRenameView"; + this.Size = new System.Drawing.Size(255, 112); + this.panel_bottom.ResumeLayout(false); + this.panel_bottom.PerformLayout(); + this.panel_Top.ResumeLayout(false); + this.panel_Top.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label label_Error; + private System.Windows.Forms.Panel panel_bottom; + public System.Windows.Forms.Button button_rename; + private System.Windows.Forms.Panel panel_Top; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label_orginalCollectionName; + private System.Windows.Forms.TextBox textBox_newCollectionName; + public System.Windows.Forms.Button button_cancel; + } +} diff --git a/GuiComponents/Controls/CollectionRenameView.cs b/GuiComponents/Controls/CollectionRenameView.cs new file mode 100644 index 0000000..a8f98c3 --- /dev/null +++ b/GuiComponents/Controls/CollectionRenameView.cs @@ -0,0 +1,73 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class CollectionRenameView : UserControl, ICollectionRenameView, ICollectionAddView + { + public CollectionRenameView() + { + InitializeComponent(); + + textBox_newCollectionName.TextChanged += (s, a) => CollectionNameChanged?.Invoke(this, EventArgs.Empty); + button_rename.Click += (s, a) => Submited?.Invoke(this, EventArgs.Empty); + button_cancel.Click += (s, a) => Canceled?.Invoke(this, EventArgs.Empty); + } + + private bool _isRenameView = true; + [Description("Is this control used for renaming(true) or adding(false) new collection?"), Category("Layout")] + public bool IsRenameView + { + get { return _isRenameView; } + set + { + if (_isRenameView != value) + { + //set control positions + int offset = value ? 25 : -25; + + panel_Top.Location = new Point(panel_Top.Location.X, panel_Top.Location.Y + offset); + panel_bottom.Location = new Point(panel_bottom.Location.X, panel_bottom.Location.Y + offset); + Size = new Size(Size.Width, Size.Height + offset); + + //change label & button text + button_rename.Text = value ? "Rename" : "Create"; + label2.Text = value ? "New name:" : "Name:"; + } + _isRenameView = value; + } + } + + public event EventHandler CollectionNameChanged; + public event EventHandler Submited; + public event EventHandler Canceled; + + public string NewCollectionName => textBox_newCollectionName.Text; + + public string OrginalCollectionName + { + get + { + return label_orginalCollectionName.Text; + } + set { label_orginalCollectionName.Text = value; } + } + + public string ErrorText + { + set + { + label_Error.Text = value; + label_Error.Visible = !string.IsNullOrWhiteSpace(value); + } + } + + public bool CanSubmit + { + set { button_rename.Enabled = value; } + } + } +} diff --git a/GuiComponents/Controls/CollectionRenameView.resx b/GuiComponents/Controls/CollectionRenameView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/CollectionRenameView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/CollectionTextView.Designer.cs b/GuiComponents/Controls/CollectionTextView.Designer.cs new file mode 100644 index 0000000..1ebf588 --- /dev/null +++ b/GuiComponents/Controls/CollectionTextView.Designer.cs @@ -0,0 +1,86 @@ +namespace GuiComponents.Controls +{ + partial class CollectionTextView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.textBox1 = new System.Windows.Forms.TextBox(); + this.comboBox_textType = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // textBox1 + // + this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.textBox1.Location = new System.Drawing.Point(0, 30); + this.textBox1.Multiline = true; + this.textBox1.Name = "textBox1"; + this.textBox1.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.textBox1.Size = new System.Drawing.Size(312, 324); + this.textBox1.TabIndex = 0; + // + // comboBox_textType + // + this.comboBox_textType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboBox_textType.FormattingEnabled = true; + this.comboBox_textType.Location = new System.Drawing.Point(72, 3); + this.comboBox_textType.Name = "comboBox_textType"; + this.comboBox_textType.Size = new System.Drawing.Size(153, 21); + this.comboBox_textType.TabIndex = 1; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, 6); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(63, 13); + this.label1.TabIndex = 2; + this.label1.Text = "Text format:"; + // + // CollectionTextView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.label1); + this.Controls.Add(this.comboBox_textType); + this.Controls.Add(this.textBox1); + this.Name = "CollectionTextView"; + this.Size = new System.Drawing.Size(312, 354); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TextBox textBox1; + private System.Windows.Forms.ComboBox comboBox_textType; + private System.Windows.Forms.Label label1; + } +} diff --git a/GuiComponents/Controls/CollectionTextView.cs b/GuiComponents/Controls/CollectionTextView.cs new file mode 100644 index 0000000..592d7bc --- /dev/null +++ b/GuiComponents/Controls/CollectionTextView.cs @@ -0,0 +1,38 @@ +using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class CollectionTextView : UserControl, ICollectionTextView + { + + public event EventHandler SaveTypeChanged; + public void SetListTypes(Array types) + { + comboBox_textType.DataSource = types; + } + + public string GeneratedText { set { textBox1.Text = value; } } + public CollectionTextView() + { + InitializeComponent(); + textBox1.KeyDown += textBox1_KeyDown; + comboBox_textType.SelectedIndexChanged += delegate + { + SaveTypeChanged?.Invoke(this, EventArgs.Empty); + }; + } + + public string SelectedSaveType => comboBox_textType.SelectedValue.ToString(); + private void textBox1_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyData == (Keys.Control | Keys.A)) + { + textBox1.SelectAll(); + e.Handled = e.SuppressKeyPress = true; + } + } + + } +} diff --git a/GuiComponents/Controls/CollectionTextView.resx b/GuiComponents/Controls/CollectionTextView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/CollectionTextView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/CombinedBeatmapPreviewView.Designer.cs b/GuiComponents/Controls/CombinedBeatmapPreviewView.Designer.cs new file mode 100644 index 0000000..2a6c684 --- /dev/null +++ b/GuiComponents/Controls/CombinedBeatmapPreviewView.Designer.cs @@ -0,0 +1,72 @@ +namespace GuiComponents.Controls +{ + partial class CombinedBeatmapPreviewView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.beatmapThumbnailView1 = new GuiComponents.Controls.BeatmapThumbnailView(); + this.musicControlView1 = new GuiComponents.Controls.MusicControlView(); + this.SuspendLayout(); + // + // beatmapThumbnailView1 + // + this.beatmapThumbnailView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.beatmapThumbnailView1.Location = new System.Drawing.Point(2, 2); + this.beatmapThumbnailView1.Name = "beatmapThumbnailView1"; + this.beatmapThumbnailView1.Size = new System.Drawing.Size(396, 336); + this.beatmapThumbnailView1.TabIndex = 0; + // + // musicControlView1 + // + this.musicControlView1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.musicControlView1.IsUserSeeking = false; + this.musicControlView1.Location = new System.Drawing.Point(186, 294); + this.musicControlView1.Name = "musicControlView1"; + this.musicControlView1.Position = 0; + this.musicControlView1.Size = new System.Drawing.Size(214, 58); + this.musicControlView1.TabIndex = 1; + // + // CombinedBeatmapPreviewView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.musicControlView1); + this.Controls.Add(this.beatmapThumbnailView1); + this.Name = "CombinedBeatmapPreviewView"; + this.Size = new System.Drawing.Size(400, 352); + this.ResumeLayout(false); + + } + + #endregion + + private BeatmapThumbnailView beatmapThumbnailView1; + private MusicControlView musicControlView1; + } +} diff --git a/GuiComponents/Controls/CombinedBeatmapPreviewView.cs b/GuiComponents/Controls/CombinedBeatmapPreviewView.cs new file mode 100644 index 0000000..ba4c643 --- /dev/null +++ b/GuiComponents/Controls/CombinedBeatmapPreviewView.cs @@ -0,0 +1,16 @@ +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class CombinedBeatmapPreviewView : UserControl, ICombinedBeatmapPreviewView + { + public CombinedBeatmapPreviewView() + { + InitializeComponent(); + } + + public IBeatmapThumbnailView BeatmapThumbnailView => beatmapThumbnailView1; + public IMusicControlView MusicControlView => musicControlView1; + } +} diff --git a/GuiComponents/Controls/CombinedBeatmapPreviewView.resx b/GuiComponents/Controls/CombinedBeatmapPreviewView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/CombinedBeatmapPreviewView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/CombinedListingView.Designer.cs b/GuiComponents/Controls/CombinedListingView.Designer.cs new file mode 100644 index 0000000..0399656 --- /dev/null +++ b/GuiComponents/Controls/CombinedListingView.Designer.cs @@ -0,0 +1,98 @@ +namespace GuiComponents.Controls +{ + partial class CombinedListingView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.collectionListingView1 = new GuiComponents.Controls.CollectionListingView(); + this.beatmapListingView1 = new GuiComponents.Controls.BeatmapListingView(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.SuspendLayout(); + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(0, 0); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.collectionListingView1); + this.splitContainer1.Panel1MinSize = 230; + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.beatmapListingView1); + this.splitContainer1.Size = new System.Drawing.Size(1112, 406); + this.splitContainer1.SplitterDistance = 230; + this.splitContainer1.TabIndex = 0; + // + // collectionListingView1 + // + this.collectionListingView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.collectionListingView1.Location = new System.Drawing.Point(0, 0); + this.collectionListingView1.Name = "collectionListingView1"; + this.collectionListingView1.SelectedCollection = null; + this.collectionListingView1.Size = new System.Drawing.Size(230, 406); + this.collectionListingView1.TabIndex = 0; + // + // beatmapListingView1 + // + this.beatmapListingView1.AllowForDeletion = true; + this.beatmapListingView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.beatmapListingView1.Location = new System.Drawing.Point(0, 0); + this.beatmapListingView1.Name = "beatmapListingView1"; + this.beatmapListingView1.ResultText = null; + this.beatmapListingView1.Size = new System.Drawing.Size(878, 406); + this.beatmapListingView1.TabIndex = 0; + // + // CombinedListingView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.splitContainer1); + this.Name = "CombinedListingView"; + this.Size = new System.Drawing.Size(1112, 406); + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.SplitContainer splitContainer1; + private CollectionListingView collectionListingView1; + private BeatmapListingView beatmapListingView1; + } +} diff --git a/GuiComponents/Controls/CombinedListingView.cs b/GuiComponents/Controls/CombinedListingView.cs new file mode 100644 index 0000000..cca1f7d --- /dev/null +++ b/GuiComponents/Controls/CombinedListingView.cs @@ -0,0 +1,17 @@ +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class CombinedListingView : UserControl, ICombinedListingView + { + public CombinedListingView() + { + InitializeComponent(); + splitContainer1.Paint += Helpers.SplitterPaint; + } + + public IBeatmapListingView beatmapListingView => beatmapListingView1; + public ICollectionListingView CollectionListingView => collectionListingView1; + } +} diff --git a/GuiComponents/Controls/CombinedListingView.resx b/GuiComponents/Controls/CombinedListingView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/CombinedListingView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/DownloadManagerView.Designer.cs b/GuiComponents/Controls/DownloadManagerView.Designer.cs new file mode 100644 index 0000000..a40aacc --- /dev/null +++ b/GuiComponents/Controls/DownloadManagerView.Designer.cs @@ -0,0 +1,125 @@ +namespace GuiComponents.Controls +{ + partial class DownloadManagerView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.ListViewDownload = new BrightIdeasSoftware.FastObjectListView(); + this.olvColumn3 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn1 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.olvColumn2 = ((BrightIdeasSoftware.OLVColumn)(new BrightIdeasSoftware.OLVColumn())); + this.button_ToggleDownloads = new System.Windows.Forms.Button(); + this.label_status = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.ListViewDownload)).BeginInit(); + this.SuspendLayout(); + // + // ListViewDownload + // + this.ListViewDownload.AllColumns.Add(this.olvColumn3); + this.ListViewDownload.AllColumns.Add(this.olvColumn1); + this.ListViewDownload.AllColumns.Add(this.olvColumn2); + this.ListViewDownload.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ListViewDownload.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.olvColumn3, + this.olvColumn1, + this.olvColumn2}); + this.ListViewDownload.HideSelection = false; + this.ListViewDownload.Location = new System.Drawing.Point(0, 32); + this.ListViewDownload.Name = "ListViewDownload"; + this.ListViewDownload.ShowGroups = false; + this.ListViewDownload.Size = new System.Drawing.Size(492, 328); + this.ListViewDownload.TabIndex = 1; + this.ListViewDownload.UnfocusedHighlightBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); + this.ListViewDownload.UseCompatibleStateImageBehavior = false; + this.ListViewDownload.UseCustomSelectionColors = true; + this.ListViewDownload.UseNotifyPropertyChanged = true; + this.ListViewDownload.View = System.Windows.Forms.View.Details; + this.ListViewDownload.VirtualMode = true; + // + // olvColumn3 + // + this.olvColumn3.AspectName = "Id"; + this.olvColumn3.Text = "ID"; + this.olvColumn3.Width = 40; + // + // olvColumn1 + // + this.olvColumn1.AspectName = "Name"; + this.olvColumn1.Text = "Name"; + this.olvColumn1.Width = 332; + // + // olvColumn2 + // + this.olvColumn2.AspectName = "Progress"; + this.olvColumn2.Text = "Progress"; + this.olvColumn2.Width = 132; + // + // button_ToggleDownloads + // + this.button_ToggleDownloads.Location = new System.Drawing.Point(3, 3); + this.button_ToggleDownloads.Name = "button_ToggleDownloads"; + this.button_ToggleDownloads.Size = new System.Drawing.Size(157, 23); + this.button_ToggleDownloads.TabIndex = 3; + this.button_ToggleDownloads.Text = "Stop downloads"; + this.button_ToggleDownloads.UseVisualStyleBackColor = true; + // + // label_status + // + this.label_status.AutoSize = true; + this.label_status.Location = new System.Drawing.Point(166, 8); + this.label_status.Name = "label_status"; + this.label_status.Size = new System.Drawing.Size(34, 13); + this.label_status.TabIndex = 4; + this.label_status.Text = " "; + // + // DownloadManagerView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.label_status); + this.Controls.Add(this.button_ToggleDownloads); + this.Controls.Add(this.ListViewDownload); + this.Name = "DownloadManagerView"; + this.Size = new System.Drawing.Size(492, 360); + ((System.ComponentModel.ISupportInitialize)(this.ListViewDownload)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private BrightIdeasSoftware.FastObjectListView ListViewDownload; + private BrightIdeasSoftware.OLVColumn olvColumn3; + private BrightIdeasSoftware.OLVColumn olvColumn1; + private BrightIdeasSoftware.OLVColumn olvColumn2; + private System.Windows.Forms.Button button_ToggleDownloads; + private System.Windows.Forms.Label label_status; + } +} diff --git a/GuiComponents/Controls/DownloadManagerView.cs b/GuiComponents/Controls/DownloadManagerView.cs new file mode 100644 index 0000000..8d73415 --- /dev/null +++ b/GuiComponents/Controls/DownloadManagerView.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using CollectionManagerExtensionsDll.Modules.DownloadManager.API; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class DownloadManagerView : UserControl, IDownloadManagerView + { + public DownloadManagerView() + { + InitializeComponent(); + + button_ToggleDownloads.Click += (s, a) => DownloadToggleClick?.Invoke(this, EventArgs.Empty); + ListViewDownload.FullRowSelect = true; + } + + public event EventHandler DownloadToggleClick; + + public bool DownloadButtonIsEnabled + { + get { return this.button_ToggleDownloads.Enabled; } + set { button_ToggleDownloads.Enabled = value; } + } + + public string DownloadButtonText + { + set { button_ToggleDownloads.Text = value; } + } + + public void SetDownloadItems(ICollection downloadItems) + { + ListViewDownload.SetObjects(downloadItems); + } + + public void UpdateDownloadItem(DownloadItem downloadItem) + { + try + { + ListViewDownload.RefreshObject(downloadItem); + } + catch + { + // ignored + } + } + } +} diff --git a/GuiComponents/Controls/DownloadManagerView.resx b/GuiComponents/Controls/DownloadManagerView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/DownloadManagerView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/InfoTextView.Designer.cs b/GuiComponents/Controls/InfoTextView.Designer.cs new file mode 100644 index 0000000..939908c --- /dev/null +++ b/GuiComponents/Controls/InfoTextView.Designer.cs @@ -0,0 +1,107 @@ +namespace GuiComponents.Controls +{ + partial class InfoTextView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label_UpdateText = new System.Windows.Forms.Label(); + this.label_beatmapsMissing = new System.Windows.Forms.Label(); + this.label_LoadedCollections = new System.Windows.Forms.Label(); + this.label_LoadedBeatmaps = new System.Windows.Forms.Label(); + this.label_BeatmapsInCollections = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // label_UpdateText + // + this.label_UpdateText.AutoSize = true; + this.label_UpdateText.Location = new System.Drawing.Point(1, 0); + this.label_UpdateText.Name = "label_UpdateText"; + this.label_UpdateText.Size = new System.Drawing.Size(75, 13); + this.label_UpdateText.TabIndex = 13; + this.label_UpdateText.Text = ""; + // + // label_beatmapsMissing + // + this.label_beatmapsMissing.AutoSize = true; + this.label_beatmapsMissing.Location = new System.Drawing.Point(1, 68); + this.label_beatmapsMissing.Name = "label_beatmapsMissing"; + this.label_beatmapsMissing.Size = new System.Drawing.Size(101, 13); + this.label_beatmapsMissing.TabIndex = 12; + this.label_beatmapsMissing.Text = ""; + // + // label_LoadedCollections + // + this.label_LoadedCollections.AutoSize = true; + this.label_LoadedCollections.Location = new System.Drawing.Point(1, 34); + this.label_LoadedCollections.Name = "label_LoadedCollections"; + this.label_LoadedCollections.Size = new System.Drawing.Size(106, 13); + this.label_LoadedCollections.TabIndex = 11; + this.label_LoadedCollections.Text = ""; + // + // label_LoadedBeatmaps + // + this.label_LoadedBeatmaps.AutoSize = true; + this.label_LoadedBeatmaps.Location = new System.Drawing.Point(1, 17); + this.label_LoadedBeatmaps.Name = "label_LoadedBeatmaps"; + this.label_LoadedBeatmaps.Size = new System.Drawing.Size(101, 13); + this.label_LoadedBeatmaps.TabIndex = 10; + this.label_LoadedBeatmaps.Text = ""; + // + // label_BeatmapsInCollections + // + this.label_BeatmapsInCollections.AutoSize = true; + this.label_BeatmapsInCollections.Location = new System.Drawing.Point(12, 51); + this.label_BeatmapsInCollections.Name = "label_BeatmapsInCollections"; + this.label_BeatmapsInCollections.Size = new System.Drawing.Size(126, 13); + this.label_BeatmapsInCollections.TabIndex = 14; + this.label_BeatmapsInCollections.Text = ""; + // + // InfoTextView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.label_BeatmapsInCollections); + this.Controls.Add(this.label_UpdateText); + this.Controls.Add(this.label_beatmapsMissing); + this.Controls.Add(this.label_LoadedCollections); + this.Controls.Add(this.label_LoadedBeatmaps); + this.Name = "InfoTextView"; + this.Size = new System.Drawing.Size(182, 92); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label_UpdateText; + private System.Windows.Forms.Label label_beatmapsMissing; + private System.Windows.Forms.Label label_LoadedCollections; + private System.Windows.Forms.Label label_LoadedBeatmaps; + private System.Windows.Forms.Label label_BeatmapsInCollections; + } +} diff --git a/GuiComponents/Controls/InfoTextView.cs b/GuiComponents/Controls/InfoTextView.cs new file mode 100644 index 0000000..f9fa779 --- /dev/null +++ b/GuiComponents/Controls/InfoTextView.cs @@ -0,0 +1,38 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class InfoTextView : UserControl, IInfoTextView + { + public InfoTextView() + { + InitializeComponent(); + + label_UpdateText.Click += (s, a) => { UpdateTextClicked?.Invoke(this, EventArgs.Empty); }; + } + + public bool UpdateTextIsClickable + { + set + { + if (value) + { + label_UpdateText.Cursor = Cursors.Hand; //TODO: indicate that link is clickable + } + else + label_UpdateText.Cursor = DefaultCursor; + } + } + + public string UpdateText { set { label_UpdateText.Text = value; } } + public string BeatmapLoaded { set { label_LoadedBeatmaps.Text = value; } } + public string CollectionsLoaded { set { label_LoadedCollections.Text = value; } } + public string BeatmapsInCollections { set { label_BeatmapsInCollections.Text = value; } } + public string BeatmapsMissing { set { label_beatmapsMissing.Text = value; } } + + public event EventHandler UpdateTextClicked; + } +} diff --git a/GuiComponents/Controls/InfoTextView.resx b/GuiComponents/Controls/InfoTextView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/InfoTextView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/MainSidePanelView.Designer.cs b/GuiComponents/Controls/MainSidePanelView.Designer.cs new file mode 100644 index 0000000..333e6f3 --- /dev/null +++ b/GuiComponents/Controls/MainSidePanelView.Designer.cs @@ -0,0 +1,312 @@ +namespace GuiComponents.Controls +{ + partial class MainSidePanelView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.panel1 = new System.Windows.Forms.Panel(); + this.button_mapDownloads = new System.Windows.Forms.Button(); + this.button_beatmapListing = new System.Windows.Forms.Button(); + this.groupBox_onlineServices = new System.Windows.Forms.GroupBox(); + this.button_getUserTops = new System.Windows.Forms.Button(); + this.button_GetMissingMaps = new System.Windows.Forms.Button(); + this.groupBox3 = new System.Windows.Forms.GroupBox(); + this.button_listMissingMaps = new System.Windows.Forms.Button(); + this.button_listAllCollections = new System.Windows.Forms.Button(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.button_saveAllCollections = new System.Windows.Forms.Button(); + this.button_collectionsSplit = new System.Windows.Forms.Button(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.button_unloadCollections = new System.Windows.Forms.Button(); + this.button_loadDefaultCollection = new System.Windows.Forms.Button(); + this.button_loadCollection = new System.Windows.Forms.Button(); + this.button_refreshBeatmapList = new System.Windows.Forms.Button(); + this.button_downloadAllMissing = new System.Windows.Forms.Button(); + this.panel1.SuspendLayout(); + this.groupBox_onlineServices.SuspendLayout(); + this.groupBox3.SuspendLayout(); + this.groupBox2.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.SuspendLayout(); + // + // panel1 + // + this.panel1.Controls.Add(this.button_downloadAllMissing); + this.panel1.Controls.Add(this.button_mapDownloads); + this.panel1.Controls.Add(this.button_beatmapListing); + this.panel1.Controls.Add(this.groupBox_onlineServices); + this.panel1.Controls.Add(this.groupBox3); + this.panel1.Controls.Add(this.groupBox2); + this.panel1.Controls.Add(this.groupBox1); + this.panel1.Controls.Add(this.button_refreshBeatmapList); + this.panel1.Dock = System.Windows.Forms.DockStyle.Fill; + this.panel1.Location = new System.Drawing.Point(0, 0); + this.panel1.Name = "panel1"; + this.panel1.Size = new System.Drawing.Size(184, 512); + this.panel1.TabIndex = 9; + // + // button_mapDownloads + // + this.button_mapDownloads.Location = new System.Drawing.Point(4, 311); + this.button_mapDownloads.Name = "button_mapDownloads"; + this.button_mapDownloads.Size = new System.Drawing.Size(170, 23); + this.button_mapDownloads.TabIndex = 13; + this.button_mapDownloads.Text = "Show map downloads"; + this.button_mapDownloads.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_mapDownloads.UseVisualStyleBackColor = true; + // + // button_beatmapListing + // + this.button_beatmapListing.Location = new System.Drawing.Point(4, 282); + this.button_beatmapListing.Name = "button_beatmapListing"; + this.button_beatmapListing.Size = new System.Drawing.Size(170, 23); + this.button_beatmapListing.TabIndex = 12; + this.button_beatmapListing.Text = "Show beatmap listing"; + this.button_beatmapListing.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_beatmapListing.UseVisualStyleBackColor = true; + // + // groupBox_onlineServices + // + this.groupBox_onlineServices.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox_onlineServices.Controls.Add(this.button_getUserTops); + this.groupBox_onlineServices.Controls.Add(this.button_GetMissingMaps); + this.groupBox_onlineServices.Location = new System.Drawing.Point(3, 376); + this.groupBox_onlineServices.Name = "groupBox_onlineServices"; + this.groupBox_onlineServices.Size = new System.Drawing.Size(174, 76); + this.groupBox_onlineServices.TabIndex = 12; + this.groupBox_onlineServices.TabStop = false; + this.groupBox_onlineServices.Text = "Online services"; + this.groupBox_onlineServices.Visible = false; + // + // button_getUserTops + // + this.button_getUserTops.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_getUserTops.Location = new System.Drawing.Point(1, 47); + this.button_getUserTops.Name = "button_getUserTops"; + this.button_getUserTops.Size = new System.Drawing.Size(170, 23); + this.button_getUserTops.TabIndex = 11; + this.button_getUserTops.Text = "Get user top ranks"; + this.button_getUserTops.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_getUserTops.UseVisualStyleBackColor = true; + // + // button_GetMissingMaps + // + this.button_GetMissingMaps.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_GetMissingMaps.Location = new System.Drawing.Point(1, 19); + this.button_GetMissingMaps.Name = "button_GetMissingMaps"; + this.button_GetMissingMaps.Size = new System.Drawing.Size(170, 23); + this.button_GetMissingMaps.TabIndex = 9; + this.button_GetMissingMaps.Text = "Get missing maps"; + this.button_GetMissingMaps.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_GetMissingMaps.UseVisualStyleBackColor = true; + // + // groupBox3 + // + this.groupBox3.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox3.Controls.Add(this.button_listMissingMaps); + this.groupBox3.Controls.Add(this.button_listAllCollections); + this.groupBox3.Location = new System.Drawing.Point(3, 198); + this.groupBox3.Name = "groupBox3"; + this.groupBox3.Size = new System.Drawing.Size(174, 76); + this.groupBox3.TabIndex = 11; + this.groupBox3.TabStop = false; + this.groupBox3.Text = "Collections listing"; + // + // button_listMissingMaps + // + this.button_listMissingMaps.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_listMissingMaps.Location = new System.Drawing.Point(1, 48); + this.button_listMissingMaps.Name = "button_listMissingMaps"; + this.button_listMissingMaps.Size = new System.Drawing.Size(170, 23); + this.button_listMissingMaps.TabIndex = 8; + this.button_listMissingMaps.Text = "List missing maps"; + this.button_listMissingMaps.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_listMissingMaps.UseVisualStyleBackColor = true; + // + // button_listAllCollections + // + this.button_listAllCollections.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_listAllCollections.Location = new System.Drawing.Point(1, 19); + this.button_listAllCollections.Name = "button_listAllCollections"; + this.button_listAllCollections.Size = new System.Drawing.Size(170, 23); + this.button_listAllCollections.TabIndex = 7; + this.button_listAllCollections.Text = "List all collections"; + this.button_listAllCollections.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_listAllCollections.UseVisualStyleBackColor = true; + // + // groupBox2 + // + this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox2.Controls.Add(this.button_saveAllCollections); + this.groupBox2.Controls.Add(this.button_collectionsSplit); + this.groupBox2.Location = new System.Drawing.Point(3, 121); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Size = new System.Drawing.Size(174, 76); + this.groupBox2.TabIndex = 10; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "Collections saving"; + // + // button_saveAllCollections + // + this.button_saveAllCollections.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_saveAllCollections.Location = new System.Drawing.Point(1, 19); + this.button_saveAllCollections.Name = "button_saveAllCollections"; + this.button_saveAllCollections.Size = new System.Drawing.Size(170, 23); + this.button_saveAllCollections.TabIndex = 5; + this.button_saveAllCollections.Text = "Save Collections"; + this.button_saveAllCollections.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_saveAllCollections.UseVisualStyleBackColor = true; + // + // button_collectionsSplit + // + this.button_collectionsSplit.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_collectionsSplit.Location = new System.Drawing.Point(1, 48); + this.button_collectionsSplit.Name = "button_collectionsSplit"; + this.button_collectionsSplit.Size = new System.Drawing.Size(170, 23); + this.button_collectionsSplit.TabIndex = 6; + this.button_collectionsSplit.Text = "Save collections in separate files"; + this.button_collectionsSplit.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_collectionsSplit.UseVisualStyleBackColor = true; + // + // groupBox1 + // + this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox1.Controls.Add(this.button_unloadCollections); + this.groupBox1.Controls.Add(this.button_loadDefaultCollection); + this.groupBox1.Controls.Add(this.button_loadCollection); + this.groupBox1.Location = new System.Drawing.Point(3, 7); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(174, 108); + this.groupBox1.TabIndex = 9; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Collection loading"; + // + // button_unloadCollections + // + this.button_unloadCollections.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_unloadCollections.Location = new System.Drawing.Point(1, 77); + this.button_unloadCollections.Name = "button_unloadCollections"; + this.button_unloadCollections.Size = new System.Drawing.Size(170, 23); + this.button_unloadCollections.TabIndex = 8; + this.button_unloadCollections.Text = "Clear collections"; + this.button_unloadCollections.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_unloadCollections.UseVisualStyleBackColor = true; + // + // button_loadDefaultCollection + // + this.button_loadDefaultCollection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_loadDefaultCollection.Location = new System.Drawing.Point(1, 48); + this.button_loadDefaultCollection.Name = "button_loadDefaultCollection"; + this.button_loadDefaultCollection.Size = new System.Drawing.Size(170, 23); + this.button_loadDefaultCollection.TabIndex = 7; + this.button_loadDefaultCollection.Text = "Load osu! collection"; + this.button_loadDefaultCollection.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_loadDefaultCollection.UseVisualStyleBackColor = true; + // + // button_loadCollection + // + this.button_loadCollection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.button_loadCollection.Location = new System.Drawing.Point(1, 19); + this.button_loadCollection.Name = "button_loadCollection"; + this.button_loadCollection.Size = new System.Drawing.Size(170, 23); + this.button_loadCollection.TabIndex = 3; + this.button_loadCollection.Text = "Load collection( .db/.osdb)"; + this.button_loadCollection.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_loadCollection.UseVisualStyleBackColor = true; + // + // button_refreshBeatmapList + // + this.button_refreshBeatmapList.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.button_refreshBeatmapList.Location = new System.Drawing.Point(114, 475); + this.button_refreshBeatmapList.Name = "button_refreshBeatmapList"; + this.button_refreshBeatmapList.Size = new System.Drawing.Size(60, 23); + this.button_refreshBeatmapList.TabIndex = 10; + this.button_refreshBeatmapList.Text = "Refresh"; + this.button_refreshBeatmapList.UseVisualStyleBackColor = true; + this.button_refreshBeatmapList.Visible = false; + // + // button_downloadAllMissing + // + this.button_downloadAllMissing.Location = new System.Drawing.Point(4, 340); + this.button_downloadAllMissing.Name = "button_downloadAllMissing"; + this.button_downloadAllMissing.Size = new System.Drawing.Size(170, 23); + this.button_downloadAllMissing.TabIndex = 14; + this.button_downloadAllMissing.Text = "Download all missing maps"; + this.button_downloadAllMissing.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.button_downloadAllMissing.UseVisualStyleBackColor = true; + // + // MainSidePanelView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.panel1); + this.Name = "MainSidePanelView"; + this.Size = new System.Drawing.Size(184, 512); + this.panel1.ResumeLayout(false); + this.groupBox_onlineServices.ResumeLayout(false); + this.groupBox3.ResumeLayout(false); + this.groupBox2.ResumeLayout(false); + this.groupBox1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Panel panel1; + public System.Windows.Forms.Button button_mapDownloads; + public System.Windows.Forms.Button button_beatmapListing; + public System.Windows.Forms.GroupBox groupBox_onlineServices; + public System.Windows.Forms.Button button_getUserTops; + public System.Windows.Forms.Button button_GetMissingMaps; + private System.Windows.Forms.GroupBox groupBox3; + public System.Windows.Forms.Button button_listMissingMaps; + public System.Windows.Forms.Button button_listAllCollections; + private System.Windows.Forms.GroupBox groupBox2; + public System.Windows.Forms.Button button_saveAllCollections; + public System.Windows.Forms.Button button_collectionsSplit; + private System.Windows.Forms.GroupBox groupBox1; + public System.Windows.Forms.Button button_loadDefaultCollection; + public System.Windows.Forms.Button button_loadCollection; + public System.Windows.Forms.Button button_unloadCollections; + public System.Windows.Forms.Button button_refreshBeatmapList; + public System.Windows.Forms.Button button_downloadAllMissing; + } +} diff --git a/GuiComponents/Controls/MainSidePanelView.cs b/GuiComponents/Controls/MainSidePanelView.cs new file mode 100644 index 0000000..e6a96d0 --- /dev/null +++ b/GuiComponents/Controls/MainSidePanelView.cs @@ -0,0 +1,84 @@ +using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class MainSidePanelView : UserControl, IMainSidePanelView + { + public event EventHandler LoadCollection; + public event EventHandler LoadDefaultCollection; + public event EventHandler ClearCollections; + public event EventHandler SaveCollections; + public event EventHandler SaveInvidualCollections; + public event EventHandler ListAllMaps; + public event EventHandler ListMissingMaps; + public event EventHandler ShowBeatmapListing; + public event EventHandler ShowDownloadManager; + public event EventHandler DownloadAllMissing; + + public MainSidePanelView() + { + InitializeComponent(); + button_loadCollection.Click += delegate { OnLoadCollection(); }; + button_loadDefaultCollection.Click += delegate { OnLoadDefaultCollection(); }; + button_unloadCollections.Click += delegate { OnClearCollections(); }; + button_saveAllCollections.Click += delegate { OnSaveCollections(); }; + button_collectionsSplit.Click += delegate { OnSaveInvidualCollections(); }; + button_listAllCollections.Click += delegate { OnListAllMaps(); }; + button_listMissingMaps.Click += delegate { OnListMissingMaps(); }; + button_beatmapListing.Click += delegate { OnShowBeatmapListing(); }; + button_mapDownloads.Click += delegate { OnShowDownloadManager(); }; + button_downloadAllMissing.Click += delegate { OnDownloadAllMissing(); }; + } + private void OnLoadCollection() + { + LoadCollection?.Invoke(this, null); + } + + private void OnLoadDefaultCollection() + { + LoadDefaultCollection?.Invoke(this, null); + } + + private void OnClearCollections() + { + ClearCollections?.Invoke(this, null); + } + + private void OnSaveCollections() + { + SaveCollections?.Invoke(this, null); + } + + private void OnSaveInvidualCollections() + { + SaveInvidualCollections?.Invoke(this, null); + } + + private void OnListAllMaps() + { + ListAllMaps?.Invoke(this, null); + } + + private void OnListMissingMaps() + { + ListMissingMaps?.Invoke(this, null); + } + + private void OnShowBeatmapListing() + { + ShowBeatmapListing?.Invoke(this, null); + } + + private void OnShowDownloadManager() + { + ShowDownloadManager?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnDownloadAllMissing() + { + DownloadAllMissing?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/GuiComponents/Controls/MainSidePanelView.resx b/GuiComponents/Controls/MainSidePanelView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/MainSidePanelView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/MusicControlView.Designer.cs b/GuiComponents/Controls/MusicControlView.Designer.cs new file mode 100644 index 0000000..73a74d6 --- /dev/null +++ b/GuiComponents/Controls/MusicControlView.Designer.cs @@ -0,0 +1,160 @@ +namespace GuiComponents.Controls +{ + partial class MusicControlView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.trackBar_Volume = new System.Windows.Forms.TrackBar(); + this.checkBox_musicPlayer = new System.Windows.Forms.CheckBox(); + this.panel_audioPlayback = new System.Windows.Forms.Panel(); + this.checkBox_DT = new System.Windows.Forms.CheckBox(); + this.trackBar_position = new System.Windows.Forms.TrackBar(); + this.button_StopPreview = new System.Windows.Forms.Button(); + this.checkBox_autoPlay = new System.Windows.Forms.CheckBox(); + this.button_StartPreview = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.trackBar_Volume)).BeginInit(); + this.panel_audioPlayback.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trackBar_position)).BeginInit(); + this.SuspendLayout(); + // + // trackBar_Volume + // + this.trackBar_Volume.AutoSize = false; + this.trackBar_Volume.Location = new System.Drawing.Point(183, 3); + this.trackBar_Volume.Maximum = 100; + this.trackBar_Volume.Name = "trackBar_Volume"; + this.trackBar_Volume.Orientation = System.Windows.Forms.Orientation.Vertical; + this.trackBar_Volume.RightToLeft = System.Windows.Forms.RightToLeft.No; + this.trackBar_Volume.Size = new System.Drawing.Size(20, 55); + this.trackBar_Volume.TabIndex = 20; + this.trackBar_Volume.TickFrequency = 15; + this.trackBar_Volume.Value = 30; + // + // checkBox_musicPlayer + // + this.checkBox_musicPlayer.AutoSize = true; + this.checkBox_musicPlayer.Location = new System.Drawing.Point(52, 25); + this.checkBox_musicPlayer.Name = "checkBox_musicPlayer"; + this.checkBox_musicPlayer.Size = new System.Drawing.Size(64, 17); + this.checkBox_musicPlayer.TabIndex = 21; + this.checkBox_musicPlayer.Text = "♫ mode"; + this.checkBox_musicPlayer.UseVisualStyleBackColor = true; + // + // panel_audioPlayback + // + this.panel_audioPlayback.Controls.Add(this.checkBox_DT); + this.panel_audioPlayback.Controls.Add(this.trackBar_position); + this.panel_audioPlayback.Controls.Add(this.checkBox_musicPlayer); + this.panel_audioPlayback.Controls.Add(this.button_StopPreview); + this.panel_audioPlayback.Controls.Add(this.checkBox_autoPlay); + this.panel_audioPlayback.Controls.Add(this.button_StartPreview); + this.panel_audioPlayback.Location = new System.Drawing.Point(0, 0); + this.panel_audioPlayback.Name = "panel_audioPlayback"; + this.panel_audioPlayback.Size = new System.Drawing.Size(169, 60); + this.panel_audioPlayback.TabIndex = 19; + // + // checkBox_DT + // + this.checkBox_DT.AutoSize = true; + this.checkBox_DT.Location = new System.Drawing.Point(122, 25); + this.checkBox_DT.Name = "checkBox_DT"; + this.checkBox_DT.Size = new System.Drawing.Size(41, 17); + this.checkBox_DT.TabIndex = 23; + this.checkBox_DT.Text = "DT"; + this.checkBox_DT.UseVisualStyleBackColor = true; + // + // trackBar_position + // + this.trackBar_position.AutoSize = false; + this.trackBar_position.Location = new System.Drawing.Point(4, 4); + this.trackBar_position.Maximum = 100; + this.trackBar_position.Name = "trackBar_position"; + this.trackBar_position.Size = new System.Drawing.Size(162, 20); + this.trackBar_position.TabIndex = 11; + this.trackBar_position.TickFrequency = 5; + // + // button_StopPreview + // + this.button_StopPreview.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.button_StopPreview.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); + this.button_StopPreview.ImageAlign = System.Drawing.ContentAlignment.TopCenter; + this.button_StopPreview.Location = new System.Drawing.Point(25, 34); + this.button_StopPreview.Name = "button_StopPreview"; + this.button_StopPreview.Size = new System.Drawing.Size(23, 23); + this.button_StopPreview.TabIndex = 9; + this.button_StopPreview.Text = "⏸"; + this.button_StopPreview.UseVisualStyleBackColor = true; + // + // checkBox_autoPlay + // + this.checkBox_autoPlay.AutoSize = true; + this.checkBox_autoPlay.Location = new System.Drawing.Point(52, 41); + this.checkBox_autoPlay.Name = "checkBox_autoPlay"; + this.checkBox_autoPlay.Size = new System.Drawing.Size(67, 17); + this.checkBox_autoPlay.TabIndex = 22; + this.checkBox_autoPlay.Text = "Autoplay"; + this.checkBox_autoPlay.UseVisualStyleBackColor = true; + // + // button_StartPreview + // + this.button_StartPreview.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.button_StartPreview.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238))); + this.button_StartPreview.Location = new System.Drawing.Point(0, 34); + this.button_StartPreview.Name = "button_StartPreview"; + this.button_StartPreview.Size = new System.Drawing.Size(23, 23); + this.button_StartPreview.TabIndex = 10; + this.button_StartPreview.Text = "▶"; + this.button_StartPreview.UseVisualStyleBackColor = true; + // + // MusicControlView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.trackBar_Volume); + this.Controls.Add(this.panel_audioPlayback); + this.Name = "MusicControlView"; + this.Size = new System.Drawing.Size(214, 60); + ((System.ComponentModel.ISupportInitialize)(this.trackBar_Volume)).EndInit(); + this.panel_audioPlayback.ResumeLayout(false); + this.panel_audioPlayback.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.trackBar_position)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TrackBar trackBar_Volume; + private System.Windows.Forms.CheckBox checkBox_musicPlayer; + private System.Windows.Forms.Panel panel_audioPlayback; + private System.Windows.Forms.TrackBar trackBar_position; + private System.Windows.Forms.Button button_StopPreview; + private System.Windows.Forms.Button button_StartPreview; + private System.Windows.Forms.CheckBox checkBox_DT; + private System.Windows.Forms.CheckBox checkBox_autoPlay; + } +} diff --git a/GuiComponents/Controls/MusicControlView.cs b/GuiComponents/Controls/MusicControlView.cs new file mode 100644 index 0000000..5c3d729 --- /dev/null +++ b/GuiComponents/Controls/MusicControlView.cs @@ -0,0 +1,117 @@ +using System; +using System.Windows.Forms; +using Gui.Misc; +using GuiComponents.Interfaces; + +namespace GuiComponents.Controls +{ + public partial class MusicControlView : UserControl, IMusicControlView + { + public float Volume => trackBar_Volume.Value / 100f; + public int Position + { + get { return trackBar_position.Value; } + set + { + if (IsHandleCreated) + try + { + Invoke((MethodInvoker)(() => + { + trackBar_position.Value = value; + })); + } + catch { } + } + } + public bool IsMusicPlayerMode => checkBox_musicPlayer.Checked; + public bool IsAutoPlayEnabled => checkBox_autoPlay.Checked; + public bool IsDTEnabled => checkBox_DT.Checked; + public bool IsUserSeeking { get; set; } + private bool DTIsAvaliable = true; + + public event EventHandler PositionChanged; + public event EventHandler CheckboxChanged; + public event EventHandler PlayPressed; + public event EventHandler PausePressed; + public event EventHandler VolumeChanged; + + + + public MusicControlView() + { + InitializeComponent(); + Bind(); + } + + public void Bind() + { + checkBox_DT.CheckedChanged += CheckBoxChanged; + checkBox_autoPlay.CheckedChanged += CheckBoxChanged; + checkBox_musicPlayer.CheckedChanged += CheckBoxChanged; + + trackBar_Volume.ValueChanged += TrackBarVolumeChanged; + trackBar_position.MouseDown += TrackBar_position_MouseDown; + trackBar_position.MouseUp += TrackBarPositionOnMouseUp; + trackBar_position.Scroll += TrackBarPositionOnScroll; + + button_StartPreview.Click += (s, a) => OnPlayPressed(); + button_StopPreview.Click += (s, a) => OnPausePressed(); + HandleCreated += OnHandleCreated; + } + + private void OnHandleCreated(object sender, EventArgs eventArgs) + { + if (!DTIsAvaliable) + disableDt(); + } + + private void TrackBarPositionOnScroll(object sender, EventArgs eventArgs) + { + PositionChanged?.Invoke(this, new IntEventArgs(Position)); + } + + private void TrackBarPositionOnMouseUp(object sender, MouseEventArgs mouseEventArgs) + { + PositionChanged?.Invoke(this, new IntEventArgs(Position)); + IsUserSeeking = false; + } + + private void TrackBar_position_MouseDown(object sender, MouseEventArgs e) + { + IsUserSeeking = true; + } + + private void TrackBarVolumeChanged(object sender, EventArgs e) + { + VolumeChanged?.Invoke(this, new FloatEventArgs(Volume)); + } + + private void CheckBoxChanged(object sender, EventArgs eventArgs) + { + CheckboxChanged?.Invoke(this, null); + } + + public void disableDt() + { + DTIsAvaliable = false; + if (IsHandleCreated) + Invoke((MethodInvoker)(() => + { + checkBox_DT.Enabled = false; + checkBox_DT.Checked = false; + checkBox_DT.Visible = false; + })); + } + + protected virtual void OnPlayPressed() + { + PlayPressed?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnPausePressed() + { + PausePressed?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/GuiComponents/Controls/MusicControlView.resx b/GuiComponents/Controls/MusicControlView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Controls/MusicControlView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Controls/TabControlEx.cs b/GuiComponents/Controls/TabControlEx.cs new file mode 100644 index 0000000..86cf812 --- /dev/null +++ b/GuiComponents/Controls/TabControlEx.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace GuiComponents.Controls +{ + public class TabControlEx : TabControl + { + //tab border fix + protected override void WndProc(ref Message m) + { + if (m.Msg == 0x1300 + 40) + { + RECT rc = (RECT)m.GetLParam(typeof(RECT)); + rc.Left -= 4; + rc.Right += 2; + rc.Top -= 2; + rc.Bottom += 3; + Marshal.StructureToPtr(rc, m.LParam, true); + } + base.WndProc(ref m); + } + + } + internal struct RECT { public int Left, Top, Right, Bottom; } +} \ No newline at end of file diff --git a/GuiComponents/Forms/BaseForm.cs b/GuiComponents/Forms/BaseForm.cs new file mode 100644 index 0000000..d718faf --- /dev/null +++ b/GuiComponents/Forms/BaseForm.cs @@ -0,0 +1,21 @@ +using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public class BaseForm : Form, IForm + { + protected BaseForm() + { + FormClosing += (s, a) => Closing?.Invoke(this, EventArgs.Empty); + StartPosition = FormStartPosition.CenterParent; + } + public void ShowAndBlock() + { + this.ShowDialog(); + } + + public event EventHandler Closing; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/BeatmapListingForm.Designer.cs b/GuiComponents/Forms/BeatmapListingForm.Designer.cs new file mode 100644 index 0000000..9aa043e --- /dev/null +++ b/GuiComponents/Forms/BeatmapListingForm.Designer.cs @@ -0,0 +1,98 @@ +using GuiComponents.Controls; + +namespace GuiComponents.Forms +{ + partial class BeatmapListingForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.beatmapListingView1 = new GuiComponents.Controls.BeatmapListingView(); + this.combinedBeatmapPreviewView1 = new GuiComponents.Controls.CombinedBeatmapPreviewView(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.SuspendLayout(); + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(0, 0); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.beatmapListingView1); + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.combinedBeatmapPreviewView1); + this.splitContainer1.Size = new System.Drawing.Size(938, 387); + this.splitContainer1.SplitterDistance = 526; + this.splitContainer1.TabIndex = 0; + // + // beatmapListingView1 + // + this.beatmapListingView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.beatmapListingView1.Location = new System.Drawing.Point(0, 0); + this.beatmapListingView1.Name = "beatmapListingView1"; + this.beatmapListingView1.ResultText = null; + this.beatmapListingView1.Size = new System.Drawing.Size(526, 387); + this.beatmapListingView1.TabIndex = 0; + // + // combinedBeatmapPreviewView1 + // + this.combinedBeatmapPreviewView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.combinedBeatmapPreviewView1.Location = new System.Drawing.Point(0, 0); + this.combinedBeatmapPreviewView1.Name = "combinedBeatmapPreviewView1"; + this.combinedBeatmapPreviewView1.Size = new System.Drawing.Size(408, 387); + this.combinedBeatmapPreviewView1.TabIndex = 0; + // + // BeatmapListingForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(938, 387); + this.Controls.Add(this.splitContainer1); + this.Name = "BeatmapListingForm"; + this.Text = "Collection Manager - Beatmap listing"; + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.SplitContainer splitContainer1; + private BeatmapListingView beatmapListingView1; + private CombinedBeatmapPreviewView combinedBeatmapPreviewView1; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/BeatmapListingForm.cs b/GuiComponents/Forms/BeatmapListingForm.cs new file mode 100644 index 0000000..60dd5a2 --- /dev/null +++ b/GuiComponents/Forms/BeatmapListingForm.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public partial class BeatmapListingForm : BaseForm, IBeatmapListingForm + { + public BeatmapListingForm() + { + InitializeComponent(); + } + + public IBeatmapListingView BeatmapListingView => beatmapListingView1; + public ICombinedBeatmapPreviewView CombinedBeatmapPreviewView => combinedBeatmapPreviewView1; + } +} diff --git a/GuiComponents/Forms/BeatmapListingForm.resx b/GuiComponents/Forms/BeatmapListingForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Forms/BeatmapListingForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Forms/CollectionAddRenameForm.Designer.cs b/GuiComponents/Forms/CollectionAddRenameForm.Designer.cs new file mode 100644 index 0000000..a8ff74c --- /dev/null +++ b/GuiComponents/Forms/CollectionAddRenameForm.Designer.cs @@ -0,0 +1,63 @@ +namespace GuiComponents.Forms +{ + partial class CollectionAddRenameForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.collectionRenameView1 = new GuiComponents.Controls.CollectionRenameView(); + this.SuspendLayout(); + // + // collectionRenameView1 + // + this.collectionRenameView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.collectionRenameView1.IsRenameView = true; + this.collectionRenameView1.Location = new System.Drawing.Point(0, 0); + this.collectionRenameView1.Name = "collectionRenameView1"; + this.collectionRenameView1.OrginalCollectionName = "from"; + this.collectionRenameView1.Size = new System.Drawing.Size(378, 115); + this.collectionRenameView1.TabIndex = 0; + // + // CollectionAddRenameForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(378, 115); + this.Controls.Add(this.collectionRenameView1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "CollectionAddRenameForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "CollectionRenameForm"; + this.ResumeLayout(false); + + } + + #endregion + + protected Controls.CollectionRenameView collectionRenameView1; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/CollectionAddRenameForm.cs b/GuiComponents/Forms/CollectionAddRenameForm.cs new file mode 100644 index 0000000..5b2c159 --- /dev/null +++ b/GuiComponents/Forms/CollectionAddRenameForm.cs @@ -0,0 +1,37 @@ +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public partial class CollectionAddRenameForm : BaseForm, ICollectionAddRenameForm + { + public CollectionAddRenameForm() + { + InitializeComponent(); + IsRenameForm = collectionRenameView1.IsRenameView; + + AcceptButton = collectionRenameView1.button_rename; + CancelButton = collectionRenameView1.button_cancel; + } + + public ICollectionRenameView CollectionRenameView => collectionRenameView1; + private bool _isRenameForm; + + public bool IsRenameForm + { + get + { + return _isRenameForm; + } + set + { + _isRenameForm = value; + collectionRenameView1.IsRenameView = value; + if (value) + Text = "Collection Manager - Rename collection"; + else + Text = "Collection Manager - Add collection"; + } + } + } +} diff --git a/GuiComponents/Forms/CollectionAddRenameForm.resx b/GuiComponents/Forms/CollectionAddRenameForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Forms/CollectionAddRenameForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Forms/DownloadManagerFormView.Designer.cs b/GuiComponents/Forms/DownloadManagerFormView.Designer.cs new file mode 100644 index 0000000..03ebb95 --- /dev/null +++ b/GuiComponents/Forms/DownloadManagerFormView.Designer.cs @@ -0,0 +1,60 @@ +namespace GuiComponents.Forms +{ + partial class DownloadManagerFormView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.downloadManagerView1 = new GuiComponents.Controls.DownloadManagerView(); + this.SuspendLayout(); + // + // downloadManagerView1 + // + this.downloadManagerView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.downloadManagerView1.DownloadButtonIsEnabled = true; + this.downloadManagerView1.Location = new System.Drawing.Point(0, 0); + this.downloadManagerView1.Name = "downloadManagerView1"; + this.downloadManagerView1.Size = new System.Drawing.Size(514, 461); + this.downloadManagerView1.TabIndex = 0; + // + // DownloadManagerFormView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(514, 461); + this.Controls.Add(this.downloadManagerView1); + this.Name = "DownloadManagerFormView"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Collection Manager - Download list"; + this.ResumeLayout(false); + + } + + #endregion + + private Controls.DownloadManagerView downloadManagerView1; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/DownloadManagerFormView.cs b/GuiComponents/Forms/DownloadManagerFormView.cs new file mode 100644 index 0000000..3245ca5 --- /dev/null +++ b/GuiComponents/Forms/DownloadManagerFormView.cs @@ -0,0 +1,16 @@ +using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public partial class DownloadManagerFormView : BaseForm, IDownloadManagerFormView + { + public DownloadManagerFormView() + { + InitializeComponent(); + } + + public IDownloadManagerView DownloadManagerView => downloadManagerView1; + } +} diff --git a/GuiComponents/Forms/DownloadManagerFormView.resx b/GuiComponents/Forms/DownloadManagerFormView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Forms/DownloadManagerFormView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Forms/LoginFormView.Designer.cs b/GuiComponents/Forms/LoginFormView.Designer.cs new file mode 100644 index 0000000..077159d --- /dev/null +++ b/GuiComponents/Forms/LoginFormView.Designer.cs @@ -0,0 +1,118 @@ +namespace GuiComponents.Forms +{ + partial class LoginFormView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button_Cancel = new System.Windows.Forms.Button(); + this.button_Login = new System.Windows.Forms.Button(); + this.label2 = new System.Windows.Forms.Label(); + this.textBox_password = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.textBox_login = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // button_Cancel + // + this.button_Cancel.Location = new System.Drawing.Point(183, 72); + this.button_Cancel.Name = "button_Cancel"; + this.button_Cancel.Size = new System.Drawing.Size(75, 23); + this.button_Cancel.TabIndex = 11; + this.button_Cancel.Text = "Cancel"; + this.button_Cancel.UseVisualStyleBackColor = true; + // + // button_Login + // + this.button_Login.Location = new System.Drawing.Point(77, 72); + this.button_Login.Name = "button_Login"; + this.button_Login.Size = new System.Drawing.Size(75, 23); + this.button_Login.TabIndex = 10; + this.button_Login.Text = "Login"; + this.button_Login.UseVisualStyleBackColor = true; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(74, 39); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(78, 13); + this.label2.TabIndex = 9; + this.label2.Text = "osu! password:"; + // + // textBox_password + // + this.textBox_password.Location = new System.Drawing.Point(158, 36); + this.textBox_password.Name = "textBox_password"; + this.textBox_password.PasswordChar = '*'; + this.textBox_password.Size = new System.Drawing.Size(100, 20); + this.textBox_password.TabIndex = 8; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(97, 13); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(55, 13); + this.label1.TabIndex = 7; + this.label1.Text = "osu! login:"; + // + // textBox_login + // + this.textBox_login.Location = new System.Drawing.Point(158, 10); + this.textBox_login.Name = "textBox_login"; + this.textBox_login.Size = new System.Drawing.Size(100, 20); + this.textBox_login.TabIndex = 6; + // + // LoginFormView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(334, 101); + this.Controls.Add(this.button_Cancel); + this.Controls.Add(this.button_Login); + this.Controls.Add(this.label2); + this.Controls.Add(this.textBox_password); + this.Controls.Add(this.label1); + this.Controls.Add(this.textBox_login); + this.Name = "LoginFormView"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Collection Manager - osu! login form"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button button_Cancel; + private System.Windows.Forms.Button button_Login; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.TextBox textBox_password; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.TextBox textBox_login; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/LoginFormView.cs b/GuiComponents/Forms/LoginFormView.cs new file mode 100644 index 0000000..8fe03a6 --- /dev/null +++ b/GuiComponents/Forms/LoginFormView.cs @@ -0,0 +1,24 @@ +using System; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public partial class LoginFormView : BaseForm, ILoginFormView + { + public LoginFormView() + { + InitializeComponent(); + button_Login.Click += (s, a) => { ClickedLogin = true; this.Close(); }; + button_Login.Click += (s, a) => { this.Close(); }; + AcceptButton = button_Login; + CancelButton = button_Cancel; + } + + + public string Login => ClickedLogin ? textBox_login.Text : ""; + public string Password => ClickedLogin ? textBox_password.Text : ""; + public bool ClickedLogin { get; set; } + public event EventHandler LoginClick; + public event EventHandler CancelClick; + } +} diff --git a/GuiComponents/Forms/LoginFormView.resx b/GuiComponents/Forms/LoginFormView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Forms/LoginFormView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/Forms/MainFormView.Designer.cs b/GuiComponents/Forms/MainFormView.Designer.cs new file mode 100644 index 0000000..9a0190e --- /dev/null +++ b/GuiComponents/Forms/MainFormView.Designer.cs @@ -0,0 +1,175 @@ +using GuiComponents.Controls; + +namespace GuiComponents.Forms +{ + partial class MainFormView + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.combinedListingView1 = new GuiComponents.Controls.CombinedListingView(); + this.mainSidePanelView1 = new GuiComponents.Controls.MainSidePanelView(); + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.tabControlEx1 = new GuiComponents.Controls.TabControlEx(); + this.tabPage1 = new System.Windows.Forms.TabPage(); + this.combinedBeatmapPreviewView1 = new GuiComponents.Controls.CombinedBeatmapPreviewView(); + this.tabPage2 = new System.Windows.Forms.TabPage(); + this.collectionTextView1 = new GuiComponents.Controls.CollectionTextView(); + this.infoTextView1 = new GuiComponents.Controls.InfoTextView(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.tabControlEx1.SuspendLayout(); + this.tabPage1.SuspendLayout(); + this.tabPage2.SuspendLayout(); + this.SuspendLayout(); + // + // combinedListingView1 + // + this.combinedListingView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.combinedListingView1.Location = new System.Drawing.Point(0, 0); + this.combinedListingView1.Name = "combinedListingView1"; + this.combinedListingView1.Size = new System.Drawing.Size(759, 519); + this.combinedListingView1.TabIndex = 0; + // + // mainSidePanelView1 + // + this.mainSidePanelView1.Location = new System.Drawing.Point(3, 3); + this.mainSidePanelView1.Name = "mainSidePanelView1"; + this.mainSidePanelView1.Size = new System.Drawing.Size(184, 428); + this.mainSidePanelView1.TabIndex = 1; + // + // splitContainer1 + // + this.splitContainer1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.splitContainer1.Location = new System.Drawing.Point(193, 12); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.combinedListingView1); + this.splitContainer1.Panel1MinSize = 50; + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.tabControlEx1); + this.splitContainer1.Size = new System.Drawing.Size(1161, 519); + this.splitContainer1.SplitterDistance = 759; + this.splitContainer1.TabIndex = 3; + // + // tabControlEx1 + // + this.tabControlEx1.Controls.Add(this.tabPage1); + this.tabControlEx1.Controls.Add(this.tabPage2); + this.tabControlEx1.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControlEx1.Location = new System.Drawing.Point(0, 0); + this.tabControlEx1.Name = "tabControlEx1"; + this.tabControlEx1.SelectedIndex = 0; + this.tabControlEx1.Size = new System.Drawing.Size(398, 519); + this.tabControlEx1.TabIndex = 0; + // + // tabPage1 + // + this.tabPage1.BackColor = System.Drawing.SystemColors.Control; + this.tabPage1.Controls.Add(this.combinedBeatmapPreviewView1); + this.tabPage1.Location = new System.Drawing.Point(0, 20); + this.tabPage1.Name = "tabPage1"; + this.tabPage1.Padding = new System.Windows.Forms.Padding(3); + this.tabPage1.Size = new System.Drawing.Size(396, 498); + this.tabPage1.TabIndex = 0; + this.tabPage1.Text = "map preview"; + // + // combinedBeatmapPreviewView1 + // + this.combinedBeatmapPreviewView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.combinedBeatmapPreviewView1.Location = new System.Drawing.Point(3, 3); + this.combinedBeatmapPreviewView1.Name = "combinedBeatmapPreviewView1"; + this.combinedBeatmapPreviewView1.Size = new System.Drawing.Size(390, 492); + this.combinedBeatmapPreviewView1.TabIndex = 0; + // + // tabPage2 + // + this.tabPage2.BackColor = System.Drawing.SystemColors.Control; + this.tabPage2.Controls.Add(this.collectionTextView1); + this.tabPage2.Location = new System.Drawing.Point(0, 20); + this.tabPage2.Name = "tabPage2"; + this.tabPage2.Padding = new System.Windows.Forms.Padding(3); + this.tabPage2.Size = new System.Drawing.Size(396, 498); + this.tabPage2.TabIndex = 1; + this.tabPage2.Text = "Collection text"; + // + // collectionTextView1 + // + this.collectionTextView1.Dock = System.Windows.Forms.DockStyle.Fill; + this.collectionTextView1.Location = new System.Drawing.Point(3, 3); + this.collectionTextView1.Name = "collectionTextView1"; + this.collectionTextView1.Size = new System.Drawing.Size(390, 492); + this.collectionTextView1.TabIndex = 0; + // + // infoTextView1 + // + this.infoTextView1.Location = new System.Drawing.Point(3, 438); + this.infoTextView1.Name = "infoTextView1"; + this.infoTextView1.Size = new System.Drawing.Size(182, 89); + this.infoTextView1.TabIndex = 4; + // + // MainFormView + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(1356, 531); + this.Controls.Add(this.infoTextView1); + this.Controls.Add(this.splitContainer1); + this.Controls.Add(this.mainSidePanelView1); + this.Name = "MainFormView"; + this.Text = "Collection Manager by Piotrekol"; + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.tabControlEx1.ResumeLayout(false); + this.tabPage1.ResumeLayout(false); + this.tabPage2.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private CombinedListingView combinedListingView1; + private Controls.MainSidePanelView mainSidePanelView1; + private System.Windows.Forms.SplitContainer splitContainer1; + private TabControlEx tabControlEx1; + private System.Windows.Forms.TabPage tabPage1; + private System.Windows.Forms.TabPage tabPage2; + private Controls.CollectionTextView collectionTextView1; + private CombinedBeatmapPreviewView combinedBeatmapPreviewView1; + private InfoTextView infoTextView1; + } +} \ No newline at end of file diff --git a/GuiComponents/Forms/MainFormView.cs b/GuiComponents/Forms/MainFormView.cs new file mode 100644 index 0000000..8c37196 --- /dev/null +++ b/GuiComponents/Forms/MainFormView.cs @@ -0,0 +1,21 @@ + using System; +using System.Windows.Forms; +using GuiComponents.Interfaces; + +namespace GuiComponents.Forms +{ + public partial class MainFormView : BaseForm, IMainFormView + { + public MainFormView() + { + InitializeComponent(); + splitContainer1.Paint += Helpers.SplitterPaint; + } + + public ICombinedListingView CombinedListingView => combinedListingView1; + public ICombinedBeatmapPreviewView CombinedBeatmapPreviewView => combinedBeatmapPreviewView1; + public IMainSidePanelView SidePanelView => mainSidePanelView1; + public ICollectionTextView CollectionTextView => collectionTextView1; + public IInfoTextView InfoTextView => infoTextView1; + } +} diff --git a/GuiComponents/Forms/MainFormView.resx b/GuiComponents/Forms/MainFormView.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/GuiComponents/Forms/MainFormView.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GuiComponents/GuiComponents.csproj b/GuiComponents/GuiComponents.csproj new file mode 100644 index 0000000..856fe01 --- /dev/null +++ b/GuiComponents/GuiComponents.csproj @@ -0,0 +1,231 @@ + + + + + Debug + AnyCPU + {9F6C4BFE-5696-4513-BB06-90DC14A56CCF} + Library + Properties + GuiComponents + GuiComponents + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + full + true + + + + + + + + + + + + + + + UserControl + + + BeatmapListingView.cs + + + UserControl + + + BeatmapThumbnailView.cs + + + UserControl + + + CollectionListingView.cs + + + UserControl + + + CollectionRenameView.cs + + + UserControl + + + CollectionTextView.cs + + + UserControl + + + CombinedBeatmapPreviewView.cs + + + UserControl + + + CombinedListingView.cs + + + UserControl + + + DownloadManagerView.cs + + + UserControl + + + InfoTextView.cs + + + UserControl + + + MainSidePanelView.cs + + + UserControl + + + MusicControlView.cs + + + Component + + + Form + + + Form + + + BeatmapListingForm.cs + + + Form + + + CollectionAddRenameForm.cs + + + Form + + + DownloadManagerFormView.cs + + + Form + + + LoginFormView.cs + + + Form + + + MainFormView.cs + + + + + + + + BeatmapListingView.cs + + + BeatmapThumbnailView.cs + + + CollectionListingView.cs + + + CollectionRenameView.cs + + + CollectionTextView.cs + + + CombinedBeatmapPreviewView.cs + + + CombinedListingView.cs + + + DownloadManagerView.cs + + + InfoTextView.cs + + + MainSidePanelView.cs + + + MusicControlView.cs + + + BeatmapListingForm.cs + + + CollectionAddRenameForm.cs + + + DownloadManagerFormView.cs + + + LoginFormView.cs + + + MainFormView.cs + + + + + {533ab47a-d1b5-45db-a37e-f053fa3699c4} + CollectionManagerDll + + + {2bdf5d5f-1cb0-47a6-8138-e4db961740f2} + CollectionManagerExtensionsDll + + + {14768636-102a-4a27-ab5a-9b5d1ba316a6} + Common + + + {18feda0c-d147-4286-b39a-01204808106a} + ObjectListView2012 + + + + + \ No newline at end of file diff --git a/GuiComponents/Helpers.cs b/GuiComponents/Helpers.cs new file mode 100644 index 0000000..cab866a --- /dev/null +++ b/GuiComponents/Helpers.cs @@ -0,0 +1,46 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using BrightIdeasSoftware; + +namespace GuiComponents +{ + internal static class Helpers + { + public static void SelectNextOrFirst(this FastObjectListView list) + { + if (list.InvokeRequired) + { + list.Invoke((Action)(() => + { + list.SelectNextOrFirst(); + })); + return; + } + var nextItem = list.GetNextItem(list.SelectedItem); + if (nextItem == null) + nextItem = list.GetNextItem(null); + list.SelectedItem = nextItem; + } + public static void SplitterPaint(object sender, PaintEventArgs e) + { + SplitContainer s = sender as SplitContainer; + if (s != null) + { + int top = 5; + int bottom = s.Height - 5; + int left = s.SplitterDistance; + int right = left + s.SplitterWidth - 1; + e.Graphics.DrawLine(Pens.Silver, left, top, left, bottom); + e.Graphics.DrawLine(Pens.Silver, right, top, right, bottom); + int halfWidth = s.SplitterWidth / 2; + + for (int CurrBottom = bottom; CurrBottom > top; CurrBottom -= 10) + { + //e.Graphics.DrawLine(Pens.Silver, left, top, left, bottom); + e.Graphics.DrawLine(Pens.Silver, left, CurrBottom, left + halfWidth, CurrBottom + 5); + } + } + } + } +} \ No newline at end of file diff --git a/GuiComponents/Properties/AssemblyInfo.cs b/GuiComponents/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a3f3dfe --- /dev/null +++ b/GuiComponents/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GuiComponents")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GuiComponents")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9f6c4bfe-5696-4513-bb06-90dc14a56ccf")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/GuiComponents/UserDialogs.cs b/GuiComponents/UserDialogs.cs new file mode 100644 index 0000000..4dcbecc --- /dev/null +++ b/GuiComponents/UserDialogs.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Windows.Forms; +using Common; +using GuiComponents.Interfaces; + +namespace GuiComponents +{ + public class UserDialogs : IUserDialogs + { + public bool IsThisPathCorrect(string path) + { + var dialogResult = MessageBox.Show( + "Detected osu in: " + Environment.NewLine + path + Environment.NewLine + "Is this correct?", + "osu! directory", MessageBoxButtons.YesNo, MessageBoxIcon.Asterisk); + return dialogResult == DialogResult.Yes; + } + + + public string SelectDirectory(string text) + { + return SelectDirectory(text, false); + } + public string SelectDirectory(string text, bool showNewFolder = false) + { + FolderBrowserDialog dialog = new FolderBrowserDialog(); + //set description and base folder for browsing + + dialog.ShowNewFolderButton = true; + dialog.Description = text; + dialog.RootFolder = Environment.SpecialFolder.MyComputer; + if (dialog.ShowDialog() == DialogResult.OK && Directory.Exists((dialog.SelectedPath))) + { + return dialog.SelectedPath; + } + return string.Empty; + } + public string SelectFile(string text, string types = "", string filename = "") + { + OpenFileDialog openFileDialog = new OpenFileDialog(); + openFileDialog.Filter = types; //"Collection database (*.db)|*.db"; + openFileDialog.FileName = filename; //"collection.db"; + openFileDialog.Multiselect = false; + var result = openFileDialog.ShowDialog(); + if (result == DialogResult.OK) + return openFileDialog.FileName; + else + return string.Empty; + } + public string SaveFile(string title, string types = "all|*.*") + { + SaveFileDialog saveFileDialog = new SaveFileDialog + { + Filter = types, + Title = title + }; + saveFileDialog.ShowDialog(); + if (saveFileDialog.FileName != "") + { + return saveFileDialog.FileName; + } + return string.Empty; + } + + public bool YesNoMessageBox(string text, string caption, MessageBoxType messageBoxType = MessageBoxType.Info) + { + var icon = GetMessageBoxIcon(messageBoxType); + + var dialogResult = MessageBox.Show(null, caption, text, MessageBoxButtons.YesNo,icon); + + return dialogResult == DialogResult.Yes; + } + public void OkMessageBox(string text, string caption, MessageBoxType messageBoxType = MessageBoxType.Info) + { + var icon = GetMessageBoxIcon(messageBoxType); + + MessageBox.Show(null, text, caption, MessageBoxButtons.OK, icon); + } + + private MessageBoxIcon GetMessageBoxIcon(MessageBoxType messageBoxType) + { + MessageBoxIcon icon; + switch (messageBoxType) + { + case MessageBoxType.Error: + icon = MessageBoxIcon.Error; + break; + case MessageBoxType.Info: + icon = MessageBoxIcon.Information; + break; + case MessageBoxType.Question: + icon = MessageBoxIcon.Question; + break; + default: + icon = MessageBoxIcon.Information; + break; + } + return icon; + } + } +} \ No newline at end of file diff --git a/InnoSetup/afterInstall.txt b/InnoSetup/afterInstall.txt new file mode 100644 index 0000000..e69de29 diff --git a/InnoSetup/beforeInstall.txt b/InnoSetup/beforeInstall.txt new file mode 100644 index 0000000..e69de29 diff --git a/InnoSetup/license.txt b/InnoSetup/license.txt new file mode 100644 index 0000000..e69de29 diff --git a/InnoSetup/script.iss b/InnoSetup/script.iss new file mode 100644 index 0000000..89d8942 --- /dev/null +++ b/InnoSetup/script.iss @@ -0,0 +1,61 @@ +; Script generated by the Inno Script Studio Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "Collection Manager" +#define MyAppVersion "4.2" +#define MyAppPublisher "Piotrekol" +#define MyAppURL "http://osustats.ppy.sh" +#define MyAppExeName "App.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{53A1BDF1-29D1-47BC-BB12-C48B0AC2C636} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={pf}\{#MyAppName} +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +LicenseFile=D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\InnoSetup\license.txt +OutputDir=D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\InnoSetup\output +OutputBaseFilename=CollectionManagerSetup +Compression=lzma +SolidCompression=yes + +[CustomMessages] +MyAppOld=The Setup detected application version +MyAppRequired=The installation of {#MyAppName} requires MyApp to be installed.%nInstall MyApp before installing this update.%n%n +MyAppTerminated=The setup of update will be terminated. + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 + +[Files] +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\App.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\CollectionManager.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\CollectionManagerExtensionsDll.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\Common.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\GuiComponents.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\MusicPlayer.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\NAudio.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "D:\Kod\osu-related\Tools\osu!CollectionEditor\v3\App\bin\Release\ObjectListView.dll"; DestDir: "{app}"; Flags: ignoreversion +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/MusicPlayer/AudioFileReaderEx.cs b/MusicPlayer/AudioFileReaderEx.cs new file mode 100644 index 0000000..55ff55c --- /dev/null +++ b/MusicPlayer/AudioFileReaderEx.cs @@ -0,0 +1,25 @@ +using NAudio.Wave; + +namespace MusicPlayer +{ + public class AudioFileReaderEx : AudioFileReader + { + public AudioFileReaderEx(string fileName) : base(fileName) + { + AudioFileLocation = fileName; + } + + public string AudioFileLocation { get; } + public bool Reused { get; private set; } = false; + public AudioFileReaderEx GetAudio(string fileName) + { + if (fileName == AudioFileLocation) + { + this.Reused = true; + this.SetPosition(0d); + return this; + } + return new AudioFileReaderEx(fileName); + } + } +} \ No newline at end of file diff --git a/MusicPlayer/AudioPlayer.cs b/MusicPlayer/AudioPlayer.cs new file mode 100644 index 0000000..a4db174 --- /dev/null +++ b/MusicPlayer/AudioPlayer.cs @@ -0,0 +1,167 @@ +using System; +using NAudio.Wave; + +namespace MusicPlayer +{ + internal abstract class AudioPlayer : IMusicPlayer + { + #region IMusicPlayer members + public double CurrentTime => _audioFileReader?.CurrentTime.TotalSeconds ?? -1; + public double TotalTime => _audioFileReader?.TotalTime.TotalSeconds ?? -1; + + public bool IsPlaying => _waveOutDevice?.PlaybackState == PlaybackState.Playing; + + public virtual bool IsSpeedControlAvaliable => false; + public bool IsPaused => _waveOutDevice?.PlaybackState == PlaybackState.Paused; + private float _soundVolume = 0.3f; + protected bool _playbackAborted = false; + public float SoundVolume + { + get { return _soundVolume; } + set + { + lock (_lockingObject) + { + _soundVolume = value; + if (_audioFileReader != null) + _audioFileReader.Volume = value; + } + } + } + public event EventHandler PlaybackFinished; + + public void Seek(double position) + { + if (position > TotalTime) + position = TotalTime; + //Sound samples are read in 100ms intervals, so we need to leave atleast that much + //for everything to still work correctly after skipping part of song. + //This ensures that atleast 150ms is left, assuming that position is within TotalTime. + var enoughLeft = (TotalTime - position) < 0.15; + if (enoughLeft) + position -= 0.15; + + this._audioFileReader.SetPosition(position); + } + + public void SetVolume(float volume) + { + SoundVolume = volume; + } + + public virtual void SetSpeed(float speed) + { + throw new NotImplementedException(); + } + + public virtual void Play(string audioFile, int startTime) + { + lock (_lockingObject) + { + var audioReader = CreateAudioReader(audioFile); + if (audioReader != null) + { + SetAudioReader(audioReader); + Play(startTime); + } + else + { + CleanAfterLastPlayback(); + WaveOutDevice_PlaybackStopped(this, null); + } + } + } + public void Resume() + { + _waveOutDevice.Play(); + } + public void Pause() + { + _waveOutDevice.Pause(); + } + #endregion + + protected readonly object _lockingObject = new object(); + protected AudioFileReaderEx _audioFileReader; + protected IWavePlayer _waveOutDevice; + + protected AudioFileReaderEx CreateAudioReader(string audioFile) + { + if (audioFile.ToLower().EndsWith(".ogg")) + {//Unsupported audio type + return null; + } + if (_audioFileReader == null) + return new AudioFileReaderEx(audioFile); + else + return _audioFileReader.GetAudio(audioFile); + } + protected void SetAudioReader(AudioFileReaderEx autioReader) + { + if (!autioReader.Equals(_audioFileReader)) + CleanAfterLastPlayback(); + + _audioFileReader = autioReader; + if (_audioFileReader.Reused) + return; + _waveOutDevice = new WaveOut(); + _waveOutDevice.PlaybackStopped += WaveOutDevice_PlaybackStopped; + } + + protected virtual void Play(int startTime) + { + if (_audioFileReader == null) + return; + StopPlayback(); + _playbackAborted = false; + if (!_audioFileReader.Reused) + _waveOutDevice.Init(_audioFileReader); + _audioFileReader.Volume = SoundVolume; + _audioFileReader.Skip(startTime); + _waveOutDevice.Play(); + + } + private void CleanAfterLastPlayback() + { + lock (_lockingObject) + { + if (_waveOutDevice != null) + StopPlayback(); + if (_audioFileReader != null) + { + _audioFileReader.Dispose(); + _audioFileReader = null; + } + if (_waveOutDevice != null) + { + _waveOutDevice.PlaybackStopped -= WaveOutDevice_PlaybackStopped; + _waveOutDevice.Dispose(); + _waveOutDevice = null; + } + } + GC.Collect(); + } + public void StopPlayback() + { + if (_waveOutDevice.PlaybackState == PlaybackState.Playing) + { + _playbackAborted = true; + var oldVolume = _audioFileReader.Volume; + _audioFileReader.Volume = 0f; + _waveOutDevice.Stop(); + _audioFileReader.Volume = oldVolume; + } + } + protected virtual void WaveOutDevice_PlaybackStopped(object sender, StoppedEventArgs e) + { + if (_playbackAborted) + return; + PlaybackFinished?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + CleanAfterLastPlayback(); + } + } +} \ No newline at end of file diff --git a/MusicPlayer/IMusicPlayer.cs b/MusicPlayer/IMusicPlayer.cs new file mode 100644 index 0000000..11e4573 --- /dev/null +++ b/MusicPlayer/IMusicPlayer.cs @@ -0,0 +1,21 @@ +using System; + +namespace MusicPlayer +{ + public interface IMusicPlayer :IDisposable + { + void Seek(double position); + void SetVolume(float volume); + void SetSpeed(float speed); + void Play(string audioFile, int startTime); + void Pause(); + void Resume(); + bool IsPlaying { get; } + double TotalTime { get; } + double CurrentTime { get; } + bool IsSpeedControlAvaliable { get; } + bool IsPaused { get; } + float SoundVolume { get; } + event EventHandler PlaybackFinished; + } +} \ No newline at end of file diff --git a/MusicPlayer/MusicPlayer.csproj b/MusicPlayer/MusicPlayer.csproj new file mode 100644 index 0000000..7d741f1 --- /dev/null +++ b/MusicPlayer/MusicPlayer.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {573A1557-C916-4ABE-BD52-94760D19CC29} + Library + Properties + MusicPlayer + MusicPlayer + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + bin\Remote Debug\ + + + + False + NAudio\NAudio.dll + + + False + NAudio\NAudio.WindowsMediaFormat.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MusicPlayer/MusicPlayerManager.cs b/MusicPlayer/MusicPlayerManager.cs new file mode 100644 index 0000000..9159705 --- /dev/null +++ b/MusicPlayer/MusicPlayerManager.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; + +namespace MusicPlayer +{ + public class MusicPlayerManager : IMusicPlayer + { + + public bool IsPlaying => musicPlayer.IsPlaying; + public double TotalTime => musicPlayer.TotalTime; + public double CurrentTime => musicPlayer.CurrentTime; + public bool IsSpeedControlAvaliable => musicPlayer.IsSpeedControlAvaliable; + public bool IsPaused => musicPlayer.IsPaused; + public float SoundVolume => musicPlayer.SoundVolume; + + private readonly IMusicPlayer musicPlayer; + + public event EventHandler PlaybackFinished + { + add { musicPlayer.PlaybackFinished += value; } + remove { musicPlayer.PlaybackFinished -= value; } + } + + public MusicPlayerManager() + { + if (File.Exists("SoundTouch.dll") && File.Exists("SoundTouch_x64.dll")) + musicPlayer = new SoundTouchPlayer(); + else + musicPlayer = new NAudioPlayer(); + } + public void Seek(double position) + { + musicPlayer.Seek(position); + } + + public void SetVolume(float volume) + { + musicPlayer.SetVolume(volume); + } + + public void SetSpeed(float speed) + { + musicPlayer.SetSpeed(speed); + } + + public void Play(string audioFile, int startTime) + { + musicPlayer.Play(audioFile, startTime); + } + + public void Pause() + { + musicPlayer.Pause(); + } + + public void Resume() + { + musicPlayer.Resume(); + } + + public void Dispose() + { + musicPlayer.Dispose(); + } + } +} diff --git a/MusicPlayer/NAudio/NAudio.WindowsMediaFormat.dll b/MusicPlayer/NAudio/NAudio.WindowsMediaFormat.dll new file mode 100644 index 0000000..49de931 Binary files /dev/null and b/MusicPlayer/NAudio/NAudio.WindowsMediaFormat.dll differ diff --git a/MusicPlayer/NAudio/NAudio.dll b/MusicPlayer/NAudio/NAudio.dll new file mode 100644 index 0000000..9dd5ae7 Binary files /dev/null and b/MusicPlayer/NAudio/NAudio.dll differ diff --git a/MusicPlayer/NAudio/NAudio.xml b/MusicPlayer/NAudio/NAudio.xml new file mode 100644 index 0000000..25602d9 --- /dev/null +++ b/MusicPlayer/NAudio/NAudio.xml @@ -0,0 +1,21714 @@ + + + + NAudio + + + + + a-law decoder + based on code from: + http://hazelware.luggle.com/tutorials/mulawcompression.html + + + + + only 512 bytes required, so just use a lookup + + + + + Converts an a-law encoded byte to a 16 bit linear sample + + a-law encoded byte + Linear sample + + + + A-law encoder + + + + + Encodes a single 16 bit sample to a-law + + 16 bit PCM sample + a-law encoded byte + + + + SpanDSP - a series of DSP components for telephony + + g722_decode.c - The ITU G.722 codec, decode part. + + Written by Steve Underwood <steveu@coppice.org> + + Copyright (C) 2005 Steve Underwood + Ported to C# by Mark Heath 2011 + + Despite my general liking of the GPL, I place my own contributions + to this code in the public domain for the benefit of all mankind - + even the slimy ones who might try to proprietize my work and use it + to my detriment. + + Based in part on a single channel G.722 codec which is: + Copyright (c) CMU 1993 + Computer Science, Speech Group + Chengxiang Lu and Alex Hauptmann + + + + + hard limits to 16 bit samples + + + + + Decodes a buffer of G722 + + Codec state + Output buffer (to contain decompressed PCM samples) + + Number of bytes in input G722 data to decode + Number of samples written into output buffer + + + + Encodes a buffer of G722 + + Codec state + Output buffer (to contain encoded G722) + PCM 16 bit samples to encode + Number of samples in the input buffer to encode + Number of encoded bytes written into output buffer + + + + Stores state to be used between calls to Encode or Decode + + + + + Creates a new instance of G722 Codec State for a + new encode or decode session + + Bitrate (typically 64000) + Special options + + + + ITU Test Mode + TRUE if the operating in the special ITU test mode, with the band split filters disabled. + + + + + TRUE if the G.722 data is packed + + + + + 8kHz Sampling + TRUE if encode from 8k samples/second + + + + + Bits Per Sample + 6 for 48000kbps, 7 for 56000kbps, or 8 for 64000kbps. + + + + + Signal history for the QMF (x) + + + + + Band + + + + + In bit buffer + + + + + Number of bits in InBuffer + + + + + Out bit buffer + + + + + Number of bits in OutBuffer + + + + + Band data for G722 Codec + + + + s + + + sp + + + sz + + + r + + + a + + + ap + + + p + + + d + + + b + + + bp + + + sg + + + nb + + + det + + + + G722 Flags + + + + + None + + + + + Using a G722 sample rate of 8000 + + + + + Packed + + + + + mu-law decoder + based on code from: + http://hazelware.luggle.com/tutorials/mulawcompression.html + + + + + only 512 bytes required, so just use a lookup + + + + + Converts a mu-law encoded byte to a 16 bit linear sample + + mu-law encoded byte + Linear sample + + + + mu-law encoder + based on code from: + http://hazelware.luggle.com/tutorials/mulawcompression.html + + + + + Encodes a single 16 bit sample to mu-law + + 16 bit PCM sample + mu-law encoded byte + + + + Audio Capture Client + + + + + Gets a pointer to the buffer + + Pointer to the buffer + + + + Gets a pointer to the buffer + + Number of frames to read + Buffer flags + Pointer to the buffer + + + + Gets the size of the next packet + + + + + Release buffer + + Number of frames written + + + + Release the COM object + + + + + Windows CoreAudio AudioClient + + + + + Initializes the Audio Client + + Share Mode + Stream Flags + Buffer Duration + Periodicity + Wave Format + Audio Session GUID (can be null) + + + + Determines whether if the specified output format is supported + + The share mode. + The desired format. + True if the format is supported + + + + Determines if the specified output format is supported in shared mode + + Share Mode + Desired Format + Output The closest match format. + True if the format is supported + + + + Starts the audio stream + + + + + Stops the audio stream. + + + + + Set the Event Handle for buffer synchro. + + The Wait Handle to setup + + + + Resets the audio stream + Reset is a control method that the client calls to reset a stopped audio stream. + Resetting the stream flushes all pending data and resets the audio clock stream + position to 0. This method fails if it is called on a stream that is not stopped + + + + + Dispose + + + + + Retrieves the stream format that the audio engine uses for its internal processing of shared-mode streams. + Can be called before initialize + + + + + Retrieves the size (maximum capacity) of the audio buffer associated with the endpoint. (must initialize first) + + + + + Retrieves the maximum latency for the current stream and can be called any time after the stream has been initialized. + + + + + Retrieves the number of frames of padding in the endpoint buffer (must initialize first) + + + + + Retrieves the length of the periodic interval separating successive processing passes by the audio engine on the data in the endpoint buffer. + (can be called before initialize) + + + + + Gets the minimum device period + (can be called before initialize) + + + + + Returns the AudioStreamVolume service for this AudioClient. + + + This returns the AudioStreamVolume object ONLY for shared audio streams. + + + This is thrown when an exclusive audio stream is being used. + + + + + Gets the AudioClockClient service + + + + + Gets the AudioRenderClient service + + + + + Gets the AudioCaptureClient service + + + + + Audio Client Buffer Flags + + + + + None + + + + + AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY + + + + + AUDCLNT_BUFFERFLAGS_SILENT + + + + + AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR + + + + + The AudioClientProperties structure is used to set the parameters that describe the properties of the client's audio stream. + + http://msdn.microsoft.com/en-us/library/windows/desktop/hh968105(v=vs.85).aspx + + + + The size of the buffer for the audio stream. + + + + + Boolean value to indicate whether or not the audio stream is hardware-offloaded + + + + + An enumeration that is used to specify the category of the audio stream. + + + + + A bit-field describing the characteristics of the stream. Supported in Windows 8.1 and later. + + + + + AUDCLNT_SHAREMODE + + + + + AUDCLNT_SHAREMODE_SHARED, + + + + + AUDCLNT_SHAREMODE_EXCLUSIVE + + + + + AUDCLNT_STREAMFLAGS + + + + + None + + + + + AUDCLNT_STREAMFLAGS_CROSSPROCESS + + + + + AUDCLNT_STREAMFLAGS_LOOPBACK + + + + + AUDCLNT_STREAMFLAGS_EVENTCALLBACK + + + + + AUDCLNT_STREAMFLAGS_NOPERSIST + + + + + Defines values that describe the characteristics of an audio stream. + + + + + No stream options. + + + + + The audio stream is a 'raw' stream that bypasses all signal processing except for endpoint specific, always-on processing in the APO, driver, and hardware. + + + + + Audio Clock Client + + + + + Get Position + + + + + Dispose + + + + + Characteristics + + + + + Frequency + + + + + Adjusted Position + + + + + Can Adjust Position + + + + + Audio Endpoint Volume Channel + + + + + Volume Level + + + + + Volume Level Scalar + + + + + Audio Endpoint Volume Channels + + + + + Channel Count + + + + + Indexer - get a specific channel + + + + + Audio Endpoint Volume Notifiaction Delegate + + Audio Volume Notification Data + + + + Audio Endpoint Volume Step Information + + + + + Step + + + + + StepCount + + + + + Audio Endpoint Volume Volume Range + + + + + Minimum Decibels + + + + + Maximum Decibels + + + + + Increment Decibels + + + + + Audio Meter Information Channels + + + + + Metering Channel Count + + + + + Get Peak value + + Channel index + Peak value + + + + Audio Render Client + + + + + Gets a pointer to the buffer + + Number of frames requested + Pointer to the buffer + + + + Release buffer + + Number of frames written + Buffer flags + + + + Release the COM object + + + + + AudioSessionControl object for information + regarding an audio session + + + + + Constructor. + + + + + + Dispose + + + + + Finalizer + + + + + the grouping param for an audio session grouping + + + + + + For chanigng the grouping param and supplying the context of said change + + + + + + + Registers an even client for callbacks + + + + + + Unregisters an event client from receiving callbacks + + + + + + Audio meter information of the audio session. + + + + + Simple audio volume of the audio session (for volume and mute status). + + + + + The current state of the audio session. + + + + + The name of the audio session. + + + + + the path to the icon shown in the mixer. + + + + + The session identifier of the audio session. + + + + + The session instance identifier of the audio session. + + + + + The process identifier of the audio session. + + + + + Is the session a system sounds session. + + + + + AudioSessionEvents callback implementation + + + + + Windows CoreAudio IAudioSessionControl interface + Defined in AudioPolicy.h + + + + + Notifies the client that the display name for the session has changed. + + The new display name for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the display icon for the session has changed. + + The path for the new display icon for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the volume level or muting state of the session has changed. + + The new volume level for the audio session. + The new muting state. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the volume level of an audio channel in the session submix has changed. + + The channel count. + An array of volumnes cooresponding with each channel index. + The number of the channel whose volume level changed. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the grouping parameter for the session has changed. + + The new grouping parameter for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the stream-activity state of the session has changed. + + The new session state. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the session has been disconnected. + + The reason that the audio session was disconnected. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Constructor. + + + + + + Notifies the client that the display name for the session has changed. + + The new display name for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the display icon for the session has changed. + + The path for the new display icon for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the volume level or muting state of the session has changed. + + The new volume level for the audio session. + The new muting state. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the volume level of an audio channel in the session submix has changed. + + The channel count. + An array of volumnes cooresponding with each channel index. + The number of the channel whose volume level changed. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the grouping parameter for the session has changed. + + The new grouping parameter for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the stream-activity state of the session has changed. + + The new session state. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Notifies the client that the session has been disconnected. + + The reason that the audio session was disconnected. + An HRESULT code indicating whether the operation succeeded of failed. + + + + AudioSessionManager + + Designed to manage audio sessions and in particuar the + SimpleAudioVolume interface to adjust a session volume + + + + + Refresh session of current device. + + + + + Dispose. + + + + + Finalizer. + + + + + Occurs when audio session has been added (for example run another program that use audio playback). + + + + + SimpleAudioVolume object + for adjusting the volume for the user session + + + + + AudioSessionControl object + for registring for callbacks and other session information + + + + + Returns list of sessions of current device. + + + + + + + + + + + + Windows CoreAudio IAudioSessionNotification interface + Defined in AudioPolicy.h + + + + + + + session being added + An HRESULT code indicating whether the operation succeeded of failed. + + + + Specifies the category of an audio stream. + + + + + Other audio stream. + + + + + Media that will only stream when the app is in the foreground. + + + + + Media that can be streamed when the app is in the background. + + + + + Real-time communications, such as VOIP or chat. + + + + + Alert sounds. + + + + + Sound effects. + + + + + Game sound effects. + + + + + Background audio for games. + + + + + Manages the AudioStreamVolume for the . + + + + + Verify that the channel index is valid. + + + + + + + Return the current stream volumes for all channels + + An array of volume levels between 0.0 and 1.0 for each channel in the audio stream. + + + + Return the current volume for the requested channel. + + The 0 based index into the channels. + The volume level for the channel between 0.0 and 1.0. + + + + Set the volume level for each channel of the audio stream. + + An array of volume levels (between 0.0 and 1.0) one for each channel. + + A volume level MUST be supplied for reach channel in the audio stream. + + + Thrown when does not contain elements. + + + + + Sets the volume level for one channel in the audio stream. + + The 0-based index into the channels to adjust the volume of. + The volume level between 0.0 and 1.0 for this channel of the audio stream. + + + + Dispose + + + + + Release/cleanup objects during Dispose/finalization. + + True if disposing and false if being finalized. + + + + Returns the current number of channels in this audio stream. + + + + + Audio Volume Notification Data + + + + + Audio Volume Notification Data + + + + + + + + + Event Context + + + + + Muted + + + + + Master Volume + + + + + Channels + + + + + Channel Volume + + + + + AUDCLNT_E_NOT_INITIALIZED + + + + + AUDCLNT_E_UNSUPPORTED_FORMAT + + + + + AUDCLNT_E_DEVICE_IN_USE + + + + + Defined in AudioClient.h + + + + + Defined in AudioClient.h + + + + + Windows CoreAudio IAudioSessionControl interface + Defined in AudioPolicy.h + + + + + Retrieves the current state of the audio session. + + Receives the current session state. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the display name for the audio session. + + Receives a string that contains the display name. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a display name to the current audio session. + + A string that contains the new display name for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the path for the display icon for the audio session. + + Receives a string that specifies the fully qualified path of the file that contains the icon. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a display icon to the current session. + + A string that specifies the fully qualified path of the file that contains the new icon. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the grouping parameter of the audio session. + + Receives the grouping parameter ID. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a session to a grouping of sessions. + + The new grouping parameter ID. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Registers the client to receive notifications of session events, including changes in the session state. + + A client-implemented interface. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Deletes a previous registration by the client to receive notifications. + + A client-implemented interface. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Windows CoreAudio IAudioSessionControl interface + Defined in AudioPolicy.h + + + + + Retrieves the current state of the audio session. + + Receives the current session state. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the display name for the audio session. + + Receives a string that contains the display name. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a display name to the current audio session. + + A string that contains the new display name for the session. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the path for the display icon for the audio session. + + Receives a string that specifies the fully qualified path of the file that contains the icon. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a display icon to the current session. + + A string that specifies the fully qualified path of the file that contains the new icon. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the grouping parameter of the audio session. + + Receives the grouping parameter ID. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Assigns a session to a grouping of sessions. + + The new grouping parameter ID. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Registers the client to receive notifications of session events, including changes in the session state. + + A client-implemented interface. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Deletes a previous registration by the client to receive notifications. + + A client-implemented interface. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the identifier for the audio session. + + Receives the session identifier. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the identifier of the audio session instance. + + Receives the identifier of a particular instance. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the process identifier of the audio session. + + Receives the process identifier of the audio session. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Indicates whether the session is a system sounds session. + + An HRESULT code indicating whether the operation succeeded of failed. + + + + Enables or disables the default stream attenuation experience (auto-ducking) provided by the system. + + A variable that enables or disables system auto-ducking. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Defines constants that indicate the current state of an audio session. + + + MSDN Reference: http://msdn.microsoft.com/en-us/library/dd370792.aspx + + + + + The audio session is inactive. + + + + + The audio session is active. + + + + + The audio session has expired. + + + + + Defines constants that indicate a reason for an audio session being disconnected. + + + MSDN Reference: Unknown + + + + + The user removed the audio endpoint device. + + + + + The Windows audio service has stopped. + + + + + The stream format changed for the device that the audio session is connected to. + + + + + The user logged off the WTS session that the audio session was running in. + + + + + The WTS session that the audio session was running in was disconnected. + + + + + The (shared-mode) audio session was disconnected to make the audio endpoint device available for an exclusive-mode connection. + + + + + interface to receive session related events + + + + + notification of volume changes including muting of audio session + + the current volume + the current mute state, true muted, false otherwise + + + + notification of display name changed + + the current display name + + + + notification of icon path changed + + the current icon path + + + + notification of the client that the volume level of an audio channel in the session submix has changed + + The channel count. + An array of volumnes cooresponding with each channel index. + The number of the channel whose volume level changed. + + + + notification of the client that the grouping parameter for the session has changed + + >The new grouping parameter for the session. + + + + notification of the client that the stream-activity state of the session has changed + + The new session state. + + + + notification of the client that the session has been disconnected + + The reason that the audio session was disconnected. + + + + Windows CoreAudio IAudioSessionManager interface + Defined in AudioPolicy.h + + + + + Retrieves an audio session control. + + A new or existing session ID. + Audio session flags. + Receives an interface for the audio session. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves a simple audio volume control. + + A new or existing session ID. + Audio session flags. + Receives an interface for the audio session. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves an audio session control. + + A new or existing session ID. + Audio session flags. + Receives an interface for the audio session. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves a simple audio volume control. + + A new or existing session ID. + Audio session flags. + Receives an interface for the audio session. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Windows CoreAudio ISimpleAudioVolume interface + Defined in AudioClient.h + + + + + Sets the master volume level for the audio session. + + The new volume level expressed as a normalized value between 0.0 and 1.0. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the client volume level for the audio session. + + Receives the volume level expressed as a normalized value between 0.0 and 1.0. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Sets the muting state for the audio session. + + The new muting state. + A user context value that is passed to the notification callback. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Retrieves the current muting state for the audio session. + + Receives the muting state. + An HRESULT code indicating whether the operation succeeded of failed. + + + + Multimedia Device Collection + + + + + Get Enumerator + + Device enumerator + + + + Device count + + + + + Get device by index + + Device index + Device at the specified index + + + + Property Keys + + + + + PKEY_DeviceInterface_FriendlyName + + + + + PKEY_AudioEndpoint_FormFactor + + + + + PKEY_AudioEndpoint_ControlPanelPageProvider + + + + + PKEY_AudioEndpoint_Association + + + + + PKEY_AudioEndpoint_PhysicalSpeakers + + + + + PKEY_AudioEndpoint_GUID + + + + + PKEY_AudioEndpoint_Disable_SysFx + + + + + PKEY_AudioEndpoint_FullRangeSpeakers + + + + + PKEY_AudioEndpoint_Supports_EventDriven_Mode + + + + + PKEY_AudioEndpoint_JackSubType + + + + + PKEY_AudioEngine_DeviceFormat + + + + + PKEY_AudioEngine_OEMFormat + + + + + PKEY _Devie_FriendlyName + + + + + PKEY _Device_IconPath + + + + + Collection of sessions. + + + + + Returns session at index. + + + + + + + Number of current sessions. + + + + + Windows CoreAudio SimpleAudioVolume + + + + + Creates a new Audio endpoint volume + + ISimpleAudioVolume COM interface + + + + Dispose + + + + + Finalizer + + + + + Allows the user to adjust the volume from + 0.0 to 1.0 + + + + + Mute + + + + + Envelope generator (ADSR) + + + + + Creates and Initializes an Envelope Generator + + + + + Sets the attack curve + + + + + Sets the decay release curve + + + + + Read the next volume multiplier from the envelope generator + + A volume multiplier + + + + Trigger the gate + + If true, enter attack phase, if false enter release phase (unless already idle) + + + + Reset to idle state + + + + + Get the current output level + + + + + Attack Rate (seconds * SamplesPerSecond) + + + + + Decay Rate (seconds * SamplesPerSecond) + + + + + Release Rate (seconds * SamplesPerSecond) + + + + + Sustain Level (1 = 100%) + + + + + Current envelope state + + + + + Envelope State + + + + + Idle + + + + + Attack + + + + + Decay + + + + + Sustain + + + + + Release + + + + + Fully managed resampler, based on Cockos WDL Resampler + + + + + Creates a new Resampler + + + + + sets the mode + if sinc set, it overrides interp or filtercnt + + + + + Sets the filter parameters + used for filtercnt>0 but not sinc + + + + + Set feed mode + + if true, that means the first parameter to ResamplePrepare will specify however much input you have, not how much you want + + + + Reset + + + + + Prepare + note that it is safe to call ResamplePrepare without calling ResampleOut (the next call of ResamplePrepare will function as normal) + nb inbuffer was WDL_ResampleSample **, returning a place to put the in buffer, so we return a buffer and offset + + req_samples is output samples desired if !wantInputDriven, or if wantInputDriven is input samples that we have + + + + returns number of samples desired (put these into *inbuffer) + + + + http://tech.ebu.ch/docs/tech/tech3306-2009.pdf + + + + + WaveFormat + + + + + Data Chunk Position + + + + + Data Chunk Length + + + + + Riff Chunks + + + + + Audio Subtype GUIDs + http://msdn.microsoft.com/en-us/library/windows/desktop/aa372553%28v=vs.85%29.aspx + + + + + Advanced Audio Coding (AAC). + + + + + Not used + + + + + Dolby AC-3 audio over Sony/Philips Digital Interface (S/PDIF). + + + + + Encrypted audio data used with secure audio path. + + + + + Digital Theater Systems (DTS) audio. + + + + + Uncompressed IEEE floating-point audio. + + + + + MPEG Audio Layer-3 (MP3). + + + + + MPEG-1 audio payload. + + + + + Windows Media Audio 9 Voice codec. + + + + + Uncompressed PCM audio. + + + + + Windows Media Audio 9 Professional codec over S/PDIF. + + + + + Windows Media Audio 9 Lossless codec or Windows Media Audio 9.1 codec. + + + + + Windows Media Audio 8 codec, Windows Media Audio 9 codec, or Windows Media Audio 9.1 codec. + + + + + Windows Media Audio 9 Professional codec or Windows Media Audio 9.1 Professional codec. + + + + + Dolby Digital (AC-3). + + + + + MPEG-4 and AAC Audio Types + http://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx + Reference : wmcodecdsp.h + + + + + Dolby Audio Types + http://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx + Reference : wmcodecdsp.h + + + + + Dolby Audio Types + http://msdn.microsoft.com/en-us/library/windows/desktop/dd317599(v=vs.85).aspx + Reference : wmcodecdsp.h + + + + + μ-law coding + http://msdn.microsoft.com/en-us/library/windows/desktop/dd390971(v=vs.85).aspx + Reference : Ksmedia.h + + + + + Adaptive delta pulse code modulation (ADPCM) + http://msdn.microsoft.com/en-us/library/windows/desktop/dd390971(v=vs.85).aspx + Reference : Ksmedia.h + + + + + Dolby Digital Plus formatted for HDMI output. + http://msdn.microsoft.com/en-us/library/windows/hardware/ff538392(v=vs.85).aspx + Reference : internet + + + + + MSAudio1 - unknown meaning + Reference : wmcodecdsp.h + + + + + IMA ADPCM ACM Wrapper + + + + + WMSP2 - unknown meaning + Reference: wmsdkidl.h + + + + + Creates an instance of either the sink writer or the source reader. + + + + + Creates an instance of the sink writer or source reader, given a URL. + + + + + Creates an instance of the sink writer or source reader, given an IUnknown pointer. + + + + + CLSID_MFReadWriteClassFactory + + + + + Media Foundation Errors + + + + RANGES + 14000 - 14999 = General Media Foundation errors + 15000 - 15999 = ASF parsing errors + 16000 - 16999 = Media Source errors + 17000 - 17999 = MEDIAFOUNDATION Network Error Events + 18000 - 18999 = MEDIAFOUNDATION WMContainer Error Events + 19000 - 19999 = MEDIAFOUNDATION Media Sink Error Events + 20000 - 20999 = Renderer errors + 21000 - 21999 = Topology Errors + 25000 - 25999 = Timeline Errors + 26000 - 26999 = Unused + 28000 - 28999 = Transform errors + 29000 - 29999 = Content Protection errors + 40000 - 40999 = Clock errors + 41000 - 41999 = MF Quality Management Errors + 42000 - 42999 = MF Transcode API Errors + + + + + MessageId: MF_E_PLATFORM_NOT_INITIALIZED + + MessageText: + + Platform not initialized. Please call MFStartup().%0 + + + + + MessageId: MF_E_BUFFERTOOSMALL + + MessageText: + + The buffer was too small to carry out the requested action.%0 + + + + + MessageId: MF_E_INVALIDREQUEST + + MessageText: + + The request is invalid in the current state.%0 + + + + + MessageId: MF_E_INVALIDSTREAMNUMBER + + MessageText: + + The stream number provided was invalid.%0 + + + + + MessageId: MF_E_INVALIDMEDIATYPE + + MessageText: + + The data specified for the media type is invalid, inconsistent, or not supported by this object.%0 + + + + + MessageId: MF_E_NOTACCEPTING + + MessageText: + + The callee is currently not accepting further input.%0 + + + + + MessageId: MF_E_NOT_INITIALIZED + + MessageText: + + This object needs to be initialized before the requested operation can be carried out.%0 + + + + + MessageId: MF_E_UNSUPPORTED_REPRESENTATION + + MessageText: + + The requested representation is not supported by this object.%0 + + + + + MessageId: MF_E_NO_MORE_TYPES + + MessageText: + + An object ran out of media types to suggest therefore the requested chain of streaming objects cannot be completed.%0 + + + + + MessageId: MF_E_UNSUPPORTED_SERVICE + + MessageText: + + The object does not support the specified service.%0 + + + + + MessageId: MF_E_UNEXPECTED + + MessageText: + + An unexpected error has occurred in the operation requested.%0 + + + + + MessageId: MF_E_INVALIDNAME + + MessageText: + + Invalid name.%0 + + + + + MessageId: MF_E_INVALIDTYPE + + MessageText: + + Invalid type.%0 + + + + + MessageId: MF_E_INVALID_FILE_FORMAT + + MessageText: + + The file does not conform to the relevant file format specification. + + + + + MessageId: MF_E_INVALIDINDEX + + MessageText: + + Invalid index.%0 + + + + + MessageId: MF_E_INVALID_TIMESTAMP + + MessageText: + + An invalid timestamp was given.%0 + + + + + MessageId: MF_E_UNSUPPORTED_SCHEME + + MessageText: + + The scheme of the given URL is unsupported.%0 + + + + + MessageId: MF_E_UNSUPPORTED_BYTESTREAM_TYPE + + MessageText: + + The byte stream type of the given URL is unsupported.%0 + + + + + MessageId: MF_E_UNSUPPORTED_TIME_FORMAT + + MessageText: + + The given time format is unsupported.%0 + + + + + MessageId: MF_E_NO_SAMPLE_TIMESTAMP + + MessageText: + + The Media Sample does not have a timestamp.%0 + + + + + MessageId: MF_E_NO_SAMPLE_DURATION + + MessageText: + + The Media Sample does not have a duration.%0 + + + + + MessageId: MF_E_INVALID_STREAM_DATA + + MessageText: + + The request failed because the data in the stream is corrupt.%0\n. + + + + + MessageId: MF_E_RT_UNAVAILABLE + + MessageText: + + Real time services are not available.%0 + + + + + MessageId: MF_E_UNSUPPORTED_RATE + + MessageText: + + The specified rate is not supported.%0 + + + + + MessageId: MF_E_THINNING_UNSUPPORTED + + MessageText: + + This component does not support stream-thinning.%0 + + + + + MessageId: MF_E_REVERSE_UNSUPPORTED + + MessageText: + + The call failed because no reverse playback rates are available.%0 + + + + + MessageId: MF_E_UNSUPPORTED_RATE_TRANSITION + + MessageText: + + The requested rate transition cannot occur in the current state.%0 + + + + + MessageId: MF_E_RATE_CHANGE_PREEMPTED + + MessageText: + + The requested rate change has been pre-empted and will not occur.%0 + + + + + MessageId: MF_E_NOT_FOUND + + MessageText: + + The specified object or value does not exist.%0 + + + + + MessageId: MF_E_NOT_AVAILABLE + + MessageText: + + The requested value is not available.%0 + + + + + MessageId: MF_E_NO_CLOCK + + MessageText: + + The specified operation requires a clock and no clock is available.%0 + + + + + MessageId: MF_S_MULTIPLE_BEGIN + + MessageText: + + This callback and state had already been passed in to this event generator earlier.%0 + + + + + MessageId: MF_E_MULTIPLE_BEGIN + + MessageText: + + This callback has already been passed in to this event generator.%0 + + + + + MessageId: MF_E_MULTIPLE_SUBSCRIBERS + + MessageText: + + Some component is already listening to events on this event generator.%0 + + + + + MessageId: MF_E_TIMER_ORPHANED + + MessageText: + + This timer was orphaned before its callback time arrived.%0 + + + + + MessageId: MF_E_STATE_TRANSITION_PENDING + + MessageText: + + A state transition is already pending.%0 + + + + + MessageId: MF_E_UNSUPPORTED_STATE_TRANSITION + + MessageText: + + The requested state transition is unsupported.%0 + + + + + MessageId: MF_E_UNRECOVERABLE_ERROR_OCCURRED + + MessageText: + + An unrecoverable error has occurred.%0 + + + + + MessageId: MF_E_SAMPLE_HAS_TOO_MANY_BUFFERS + + MessageText: + + The provided sample has too many buffers.%0 + + + + + MessageId: MF_E_SAMPLE_NOT_WRITABLE + + MessageText: + + The provided sample is not writable.%0 + + + + + MessageId: MF_E_INVALID_KEY + + MessageText: + + The specified key is not valid. + + + + + MessageId: MF_E_BAD_STARTUP_VERSION + + MessageText: + + You are calling MFStartup with the wrong MF_VERSION. Mismatched bits? + + + + + MessageId: MF_E_UNSUPPORTED_CAPTION + + MessageText: + + The caption of the given URL is unsupported.%0 + + + + + MessageId: MF_E_INVALID_POSITION + + MessageText: + + The operation on the current offset is not permitted.%0 + + + + + MessageId: MF_E_ATTRIBUTENOTFOUND + + MessageText: + + The requested attribute was not found.%0 + + + + + MessageId: MF_E_PROPERTY_TYPE_NOT_ALLOWED + + MessageText: + + The specified property type is not allowed in this context.%0 + + + + + MessageId: MF_E_PROPERTY_TYPE_NOT_SUPPORTED + + MessageText: + + The specified property type is not supported.%0 + + + + + MessageId: MF_E_PROPERTY_EMPTY + + MessageText: + + The specified property is empty.%0 + + + + + MessageId: MF_E_PROPERTY_NOT_EMPTY + + MessageText: + + The specified property is not empty.%0 + + + + + MessageId: MF_E_PROPERTY_VECTOR_NOT_ALLOWED + + MessageText: + + The vector property specified is not allowed in this context.%0 + + + + + MessageId: MF_E_PROPERTY_VECTOR_REQUIRED + + MessageText: + + A vector property is required in this context.%0 + + + + + MessageId: MF_E_OPERATION_CANCELLED + + MessageText: + + The operation is cancelled.%0 + + + + + MessageId: MF_E_BYTESTREAM_NOT_SEEKABLE + + MessageText: + + The provided bytestream was expected to be seekable and it is not.%0 + + + + + MessageId: MF_E_DISABLED_IN_SAFEMODE + + MessageText: + + The Media Foundation platform is disabled when the system is running in Safe Mode.%0 + + + + + MessageId: MF_E_CANNOT_PARSE_BYTESTREAM + + MessageText: + + The Media Source could not parse the byte stream.%0 + + + + + MessageId: MF_E_SOURCERESOLVER_MUTUALLY_EXCLUSIVE_FLAGS + + MessageText: + + Mutually exclusive flags have been specified to source resolver. This flag combination is invalid.%0 + + + + + MessageId: MF_E_MEDIAPROC_WRONGSTATE + + MessageText: + + MediaProc is in the wrong state%0 + + + + + MessageId: MF_E_RT_THROUGHPUT_NOT_AVAILABLE + + MessageText: + + Real time I/O service can not provide requested throughput.%0 + + + + + MessageId: MF_E_RT_TOO_MANY_CLASSES + + MessageText: + + The workqueue cannot be registered with more classes.%0 + + + + + MessageId: MF_E_RT_WOULDBLOCK + + MessageText: + + This operation cannot succeed because another thread owns this object.%0 + + + + + MessageId: MF_E_NO_BITPUMP + + MessageText: + + Internal. Bitpump not found.%0 + + + + + MessageId: MF_E_RT_OUTOFMEMORY + + MessageText: + + No more RT memory available.%0 + + + + + MessageId: MF_E_RT_WORKQUEUE_CLASS_NOT_SPECIFIED + + MessageText: + + An MMCSS class has not been set for this work queue.%0 + + + + + MessageId: MF_E_INSUFFICIENT_BUFFER + + MessageText: + + Insufficient memory for response.%0 + + + + + MessageId: MF_E_CANNOT_CREATE_SINK + + MessageText: + + Activate failed to create mediasink. Call OutputNode::GetUINT32(MF_TOPONODE_MAJORTYPE) for more information. %0 + + + + + MessageId: MF_E_BYTESTREAM_UNKNOWN_LENGTH + + MessageText: + + The length of the provided bytestream is unknown.%0 + + + + + MessageId: MF_E_SESSION_PAUSEWHILESTOPPED + + MessageText: + + The media session cannot pause from a stopped state.%0 + + + + + MessageId: MF_S_ACTIVATE_REPLACED + + MessageText: + + The activate could not be created in the remote process for some reason it was replaced with empty one.%0 + + + + + MessageId: MF_E_FORMAT_CHANGE_NOT_SUPPORTED + + MessageText: + + The data specified for the media type is supported, but would require a format change, which is not supported by this object.%0 + + + + + MessageId: MF_E_INVALID_WORKQUEUE + + MessageText: + + The operation failed because an invalid combination of workqueue ID and flags was specified.%0 + + + + + MessageId: MF_E_DRM_UNSUPPORTED + + MessageText: + + No DRM support is available.%0 + + + + + MessageId: MF_E_UNAUTHORIZED + + MessageText: + + This operation is not authorized.%0 + + + + + MessageId: MF_E_OUT_OF_RANGE + + MessageText: + + The value is not in the specified or valid range.%0 + + + + + MessageId: MF_E_INVALID_CODEC_MERIT + + MessageText: + + The registered codec merit is not valid.%0 + + + + + MessageId: MF_E_HW_MFT_FAILED_START_STREAMING + + MessageText: + + Hardware MFT failed to start streaming due to lack of hardware resources.%0 + + + + + MessageId: MF_S_ASF_PARSEINPROGRESS + + MessageText: + + Parsing is still in progress and is not yet complete.%0 + + + + + MessageId: MF_E_ASF_PARSINGINCOMPLETE + + MessageText: + + Not enough data have been parsed to carry out the requested action.%0 + + + + + MessageId: MF_E_ASF_MISSINGDATA + + MessageText: + + There is a gap in the ASF data provided.%0 + + + + + MessageId: MF_E_ASF_INVALIDDATA + + MessageText: + + The data provided are not valid ASF.%0 + + + + + MessageId: MF_E_ASF_OPAQUEPACKET + + MessageText: + + The packet is opaque, so the requested information cannot be returned.%0 + + + + + MessageId: MF_E_ASF_NOINDEX + + MessageText: + + The requested operation failed since there is no appropriate ASF index.%0 + + + + + MessageId: MF_E_ASF_OUTOFRANGE + + MessageText: + + The value supplied is out of range for this operation.%0 + + + + + MessageId: MF_E_ASF_INDEXNOTLOADED + + MessageText: + + The index entry requested needs to be loaded before it can be available.%0 + + + + + MessageId: MF_E_ASF_TOO_MANY_PAYLOADS + + MessageText: + + The packet has reached the maximum number of payloads.%0 + + + + + MessageId: MF_E_ASF_UNSUPPORTED_STREAM_TYPE + + MessageText: + + Stream type is not supported.%0 + + + + + MessageId: MF_E_ASF_DROPPED_PACKET + + MessageText: + + One or more ASF packets were dropped.%0 + + + + + MessageId: MF_E_NO_EVENTS_AVAILABLE + + MessageText: + + There are no events available in the queue.%0 + + + + + MessageId: MF_E_INVALID_STATE_TRANSITION + + MessageText: + + A media source cannot go from the stopped state to the paused state.%0 + + + + + MessageId: MF_E_END_OF_STREAM + + MessageText: + + The media stream cannot process any more samples because there are no more samples in the stream.%0 + + + + + MessageId: MF_E_SHUTDOWN + + MessageText: + + The request is invalid because Shutdown() has been called.%0 + + + + + MessageId: MF_E_MP3_NOTFOUND + + MessageText: + + The MP3 object was not found.%0 + + + + + MessageId: MF_E_MP3_OUTOFDATA + + MessageText: + + The MP3 parser ran out of data before finding the MP3 object.%0 + + + + + MessageId: MF_E_MP3_NOTMP3 + + MessageText: + + The file is not really a MP3 file.%0 + + + + + MessageId: MF_E_MP3_NOTSUPPORTED + + MessageText: + + The MP3 file is not supported.%0 + + + + + MessageId: MF_E_NO_DURATION + + MessageText: + + The Media stream has no duration.%0 + + + + + MessageId: MF_E_INVALID_FORMAT + + MessageText: + + The Media format is recognized but is invalid.%0 + + + + + MessageId: MF_E_PROPERTY_NOT_FOUND + + MessageText: + + The property requested was not found.%0 + + + + + MessageId: MF_E_PROPERTY_READ_ONLY + + MessageText: + + The property is read only.%0 + + + + + MessageId: MF_E_PROPERTY_NOT_ALLOWED + + MessageText: + + The specified property is not allowed in this context.%0 + + + + + MessageId: MF_E_MEDIA_SOURCE_NOT_STARTED + + MessageText: + + The media source is not started.%0 + + + + + MessageId: MF_E_UNSUPPORTED_FORMAT + + MessageText: + + The Media format is recognized but not supported.%0 + + + + + MessageId: MF_E_MP3_BAD_CRC + + MessageText: + + The MPEG frame has bad CRC.%0 + + + + + MessageId: MF_E_NOT_PROTECTED + + MessageText: + + The file is not protected.%0 + + + + + MessageId: MF_E_MEDIA_SOURCE_WRONGSTATE + + MessageText: + + The media source is in the wrong state%0 + + + + + MessageId: MF_E_MEDIA_SOURCE_NO_STREAMS_SELECTED + + MessageText: + + No streams are selected in source presentation descriptor.%0 + + + + + MessageId: MF_E_CANNOT_FIND_KEYFRAME_SAMPLE + + MessageText: + + No key frame sample was found.%0 + + + + + MessageId: MF_E_NETWORK_RESOURCE_FAILURE + + MessageText: + + An attempt to acquire a network resource failed.%0 + + + + + MessageId: MF_E_NET_WRITE + + MessageText: + + Error writing to the network.%0 + + + + + MessageId: MF_E_NET_READ + + MessageText: + + Error reading from the network.%0 + + + + + MessageId: MF_E_NET_REQUIRE_NETWORK + + MessageText: + + Internal. Entry cannot complete operation without network.%0 + + + + + MessageId: MF_E_NET_REQUIRE_ASYNC + + MessageText: + + Internal. Async op is required.%0 + + + + + MessageId: MF_E_NET_BWLEVEL_NOT_SUPPORTED + + MessageText: + + Internal. Bandwidth levels are not supported.%0 + + + + + MessageId: MF_E_NET_STREAMGROUPS_NOT_SUPPORTED + + MessageText: + + Internal. Stream groups are not supported.%0 + + + + + MessageId: MF_E_NET_MANUALSS_NOT_SUPPORTED + + MessageText: + + Manual stream selection is not supported.%0 + + + + + MessageId: MF_E_NET_INVALID_PRESENTATION_DESCRIPTOR + + MessageText: + + Invalid presentation descriptor.%0 + + + + + MessageId: MF_E_NET_CACHESTREAM_NOT_FOUND + + MessageText: + + Cannot find cache stream.%0 + + + + + MessageId: MF_I_MANUAL_PROXY + + MessageText: + + The proxy setting is manual.%0 + + + + duplicate removed + MessageId=17011 Severity=Informational Facility=MEDIAFOUNDATION SymbolicName=MF_E_INVALID_REQUEST + Language=English + The request is invalid in the current state.%0 + . + + MessageId: MF_E_NET_REQUIRE_INPUT + + MessageText: + + Internal. Entry cannot complete operation without input.%0 + + + + + MessageId: MF_E_NET_REDIRECT + + MessageText: + + The client redirected to another server.%0 + + + + + MessageId: MF_E_NET_REDIRECT_TO_PROXY + + MessageText: + + The client is redirected to a proxy server.%0 + + + + + MessageId: MF_E_NET_TOO_MANY_REDIRECTS + + MessageText: + + The client reached maximum redirection limit.%0 + + + + + MessageId: MF_E_NET_TIMEOUT + + MessageText: + + The server, a computer set up to offer multimedia content to other computers, could not handle your request for multimedia content in a timely manner. Please try again later.%0 + + + + + MessageId: MF_E_NET_CLIENT_CLOSE + + MessageText: + + The control socket is closed by the client.%0 + + + + + MessageId: MF_E_NET_BAD_CONTROL_DATA + + MessageText: + + The server received invalid data from the client on the control connection.%0 + + + + + MessageId: MF_E_NET_INCOMPATIBLE_SERVER + + MessageText: + + The server is not a compatible streaming media server.%0 + + + + + MessageId: MF_E_NET_UNSAFE_URL + + MessageText: + + Url.%0 + + + + + MessageId: MF_E_NET_CACHE_NO_DATA + + MessageText: + + Data is not available.%0 + + + + + MessageId: MF_E_NET_EOL + + MessageText: + + End of line.%0 + + + + + MessageId: MF_E_NET_BAD_REQUEST + + MessageText: + + The request could not be understood by the server.%0 + + + + + MessageId: MF_E_NET_INTERNAL_SERVER_ERROR + + MessageText: + + The server encountered an unexpected condition which prevented it from fulfilling the request.%0 + + + + + MessageId: MF_E_NET_SESSION_NOT_FOUND + + MessageText: + + Session not found.%0 + + + + + MessageId: MF_E_NET_NOCONNECTION + + MessageText: + + There is no connection established with the Windows Media server. The operation failed.%0 + + + + + MessageId: MF_E_NET_CONNECTION_FAILURE + + MessageText: + + The network connection has failed.%0 + + + + + MessageId: MF_E_NET_INCOMPATIBLE_PUSHSERVER + + MessageText: + + The Server service that received the HTTP push request is not a compatible version of Windows Media Services (WMS). This error may indicate the push request was received by IIS instead of WMS. Ensure WMS is started and has the HTTP Server control protocol properly enabled and try again.%0 + + + + + MessageId: MF_E_NET_SERVER_ACCESSDENIED + + MessageText: + + The Windows Media server is denying access. The username and/or password might be incorrect.%0 + + + + + MessageId: MF_E_NET_PROXY_ACCESSDENIED + + MessageText: + + The proxy server is denying access. The username and/or password might be incorrect.%0 + + + + + MessageId: MF_E_NET_CANNOTCONNECT + + MessageText: + + Unable to establish a connection to the server.%0 + + + + + MessageId: MF_E_NET_INVALID_PUSH_TEMPLATE + + MessageText: + + The specified push template is invalid.%0 + + + + + MessageId: MF_E_NET_INVALID_PUSH_PUBLISHING_POINT + + MessageText: + + The specified push publishing point is invalid.%0 + + + + + MessageId: MF_E_NET_BUSY + + MessageText: + + The requested resource is in use.%0 + + + + + MessageId: MF_E_NET_RESOURCE_GONE + + MessageText: + + The Publishing Point or file on the Windows Media Server is no longer available.%0 + + + + + MessageId: MF_E_NET_ERROR_FROM_PROXY + + MessageText: + + The proxy experienced an error while attempting to contact the media server.%0 + + + + + MessageId: MF_E_NET_PROXY_TIMEOUT + + MessageText: + + The proxy did not receive a timely response while attempting to contact the media server.%0 + + + + + MessageId: MF_E_NET_SERVER_UNAVAILABLE + + MessageText: + + The server is currently unable to handle the request due to a temporary overloading or maintenance of the server.%0 + + + + + MessageId: MF_E_NET_TOO_MUCH_DATA + + MessageText: + + The encoding process was unable to keep up with the amount of supplied data.%0 + + + + + MessageId: MF_E_NET_SESSION_INVALID + + MessageText: + + Session not found.%0 + + + + + MessageId: MF_E_OFFLINE_MODE + + MessageText: + + The requested URL is not available in offline mode.%0 + + + + + MessageId: MF_E_NET_UDP_BLOCKED + + MessageText: + + A device in the network is blocking UDP traffic.%0 + + + + + MessageId: MF_E_NET_UNSUPPORTED_CONFIGURATION + + MessageText: + + The specified configuration value is not supported.%0 + + + + + MessageId: MF_E_NET_PROTOCOL_DISABLED + + MessageText: + + The networking protocol is disabled.%0 + + + + + MessageId: MF_E_ALREADY_INITIALIZED + + MessageText: + + This object has already been initialized and cannot be re-initialized at this time.%0 + + + + + MessageId: MF_E_BANDWIDTH_OVERRUN + + MessageText: + + The amount of data passed in exceeds the given bitrate and buffer window.%0 + + + + + MessageId: MF_E_LATE_SAMPLE + + MessageText: + + The sample was passed in too late to be correctly processed.%0 + + + + + MessageId: MF_E_FLUSH_NEEDED + + MessageText: + + The requested action cannot be carried out until the object is flushed and the queue is emptied.%0 + + + + + MessageId: MF_E_INVALID_PROFILE + + MessageText: + + The profile is invalid.%0 + + + + + MessageId: MF_E_INDEX_NOT_COMMITTED + + MessageText: + + The index that is being generated needs to be committed before the requested action can be carried out.%0 + + + + + MessageId: MF_E_NO_INDEX + + MessageText: + + The index that is necessary for the requested action is not found.%0 + + + + + MessageId: MF_E_CANNOT_INDEX_IN_PLACE + + MessageText: + + The requested index cannot be added in-place to the specified ASF content.%0 + + + + + MessageId: MF_E_MISSING_ASF_LEAKYBUCKET + + MessageText: + + The ASF leaky bucket parameters must be specified in order to carry out this request.%0 + + + + + MessageId: MF_E_INVALID_ASF_STREAMID + + MessageText: + + The stream id is invalid. The valid range for ASF stream id is from 1 to 127.%0 + + + + + MessageId: MF_E_STREAMSINK_REMOVED + + MessageText: + + The requested Stream Sink has been removed and cannot be used.%0 + + + + + MessageId: MF_E_STREAMSINKS_OUT_OF_SYNC + + MessageText: + + The various Stream Sinks in this Media Sink are too far out of sync for the requested action to take place.%0 + + + + + MessageId: MF_E_STREAMSINKS_FIXED + + MessageText: + + Stream Sinks cannot be added to or removed from this Media Sink because its set of streams is fixed.%0 + + + + + MessageId: MF_E_STREAMSINK_EXISTS + + MessageText: + + The given Stream Sink already exists.%0 + + + + + MessageId: MF_E_SAMPLEALLOCATOR_CANCELED + + MessageText: + + Sample allocations have been canceled.%0 + + + + + MessageId: MF_E_SAMPLEALLOCATOR_EMPTY + + MessageText: + + The sample allocator is currently empty, due to outstanding requests.%0 + + + + + MessageId: MF_E_SINK_ALREADYSTOPPED + + MessageText: + + When we try to sopt a stream sink, it is already stopped %0 + + + + + MessageId: MF_E_ASF_FILESINK_BITRATE_UNKNOWN + + MessageText: + + The ASF file sink could not reserve AVIO because the bitrate is unknown.%0 + + + + + MessageId: MF_E_SINK_NO_STREAMS + + MessageText: + + No streams are selected in sink presentation descriptor.%0 + + + + + MessageId: MF_S_SINK_NOT_FINALIZED + + MessageText: + + The sink has not been finalized before shut down. This may cause sink generate a corrupted content.%0 + + + + + MessageId: MF_E_METADATA_TOO_LONG + + MessageText: + + A metadata item was too long to write to the output container.%0 + + + + + MessageId: MF_E_SINK_NO_SAMPLES_PROCESSED + + MessageText: + + The operation failed because no samples were processed by the sink.%0 + + + + + MessageId: MF_E_VIDEO_REN_NO_PROCAMP_HW + + MessageText: + + There is no available procamp hardware with which to perform color correction.%0 + + + + + MessageId: MF_E_VIDEO_REN_NO_DEINTERLACE_HW + + MessageText: + + There is no available deinterlacing hardware with which to deinterlace the video stream.%0 + + + + + MessageId: MF_E_VIDEO_REN_COPYPROT_FAILED + + MessageText: + + A video stream requires copy protection to be enabled, but there was a failure in attempting to enable copy protection.%0 + + + + + MessageId: MF_E_VIDEO_REN_SURFACE_NOT_SHARED + + MessageText: + + A component is attempting to access a surface for sharing that is not shared.%0 + + + + + MessageId: MF_E_VIDEO_DEVICE_LOCKED + + MessageText: + + A component is attempting to access a shared device that is already locked by another component.%0 + + + + + MessageId: MF_E_NEW_VIDEO_DEVICE + + MessageText: + + The device is no longer available. The handle should be closed and a new one opened.%0 + + + + + MessageId: MF_E_NO_VIDEO_SAMPLE_AVAILABLE + + MessageText: + + A video sample is not currently queued on a stream that is required for mixing.%0 + + + + + MessageId: MF_E_NO_AUDIO_PLAYBACK_DEVICE + + MessageText: + + No audio playback device was found.%0 + + + + + MessageId: MF_E_AUDIO_PLAYBACK_DEVICE_IN_USE + + MessageText: + + The requested audio playback device is currently in use.%0 + + + + + MessageId: MF_E_AUDIO_PLAYBACK_DEVICE_INVALIDATED + + MessageText: + + The audio playback device is no longer present.%0 + + + + + MessageId: MF_E_AUDIO_SERVICE_NOT_RUNNING + + MessageText: + + The audio service is not running.%0 + + + + + MessageId: MF_E_TOPO_INVALID_OPTIONAL_NODE + + MessageText: + + The topology contains an invalid optional node. Possible reasons are incorrect number of outputs and inputs or optional node is at the beginning or end of a segment. %0 + + + + + MessageId: MF_E_TOPO_CANNOT_FIND_DECRYPTOR + + MessageText: + + No suitable transform was found to decrypt the content. %0 + + + + + MessageId: MF_E_TOPO_CODEC_NOT_FOUND + + MessageText: + + No suitable transform was found to encode or decode the content. %0 + + + + + MessageId: MF_E_TOPO_CANNOT_CONNECT + + MessageText: + + Unable to find a way to connect nodes%0 + + + + + MessageId: MF_E_TOPO_UNSUPPORTED + + MessageText: + + Unsupported operations in topoloader%0 + + + + + MessageId: MF_E_TOPO_INVALID_TIME_ATTRIBUTES + + MessageText: + + The topology or its nodes contain incorrectly set time attributes%0 + + + + + MessageId: MF_E_TOPO_LOOPS_IN_TOPOLOGY + + MessageText: + + The topology contains loops, which are unsupported in media foundation topologies%0 + + + + + MessageId: MF_E_TOPO_MISSING_PRESENTATION_DESCRIPTOR + + MessageText: + + A source stream node in the topology does not have a presentation descriptor%0 + + + + + MessageId: MF_E_TOPO_MISSING_STREAM_DESCRIPTOR + + MessageText: + + A source stream node in the topology does not have a stream descriptor%0 + + + + + MessageId: MF_E_TOPO_STREAM_DESCRIPTOR_NOT_SELECTED + + MessageText: + + A stream descriptor was set on a source stream node but it was not selected on the presentation descriptor%0 + + + + + MessageId: MF_E_TOPO_MISSING_SOURCE + + MessageText: + + A source stream node in the topology does not have a source%0 + + + + + MessageId: MF_E_TOPO_SINK_ACTIVATES_UNSUPPORTED + + MessageText: + + The topology loader does not support sink activates on output nodes.%0 + + + + + MessageId: MF_E_SEQUENCER_UNKNOWN_SEGMENT_ID + + MessageText: + + The sequencer cannot find a segment with the given ID.%0\n. + + + + + MessageId: MF_S_SEQUENCER_CONTEXT_CANCELED + + MessageText: + + The context was canceled.%0\n. + + + + + MessageId: MF_E_NO_SOURCE_IN_CACHE + + MessageText: + + Cannot find source in source cache.%0\n. + + + + + MessageId: MF_S_SEQUENCER_SEGMENT_AT_END_OF_STREAM + + MessageText: + + Cannot update topology flags.%0\n. + + + + + MessageId: MF_E_TRANSFORM_TYPE_NOT_SET + + MessageText: + + A valid type has not been set for this stream or a stream that it depends on.%0 + + + + + MessageId: MF_E_TRANSFORM_STREAM_CHANGE + + MessageText: + + A stream change has occurred. Output cannot be produced until the streams have been renegotiated.%0 + + + + + MessageId: MF_E_TRANSFORM_INPUT_REMAINING + + MessageText: + + The transform cannot take the requested action until all of the input data it currently holds is processed or flushed.%0 + + + + + MessageId: MF_E_TRANSFORM_PROFILE_MISSING + + MessageText: + + The transform requires a profile but no profile was supplied or found.%0 + + + + + MessageId: MF_E_TRANSFORM_PROFILE_INVALID_OR_CORRUPT + + MessageText: + + The transform requires a profile but the supplied profile was invalid or corrupt.%0 + + + + + MessageId: MF_E_TRANSFORM_PROFILE_TRUNCATED + + MessageText: + + The transform requires a profile but the supplied profile ended unexpectedly while parsing.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_PID_NOT_RECOGNIZED + + MessageText: + + The property ID does not match any property supported by the transform.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_VARIANT_TYPE_WRONG + + MessageText: + + The variant does not have the type expected for this property ID.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_NOT_WRITEABLE + + MessageText: + + An attempt was made to set the value on a read-only property.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_ARRAY_VALUE_WRONG_NUM_DIM + + MessageText: + + The array property value has an unexpected number of dimensions.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_VALUE_SIZE_WRONG + + MessageText: + + The array or blob property value has an unexpected size.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_VALUE_OUT_OF_RANGE + + MessageText: + + The property value is out of range for this transform.%0 + + + + + MessageId: MF_E_TRANSFORM_PROPERTY_VALUE_INCOMPATIBLE + + MessageText: + + The property value is incompatible with some other property or mediatype set on the transform.%0 + + + + + MessageId: MF_E_TRANSFORM_NOT_POSSIBLE_FOR_CURRENT_OUTPUT_MEDIATYPE + + MessageText: + + The requested operation is not supported for the currently set output mediatype.%0 + + + + + MessageId: MF_E_TRANSFORM_NOT_POSSIBLE_FOR_CURRENT_INPUT_MEDIATYPE + + MessageText: + + The requested operation is not supported for the currently set input mediatype.%0 + + + + + MessageId: MF_E_TRANSFORM_NOT_POSSIBLE_FOR_CURRENT_MEDIATYPE_COMBINATION + + MessageText: + + The requested operation is not supported for the currently set combination of mediatypes.%0 + + + + + MessageId: MF_E_TRANSFORM_CONFLICTS_WITH_OTHER_CURRENTLY_ENABLED_FEATURES + + MessageText: + + The requested feature is not supported in combination with some other currently enabled feature.%0 + + + + + MessageId: MF_E_TRANSFORM_NEED_MORE_INPUT + + MessageText: + + The transform cannot produce output until it gets more input samples.%0 + + + + + MessageId: MF_E_TRANSFORM_NOT_POSSIBLE_FOR_CURRENT_SPKR_CONFIG + + MessageText: + + The requested operation is not supported for the current speaker configuration.%0 + + + + + MessageId: MF_E_TRANSFORM_CANNOT_CHANGE_MEDIATYPE_WHILE_PROCESSING + + MessageText: + + The transform cannot accept mediatype changes in the middle of processing.%0 + + + + + MessageId: MF_S_TRANSFORM_DO_NOT_PROPAGATE_EVENT + + MessageText: + + The caller should not propagate this event to downstream components.%0 + + + + + MessageId: MF_E_UNSUPPORTED_D3D_TYPE + + MessageText: + + The input type is not supported for D3D device.%0 + + + + + MessageId: MF_E_TRANSFORM_ASYNC_LOCKED + + MessageText: + + The caller does not appear to support this transform's asynchronous capabilities.%0 + + + + + MessageId: MF_E_TRANSFORM_CANNOT_INITIALIZE_ACM_DRIVER + + MessageText: + + An audio compression manager driver could not be initialized by the transform.%0 + + + + + MessageId: MF_E_LICENSE_INCORRECT_RIGHTS + + MessageText: + + You are not allowed to open this file. Contact the content provider for further assistance.%0 + + + + + MessageId: MF_E_LICENSE_OUTOFDATE + + MessageText: + + The license for this media file has expired. Get a new license or contact the content provider for further assistance.%0 + + + + + MessageId: MF_E_LICENSE_REQUIRED + + MessageText: + + You need a license to perform the requested operation on this media file.%0 + + + + + MessageId: MF_E_DRM_HARDWARE_INCONSISTENT + + MessageText: + + The licenses for your media files are corrupted. Contact Microsoft product support.%0 + + + + + MessageId: MF_E_NO_CONTENT_PROTECTION_MANAGER + + MessageText: + + The APP needs to provide IMFContentProtectionManager callback to access the protected media file.%0 + + + + + MessageId: MF_E_LICENSE_RESTORE_NO_RIGHTS + + MessageText: + + Client does not have rights to restore licenses.%0 + + + + + MessageId: MF_E_BACKUP_RESTRICTED_LICENSE + + MessageText: + + Licenses are restricted and hence can not be backed up.%0 + + + + + MessageId: MF_E_LICENSE_RESTORE_NEEDS_INDIVIDUALIZATION + + MessageText: + + License restore requires machine to be individualized.%0 + + + + + MessageId: MF_S_PROTECTION_NOT_REQUIRED + + MessageText: + + Protection for stream is not required.%0 + + + + + MessageId: MF_E_COMPONENT_REVOKED + + MessageText: + + Component is revoked.%0 + + + + + MessageId: MF_E_TRUST_DISABLED + + MessageText: + + Trusted functionality is currently disabled on this component.%0 + + + + + MessageId: MF_E_WMDRMOTA_NO_ACTION + + MessageText: + + No Action is set on WMDRM Output Trust Authority.%0 + + + + + MessageId: MF_E_WMDRMOTA_ACTION_ALREADY_SET + + MessageText: + + Action is already set on WMDRM Output Trust Authority.%0 + + + + + MessageId: MF_E_WMDRMOTA_DRM_HEADER_NOT_AVAILABLE + + MessageText: + + DRM Heaader is not available.%0 + + + + + MessageId: MF_E_WMDRMOTA_DRM_ENCRYPTION_SCHEME_NOT_SUPPORTED + + MessageText: + + Current encryption scheme is not supported.%0 + + + + + MessageId: MF_E_WMDRMOTA_ACTION_MISMATCH + + MessageText: + + Action does not match with current configuration.%0 + + + + + MessageId: MF_E_WMDRMOTA_INVALID_POLICY + + MessageText: + + Invalid policy for WMDRM Output Trust Authority.%0 + + + + + MessageId: MF_E_POLICY_UNSUPPORTED + + MessageText: + + The policies that the Input Trust Authority requires to be enforced are unsupported by the outputs.%0 + + + + + MessageId: MF_E_OPL_NOT_SUPPORTED + + MessageText: + + The OPL that the license requires to be enforced are not supported by the Input Trust Authority.%0 + + + + + MessageId: MF_E_TOPOLOGY_VERIFICATION_FAILED + + MessageText: + + The topology could not be successfully verified.%0 + + + + + MessageId: MF_E_SIGNATURE_VERIFICATION_FAILED + + MessageText: + + Signature verification could not be completed successfully for this component.%0 + + + + + MessageId: MF_E_DEBUGGING_NOT_ALLOWED + + MessageText: + + Running this process under a debugger while using protected content is not allowed.%0 + + + + + MessageId: MF_E_CODE_EXPIRED + + MessageText: + + MF component has expired.%0 + + + + + MessageId: MF_E_GRL_VERSION_TOO_LOW + + MessageText: + + The current GRL on the machine does not meet the minimum version requirements.%0 + + + + + MessageId: MF_E_GRL_RENEWAL_NOT_FOUND + + MessageText: + + The current GRL on the machine does not contain any renewal entries for the specified revocation.%0 + + + + + MessageId: MF_E_GRL_EXTENSIBLE_ENTRY_NOT_FOUND + + MessageText: + + The current GRL on the machine does not contain any extensible entries for the specified extension GUID.%0 + + + + + MessageId: MF_E_KERNEL_UNTRUSTED + + MessageText: + + The kernel isn't secure for high security level content.%0 + + + + + MessageId: MF_E_PEAUTH_UNTRUSTED + + MessageText: + + The response from protected environment driver isn't valid.%0 + + + + + MessageId: MF_E_NON_PE_PROCESS + + MessageText: + + A non-PE process tried to talk to PEAuth.%0 + + + + + MessageId: MF_E_REBOOT_REQUIRED + + MessageText: + + We need to reboot the machine.%0 + + + + + MessageId: MF_S_WAIT_FOR_POLICY_SET + + MessageText: + + Protection for this stream is not guaranteed to be enforced until the MEPolicySet event is fired.%0 + + + + + MessageId: MF_S_VIDEO_DISABLED_WITH_UNKNOWN_SOFTWARE_OUTPUT + + MessageText: + + This video stream is disabled because it is being sent to an unknown software output.%0 + + + + + MessageId: MF_E_GRL_INVALID_FORMAT + + MessageText: + + The GRL file is not correctly formed, it may have been corrupted or overwritten.%0 + + + + + MessageId: MF_E_GRL_UNRECOGNIZED_FORMAT + + MessageText: + + The GRL file is in a format newer than those recognized by this GRL Reader.%0 + + + + + MessageId: MF_E_ALL_PROCESS_RESTART_REQUIRED + + MessageText: + + The GRL was reloaded and required all processes that can run protected media to restart.%0 + + + + + MessageId: MF_E_PROCESS_RESTART_REQUIRED + + MessageText: + + The GRL was reloaded and the current process needs to restart.%0 + + + + + MessageId: MF_E_USERMODE_UNTRUSTED + + MessageText: + + The user space is untrusted for protected content play.%0 + + + + + MessageId: MF_E_PEAUTH_SESSION_NOT_STARTED + + MessageText: + + PEAuth communication session hasn't been started.%0 + + + + + MessageId: MF_E_PEAUTH_PUBLICKEY_REVOKED + + MessageText: + + PEAuth's public key is revoked.%0 + + + + + MessageId: MF_E_GRL_ABSENT + + MessageText: + + The GRL is absent.%0 + + + + + MessageId: MF_S_PE_TRUSTED + + MessageText: + + The Protected Environment is trusted.%0 + + + + + MessageId: MF_E_PE_UNTRUSTED + + MessageText: + + The Protected Environment is untrusted.%0 + + + + + MessageId: MF_E_PEAUTH_NOT_STARTED + + MessageText: + + The Protected Environment Authorization service (PEAUTH) has not been started.%0 + + + + + MessageId: MF_E_INCOMPATIBLE_SAMPLE_PROTECTION + + MessageText: + + The sample protection algorithms supported by components are not compatible.%0 + + + + + MessageId: MF_E_PE_SESSIONS_MAXED + + MessageText: + + No more protected environment sessions can be supported.%0 + + + + + MessageId: MF_E_HIGH_SECURITY_LEVEL_CONTENT_NOT_ALLOWED + + MessageText: + + WMDRM ITA does not allow protected content with high security level for this release.%0 + + + + + MessageId: MF_E_TEST_SIGNED_COMPONENTS_NOT_ALLOWED + + MessageText: + + WMDRM ITA cannot allow the requested action for the content as one or more components is not properly signed.%0 + + + + + MessageId: MF_E_ITA_UNSUPPORTED_ACTION + + MessageText: + + WMDRM ITA does not support the requested action.%0 + + + + + MessageId: MF_E_ITA_ERROR_PARSING_SAP_PARAMETERS + + MessageText: + + WMDRM ITA encountered an error in parsing the Secure Audio Path parameters.%0 + + + + + MessageId: MF_E_POLICY_MGR_ACTION_OUTOFBOUNDS + + MessageText: + + The Policy Manager action passed in is invalid.%0 + + + + + MessageId: MF_E_BAD_OPL_STRUCTURE_FORMAT + + MessageText: + + The structure specifying Output Protection Level is not the correct format.%0 + + + + + MessageId: MF_E_ITA_UNRECOGNIZED_ANALOG_VIDEO_PROTECTION_GUID + + MessageText: + + WMDRM ITA does not recognize the Explicite Analog Video Output Protection guid specified in the license.%0 + + + + + MessageId: MF_E_NO_PMP_HOST + + MessageText: + + IMFPMPHost object not available.%0 + + + + + MessageId: MF_E_ITA_OPL_DATA_NOT_INITIALIZED + + MessageText: + + WMDRM ITA could not initialize the Output Protection Level data.%0 + + + + + MessageId: MF_E_ITA_UNRECOGNIZED_ANALOG_VIDEO_OUTPUT + + MessageText: + + WMDRM ITA does not recognize the Analog Video Output specified by the OTA.%0 + + + + + MessageId: MF_E_ITA_UNRECOGNIZED_DIGITAL_VIDEO_OUTPUT + + MessageText: + + WMDRM ITA does not recognize the Digital Video Output specified by the OTA.%0 + + + + + MessageId: MF_E_CLOCK_INVALID_CONTINUITY_KEY + + MessageText: + + The continuity key supplied is not currently valid.%0 + + + + + MessageId: MF_E_CLOCK_NO_TIME_SOURCE + + MessageText: + + No Presentation Time Source has been specified.%0 + + + + + MessageId: MF_E_CLOCK_STATE_ALREADY_SET + + MessageText: + + The clock is already in the requested state.%0 + + + + + MessageId: MF_E_CLOCK_NOT_SIMPLE + + MessageText: + + The clock has too many advanced features to carry out the request.%0 + + + + + MessageId: MF_S_CLOCK_STOPPED + + MessageText: + + Timer::SetTimer returns this success code if called happened while timer is stopped. Timer is not going to be dispatched until clock is running%0 + + + + + MessageId: MF_E_NO_MORE_DROP_MODES + + MessageText: + + The component does not support any more drop modes.%0 + + + + + MessageId: MF_E_NO_MORE_QUALITY_LEVELS + + MessageText: + + The component does not support any more quality levels.%0 + + + + + MessageId: MF_E_DROPTIME_NOT_SUPPORTED + + MessageText: + + The component does not support drop time functionality.%0 + + + + + MessageId: MF_E_QUALITYKNOB_WAIT_LONGER + + MessageText: + + Quality Manager needs to wait longer before bumping the Quality Level up.%0 + + + + + MessageId: MF_E_QM_INVALIDSTATE + + MessageText: + + Quality Manager is in an invalid state. Quality Management is off at this moment.%0 + + + + + MessageId: MF_E_TRANSCODE_NO_CONTAINERTYPE + + MessageText: + + No transcode output container type is specified.%0 + + + + + MessageId: MF_E_TRANSCODE_PROFILE_NO_MATCHING_STREAMS + + MessageText: + + The profile does not have a media type configuration for any selected source streams.%0 + + + + + MessageId: MF_E_TRANSCODE_NO_MATCHING_ENCODER + + MessageText: + + Cannot find an encoder MFT that accepts the user preferred output type.%0 + + + + + MessageId: MF_E_ALLOCATOR_NOT_INITIALIZED + + MessageText: + + Memory allocator is not initialized.%0 + + + + + MessageId: MF_E_ALLOCATOR_NOT_COMMITED + + MessageText: + + Memory allocator is not committed yet.%0 + + + + + MessageId: MF_E_ALLOCATOR_ALREADY_COMMITED + + MessageText: + + Memory allocator has already been committed.%0 + + + + + MessageId: MF_E_STREAM_ERROR + + MessageText: + + An error occurred in media stream.%0 + + + + + MessageId: MF_E_INVALID_STREAM_STATE + + MessageText: + + Stream is not in a state to handle the request.%0 + + + + + MessageId: MF_E_HW_STREAM_NOT_CONNECTED + + MessageText: + + Hardware stream is not connected yet.%0 + + + + + Major Media Types + http://msdn.microsoft.com/en-us/library/windows/desktop/aa367377%28v=vs.85%29.aspx + + + + + Default + + + + + Audio + + + + + Video + + + + + Protected Media + + + + + Synchronized Accessible Media Interchange (SAMI) captions. + + + + + Script stream + + + + + Still image stream. + + + + + HTML stream. + + + + + Binary stream. + + + + + A stream that contains data files. + + + + + Chunk Identifier helpers + + + + + Chunk identifier to Int32 (replaces mmioStringToFOURCC) + + four character chunk identifier + Chunk identifier as int 32 + + + + Allows us to add descriptions to interop members + + + + + Field description + + + + + String representation + + + + + + The description + + + + + IMFActivate, defined in mfobjects.h + + + + + Provides a generic way to store key/value pairs on an object. + http://msdn.microsoft.com/en-gb/library/windows/desktop/ms704598%28v=vs.85%29.aspx + + + + + Retrieves the value associated with a key. + + + + + Retrieves the data type of the value associated with a key. + + + + + Queries whether a stored attribute value equals a specified PROPVARIANT. + + + + + Compares the attributes on this object with the attributes on another object. + + + + + Retrieves a UINT32 value associated with a key. + + + + + Retrieves a UINT64 value associated with a key. + + + + + Retrieves a double value associated with a key. + + + + + Retrieves a GUID value associated with a key. + + + + + Retrieves the length of a string value associated with a key. + + + + + Retrieves a wide-character string associated with a key. + + + + + Retrieves a wide-character string associated with a key. This method allocates the memory for the string. + + + + + Retrieves the length of a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. This method allocates the memory for the array. + + + + + Retrieves an interface pointer associated with a key. + + + + + Associates an attribute value with a key. + + + + + Removes a key/value pair from the object's attribute list. + + + + + Removes all key/value pairs from the object's attribute list. + + + + + Associates a UINT32 value with a key. + + + + + Associates a UINT64 value with a key. + + + + + Associates a double value with a key. + + + + + Associates a GUID value with a key. + + + + + Associates a wide-character string with a key. + + + + + Associates a byte array with a key. + + + + + Associates an IUnknown pointer with a key. + + + + + Locks the attribute store so that no other thread can access it. + + + + + Unlocks the attribute store. + + + + + Retrieves the number of attributes that are set on this object. + + + + + Retrieves an attribute at the specified index. + + + + + Copies all of the attributes from this object into another attribute store. + + + + + Retrieves the value associated with a key. + + + + + Retrieves the data type of the value associated with a key. + + + + + Queries whether a stored attribute value equals a specified PROPVARIANT. + + + + + Compares the attributes on this object with the attributes on another object. + + + + + Retrieves a UINT32 value associated with a key. + + + + + Retrieves a UINT64 value associated with a key. + + + + + Retrieves a double value associated with a key. + + + + + Retrieves a GUID value associated with a key. + + + + + Retrieves the length of a string value associated with a key. + + + + + Retrieves a wide-character string associated with a key. + + + + + Retrieves a wide-character string associated with a key. This method allocates the memory for the string. + + + + + Retrieves the length of a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. This method allocates the memory for the array. + + + + + Retrieves an interface pointer associated with a key. + + + + + Associates an attribute value with a key. + + + + + Removes a key/value pair from the object's attribute list. + + + + + Removes all key/value pairs from the object's attribute list. + + + + + Associates a UINT32 value with a key. + + + + + Associates a UINT64 value with a key. + + + + + Associates a double value with a key. + + + + + Associates a GUID value with a key. + + + + + Associates a wide-character string with a key. + + + + + Associates a byte array with a key. + + + + + Associates an IUnknown pointer with a key. + + + + + Locks the attribute store so that no other thread can access it. + + + + + Unlocks the attribute store. + + + + + Retrieves the number of attributes that are set on this object. + + + + + Retrieves an attribute at the specified index. + + + + + Copies all of the attributes from this object into another attribute store. + + + + + Creates the object associated with this activation object. + + + + + Shuts down the created object. + + + + + Detaches the created object from the activation object. + + + + + Represents a generic collection of IUnknown pointers. + + + + + Retrieves the number of objects in the collection. + + + + + Retrieves an object in the collection. + + + + + Adds an object to the collection. + + + + + Removes an object from the collection. + + + + + Removes an object from the collection. + + + + + Removes all items from the collection. + + + + + IMFMediaEvent - Represents an event generated by a Media Foundation object. Use this interface to get information about the event. + http://msdn.microsoft.com/en-us/library/windows/desktop/ms702249%28v=vs.85%29.aspx + Mfobjects.h + + + + + Retrieves the value associated with a key. + + + + + Retrieves the data type of the value associated with a key. + + + + + Queries whether a stored attribute value equals a specified PROPVARIANT. + + + + + Compares the attributes on this object with the attributes on another object. + + + + + Retrieves a UINT32 value associated with a key. + + + + + Retrieves a UINT64 value associated with a key. + + + + + Retrieves a double value associated with a key. + + + + + Retrieves a GUID value associated with a key. + + + + + Retrieves the length of a string value associated with a key. + + + + + Retrieves a wide-character string associated with a key. + + + + + Retrieves a wide-character string associated with a key. This method allocates the memory for the string. + + + + + Retrieves the length of a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. This method allocates the memory for the array. + + + + + Retrieves an interface pointer associated with a key. + + + + + Associates an attribute value with a key. + + + + + Removes a key/value pair from the object's attribute list. + + + + + Removes all key/value pairs from the object's attribute list. + + + + + Associates a UINT32 value with a key. + + + + + Associates a UINT64 value with a key. + + + + + Associates a double value with a key. + + + + + Associates a GUID value with a key. + + + + + Associates a wide-character string with a key. + + + + + Associates a byte array with a key. + + + + + Associates an IUnknown pointer with a key. + + + + + Locks the attribute store so that no other thread can access it. + + + + + Unlocks the attribute store. + + + + + Retrieves the number of attributes that are set on this object. + + + + + Retrieves an attribute at the specified index. + + + + + Copies all of the attributes from this object into another attribute store. + + + + + Retrieves the event type. + + + virtual HRESULT STDMETHODCALLTYPE GetType( + /* [out] */ __RPC__out MediaEventType *pmet) = 0; + + + + + Retrieves the extended type of the event. + + + virtual HRESULT STDMETHODCALLTYPE GetExtendedType( + /* [out] */ __RPC__out GUID *pguidExtendedType) = 0; + + + + + Retrieves an HRESULT that specifies the event status. + + + virtual HRESULT STDMETHODCALLTYPE GetStatus( + /* [out] */ __RPC__out HRESULT *phrStatus) = 0; + + + + + Retrieves the value associated with the event, if any. + + + virtual HRESULT STDMETHODCALLTYPE GetValue( + /* [out] */ __RPC__out PROPVARIANT *pvValue) = 0; + + + + + Implemented by the Microsoft Media Foundation sink writer object. + + + + + Adds a stream to the sink writer. + + + + + Sets the input format for a stream on the sink writer. + + + + + Initializes the sink writer for writing. + + + + + Delivers a sample to the sink writer. + + + + + Indicates a gap in an input stream. + + + + + Places a marker in the specified stream. + + + + + Notifies the media sink that a stream has reached the end of a segment. + + + + + Flushes one or more streams. + + + + + (Finalize) Completes all writing operations on the sink writer. + + + + + Queries the underlying media sink or encoder for an interface. + + + + + Gets statistics about the performance of the sink writer. + + + + + IMFTransform, defined in mftransform.h + + + + + Retrieves the minimum and maximum number of input and output streams. + + + virtual HRESULT STDMETHODCALLTYPE GetStreamLimits( + /* [out] */ __RPC__out DWORD *pdwInputMinimum, + /* [out] */ __RPC__out DWORD *pdwInputMaximum, + /* [out] */ __RPC__out DWORD *pdwOutputMinimum, + /* [out] */ __RPC__out DWORD *pdwOutputMaximum) = 0; + + + + + Retrieves the current number of input and output streams on this MFT. + + + virtual HRESULT STDMETHODCALLTYPE GetStreamCount( + /* [out] */ __RPC__out DWORD *pcInputStreams, + /* [out] */ __RPC__out DWORD *pcOutputStreams) = 0; + + + + + Retrieves the stream identifiers for the input and output streams on this MFT. + + + virtual HRESULT STDMETHODCALLTYPE GetStreamIDs( + DWORD dwInputIDArraySize, + /* [size_is][out] */ __RPC__out_ecount_full(dwInputIDArraySize) DWORD *pdwInputIDs, + DWORD dwOutputIDArraySize, + /* [size_is][out] */ __RPC__out_ecount_full(dwOutputIDArraySize) DWORD *pdwOutputIDs) = 0; + + + + + Gets the buffer requirements and other information for an input stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetInputStreamInfo( + DWORD dwInputStreamID, + /* [out] */ __RPC__out MFT_INPUT_STREAM_INFO *pStreamInfo) = 0; + + + + + Gets the buffer requirements and other information for an output stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetOutputStreamInfo( + DWORD dwOutputStreamID, + /* [out] */ __RPC__out MFT_OUTPUT_STREAM_INFO *pStreamInfo) = 0; + + + + + Gets the global attribute store for this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetAttributes( + /* [out] */ __RPC__deref_out_opt IMFAttributes **pAttributes) = 0; + + + + + Retrieves the attribute store for an input stream on this MFT. + + + virtual HRESULT STDMETHODCALLTYPE GetInputStreamAttributes( + DWORD dwInputStreamID, + /* [out] */ __RPC__deref_out_opt IMFAttributes **pAttributes) = 0; + + + + + Retrieves the attribute store for an output stream on this MFT. + + + virtual HRESULT STDMETHODCALLTYPE GetOutputStreamAttributes( + DWORD dwOutputStreamID, + /* [out] */ __RPC__deref_out_opt IMFAttributes **pAttributes) = 0; + + + + + Removes an input stream from this MFT. + + + virtual HRESULT STDMETHODCALLTYPE DeleteInputStream( + DWORD dwStreamID) = 0; + + + + + Adds one or more new input streams to this MFT. + + + virtual HRESULT STDMETHODCALLTYPE AddInputStreams( + DWORD cStreams, + /* [in] */ __RPC__in DWORD *adwStreamIDs) = 0; + + + + + Gets an available media type for an input stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetInputAvailableType( + DWORD dwInputStreamID, + DWORD dwTypeIndex, + /* [out] */ __RPC__deref_out_opt IMFMediaType **ppType) = 0; + + + + + Retrieves an available media type for an output stream on this MFT. + + + virtual HRESULT STDMETHODCALLTYPE GetOutputAvailableType( + DWORD dwOutputStreamID, + DWORD dwTypeIndex, + /* [out] */ __RPC__deref_out_opt IMFMediaType **ppType) = 0; + + + + + Sets, tests, or clears the media type for an input stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE SetInputType( + DWORD dwInputStreamID, + /* [in] */ __RPC__in_opt IMFMediaType *pType, + DWORD dwFlags) = 0; + + + + + Sets, tests, or clears the media type for an output stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE SetOutputType( + DWORD dwOutputStreamID, + /* [in] */ __RPC__in_opt IMFMediaType *pType, + DWORD dwFlags) = 0; + + + + + Gets the current media type for an input stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetInputCurrentType( + DWORD dwInputStreamID, + /* [out] */ __RPC__deref_out_opt IMFMediaType **ppType) = 0; + + + + + Gets the current media type for an output stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE GetOutputCurrentType( + DWORD dwOutputStreamID, + /* [out] */ __RPC__deref_out_opt IMFMediaType **ppType) = 0; + + + + + Queries whether an input stream on this Media Foundation transform (MFT) can accept more data. + + + virtual HRESULT STDMETHODCALLTYPE GetInputStatus( + DWORD dwInputStreamID, + /* [out] */ __RPC__out DWORD *pdwFlags) = 0; + + + + + Queries whether the Media Foundation transform (MFT) is ready to produce output data. + + + virtual HRESULT STDMETHODCALLTYPE GetOutputStatus( + /* [out] */ __RPC__out DWORD *pdwFlags) = 0; + + + + + Sets the range of time stamps the client needs for output. + + + virtual HRESULT STDMETHODCALLTYPE SetOutputBounds( + LONGLONG hnsLowerBound, + LONGLONG hnsUpperBound) = 0; + + + + + Sends an event to an input stream on this Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE ProcessEvent( + DWORD dwInputStreamID, + /* [in] */ __RPC__in_opt IMFMediaEvent *pEvent) = 0; + + + + + Sends a message to the Media Foundation transform (MFT). + + + virtual HRESULT STDMETHODCALLTYPE ProcessMessage( + MFT_MESSAGE_TYPE eMessage, + ULONG_PTR ulParam) = 0; + + + + + Delivers data to an input stream on this Media Foundation transform (MFT). + + + virtual /* [local] */ HRESULT STDMETHODCALLTYPE ProcessInput( + DWORD dwInputStreamID, + IMFSample *pSample, + DWORD dwFlags) = 0; + + + + + Generates output from the current input data. + + + virtual /* [local] */ HRESULT STDMETHODCALLTYPE ProcessOutput( + DWORD dwFlags, + DWORD cOutputBufferCount, + /* [size_is][out][in] */ MFT_OUTPUT_DATA_BUFFER *pOutputSamples, + /* [out] */ DWORD *pdwStatus) = 0; + + + + + See mfobjects.h + + + + + Unknown event type. + + + + + Signals a serious error. + + + + + Custom event type. + + + + + A non-fatal error occurred during streaming. + + + + + Session Unknown + + + + + Raised after the IMFMediaSession::SetTopology method completes asynchronously + + + + + Raised by the Media Session when the IMFMediaSession::ClearTopologies method completes asynchronously. + + + + + Raised when the IMFMediaSession::Start method completes asynchronously. + + + + + Raised when the IMFMediaSession::Pause method completes asynchronously. + + + + + Raised when the IMFMediaSession::Stop method completes asynchronously. + + + + + Raised when the IMFMediaSession::Close method completes asynchronously. + + + + + Raised by the Media Session when it has finished playing the last presentation in the playback queue. + + + + + Raised by the Media Session when the playback rate changes. + + + + + Raised by the Media Session when it completes a scrubbing request. + + + + + Raised by the Media Session when the session capabilities change. + + + + + Raised by the Media Session when the status of a topology changes. + + + + + Raised by the Media Session when a new presentation starts. + + + + + Raised by a media source a new presentation is ready. + + + + + License acquisition is about to begin. + + + + + License acquisition is complete. + + + + + Individualization is about to begin. + + + + + Individualization is complete. + + + + + Signals the progress of a content enabler object. + + + + + A content enabler object's action is complete. + + + + + Raised by a trusted output if an error occurs while enforcing the output policy. + + + + + Contains status information about the enforcement of an output policy. + + + + + A media source started to buffer data. + + + + + A media source stopped buffering data. + + + + + The network source started opening a URL. + + + + + The network source finished opening a URL. + + + + + Raised by a media source at the start of a reconnection attempt. + + + + + Raised by a media source at the end of a reconnection attempt. + + + + + Raised by the enhanced video renderer (EVR) when it receives a user event from the presenter. + + + + + Raised by the Media Session when the format changes on a media sink. + + + + + Source Unknown + + + + + Raised when a media source starts without seeking. + + + + + Raised by a media stream when the source starts without seeking. + + + + + Raised when a media source seeks to a new position. + + + + + Raised by a media stream after a call to IMFMediaSource::Start causes a seek in the stream. + + + + + Raised by a media source when it starts a new stream. + + + + + Raised by a media source when it restarts or seeks a stream that is already active. + + + + + Raised by a media source when the IMFMediaSource::Stop method completes asynchronously. + + + + + Raised by a media stream when the IMFMediaSource::Stop method completes asynchronously. + + + + + Raised by a media source when the IMFMediaSource::Pause method completes asynchronously. + + + + + Raised by a media stream when the IMFMediaSource::Pause method completes asynchronously. + + + + + Raised by a media source when a presentation ends. + + + + + Raised by a media stream when the stream ends. + + + + + Raised when a media stream delivers a new sample. + + + + + Signals that a media stream does not have data available at a specified time. + + + + + Raised by a media stream when it starts or stops thinning the stream. + + + + + Raised by a media stream when the media type of the stream changes. + + + + + Raised by a media source when the playback rate changes. + + + + + Raised by the sequencer source when a segment is completed and is followed by another segment. + + + + + Raised by a media source when the source's characteristics change. + + + + + Raised by a media source to request a new playback rate. + + + + + Raised by a media source when it updates its metadata. + + + + + Raised by the sequencer source when the IMFSequencerSource::UpdateTopology method completes asynchronously. + + + + + Sink Unknown + + + + + Raised by a stream sink when it completes the transition to the running state. + + + + + Raised by a stream sink when it completes the transition to the stopped state. + + + + + Raised by a stream sink when it completes the transition to the paused state. + + + + + Raised by a stream sink when the rate has changed. + + + + + Raised by a stream sink to request a new media sample from the pipeline. + + + + + Raised by a stream sink after the IMFStreamSink::PlaceMarker method is called. + + + + + Raised by a stream sink when the stream has received enough preroll data to begin rendering. + + + + + Raised by a stream sink when it completes a scrubbing request. + + + + + Raised by a stream sink when the sink's media type is no longer valid. + + + + + Raised by the stream sinks of the EVR if the video device changes. + + + + + Provides feedback about playback quality to the quality manager. + + + + + Raised when a media sink becomes invalid. + + + + + The audio session display name changed. + + + + + The volume or mute state of the audio session changed + + + + + The audio device was removed. + + + + + The Windows audio server system was shut down. + + + + + The grouping parameters changed for the audio session. + + + + + The audio session icon changed. + + + + + The default audio format for the audio device changed. + + + + + The audio session was disconnected from a Windows Terminal Services session + + + + + The audio session was preempted by an exclusive-mode connection. + + + + + Trust Unknown + + + + + The output policy for a stream changed. + + + + + Content protection message + + + + + The IMFOutputTrustAuthority::SetPolicy method completed. + + + + + DRM License Backup Completed + + + + + DRM License Backup Progress + + + + + DRM License Restore Completed + + + + + DRM License Restore Progress + + + + + DRM License Acquisition Completed + + + + + DRM Individualization Completed + + + + + DRM Individualization Progress + + + + + DRM Proximity Completed + + + + + DRM License Store Cleaned + + + + + DRM Revocation Download Completed + + + + + Transform Unknown + + + + + Sent by an asynchronous MFT to request a new input sample. + + + + + Sent by an asynchronous MFT when new output data is available from the MFT. + + + + + Sent by an asynchronous Media Foundation transform (MFT) when a drain operation is complete. + + + + + Sent by an asynchronous MFT in response to an MFT_MESSAGE_COMMAND_MARKER message. + + + + + Media Foundation attribute guids + http://msdn.microsoft.com/en-us/library/windows/desktop/ms696989%28v=vs.85%29.aspx + + + + + Specifies whether an MFT performs asynchronous processing. + + + + + Enables the use of an asynchronous MFT. + + + + + Contains flags for an MFT activation object. + + + + + Specifies the category for an MFT. + + + + + Contains the class identifier (CLSID) of an MFT. + + + + + Contains the registered input types for a Media Foundation transform (MFT). + + + + + Contains the registered output types for a Media Foundation transform (MFT). + + + + + Contains the symbolic link for a hardware-based MFT. + + + + + Contains the display name for a hardware-based MFT. + + + + + Contains a pointer to the stream attributes of the connected stream on a hardware-based MFT. + + + + + Specifies whether a hardware-based MFT is connected to another hardware-based MFT. + + + + + Specifies the preferred output format for an encoder. + + + + + Specifies whether an MFT is registered only in the application's process. + + + + + Contains configuration properties for an encoder. + + + + + Specifies whether a hardware device source uses the system time for time stamps. + + + + + Contains an IMFFieldOfUseMFTUnlock pointer, which can be used to unlock the MFT. + + + + + Contains the merit value of a hardware codec. + + + + + Specifies whether a decoder is optimized for transcoding rather than for playback. + + + + + Contains a pointer to the proxy object for the application's presentation descriptor. + + + + + Contains a pointer to the presentation descriptor from the protected media path (PMP). + + + + + Specifies the duration of a presentation, in 100-nanosecond units. + + + + + Specifies the total size of the source file, in bytes. + + + + + Specifies the audio encoding bit rate for the presentation, in bits per second. + + + + + Specifies the video encoding bit rate for the presentation, in bits per second. + + + + + Specifies the MIME type of the content. + + + + + Specifies when a presentation was last modified. + + + + + The identifier of the playlist element in the presentation. + + + + + Contains the preferred RFC 1766 language of the media source. + + + + + The time at which the presentation must begin, relative to the start of the media source. + + + + + Specifies whether the audio streams in the presentation have a variable bit rate. + + + + + Media type Major Type + + + + + Media Type subtype + + + + + Audio block alignment + + + + + Audio average bytes per second + + + + + Audio number of channels + + + + + Audio samples per second + + + + + Audio bits per sample + + + + + Enables the source reader or sink writer to use hardware-based Media Foundation transforms (MFTs). + + + + + Contains additional format data for a media type. + + + + + Specifies for a media type whether each sample is independent of the other samples in the stream. + + + + + Specifies for a media type whether the samples have a fixed size. + + + + + Contains a DirectShow format GUID for a media type. + + + + + Specifies the preferred legacy format structure to use when converting an audio media type. + + + + + Specifies for a media type whether the media data is compressed. + + + + + Approximate data rate of the video stream, in bits per second, for a video media type. + + + + + Specifies the payload type of an Advanced Audio Coding (AAC) stream. + 0 - The stream contains raw_data_block elements only + 1 - Audio Data Transport Stream (ADTS). The stream contains an adts_sequence, as defined by MPEG-2. + 2 - Audio Data Interchange Format (ADIF). The stream contains an adif_sequence, as defined by MPEG-2. + 3 - The stream contains an MPEG-4 audio transport stream with a synchronization layer (LOAS) and a multiplex layer (LATM). + + + + + Specifies the audio profile and level of an Advanced Audio Coding (AAC) stream, as defined by ISO/IEC 14496-3. + + + + + Main interface for using Media Foundation with NAudio + + + + + initializes MediaFoundation - only needs to be called once per process + + + + + Enumerate the installed MediaFoundation transforms in the specified category + + A category from MediaFoundationTransformCategories + + + + + uninitializes MediaFoundation + + + + + Creates a Media type + + + + + Creates a media type from a WaveFormat + + + + + Creates a memory buffer of the specified size + + Memory buffer size in bytes + The memory buffer + + + + Creates a sample object + + The sample object + + + + Creates a new attributes store + + Initial size + The attributes store + + + + Creates a media foundation byte stream based on a stream object + (usable with WinRT streams) + + The input stream + A media foundation byte stream + + + + Creates a source reader based on a byte stream + + The byte stream + A media foundation source reader + + + + Interop definitions for MediaFoundation + thanks to Lucian Wischik for the initial work on many of these definitions (also various interfaces) + n.b. the goal is to make as much of this internal as possible, and provide + better .NET APIs using the MediaFoundationApi class instead + + + + + All streams + + + + + First audio stream + + + + + First video stream + + + + + Media source + + + + + Media Foundation SDK Version + + + + + Media Foundation API Version + + + + + Media Foundation Version + + + + + Initializes Microsoft Media Foundation. + + + + + Shuts down the Microsoft Media Foundation platform + + + + + Creates an empty media type. + + + + + Initializes a media type from a WAVEFORMATEX structure. + + + + + Converts a Media Foundation audio media type to a WAVEFORMATEX structure. + + TODO: try making second parameter out WaveFormatExtraData + + + + Creates the source reader from a URL. + + + + + Creates the source reader from a byte stream. + + + + + Creates the sink writer from a URL or byte stream. + + + + + Creates a Microsoft Media Foundation byte stream that wraps an IRandomAccessStream object. + + + + + Gets a list of Microsoft Media Foundation transforms (MFTs) that match specified search criteria. + + + + + Creates an empty media sample. + + + + + Allocates system memory and creates a media buffer to manage it. + + + + + Creates an empty attribute store. + + + + + Gets a list of output formats from an audio encoder. + + + + + IMFByteStream + http://msdn.microsoft.com/en-gb/library/windows/desktop/ms698720%28v=vs.85%29.aspx + + + + + Retrieves the characteristics of the byte stream. + virtual HRESULT STDMETHODCALLTYPE GetCapabilities(/*[out]*/ __RPC__out DWORD *pdwCapabilities) = 0; + + + + + Retrieves the length of the stream. + virtual HRESULT STDMETHODCALLTYPE GetLength(/*[out]*/ __RPC__out QWORD *pqwLength) = 0; + + + + + Sets the length of the stream. + virtual HRESULT STDMETHODCALLTYPE SetLength(/*[in]*/ QWORD qwLength) = 0; + + + + + Retrieves the current read or write position in the stream. + virtual HRESULT STDMETHODCALLTYPE GetCurrentPosition(/*[out]*/ __RPC__out QWORD *pqwPosition) = 0; + + + + + Sets the current read or write position. + virtual HRESULT STDMETHODCALLTYPE SetCurrentPosition(/*[in]*/ QWORD qwPosition) = 0; + + + + + Queries whether the current position has reached the end of the stream. + virtual HRESULT STDMETHODCALLTYPE IsEndOfStream(/*[out]*/ __RPC__out BOOL *pfEndOfStream) = 0; + + + + + Reads data from the stream. + virtual HRESULT STDMETHODCALLTYPE Read(/*[size_is][out]*/ __RPC__out_ecount_full(cb) BYTE *pb, /*[in]*/ ULONG cb, /*[out]*/ __RPC__out ULONG *pcbRead) = 0; + + + + + Begins an asynchronous read operation from the stream. + virtual /*[local]*/ HRESULT STDMETHODCALLTYPE BeginRead(/*[out]*/ _Out_writes_bytes_(cb) BYTE *pb, /*[in]*/ ULONG cb, /*[in]*/ IMFAsyncCallback *pCallback, /*[in]*/ IUnknown *punkState) = 0; + + + + + Completes an asynchronous read operation. + virtual /*[local]*/ HRESULT STDMETHODCALLTYPE EndRead(/*[in]*/ IMFAsyncResult *pResult, /*[out]*/ _Out_ ULONG *pcbRead) = 0; + + + + + Writes data to the stream. + virtual HRESULT STDMETHODCALLTYPE Write(/*[size_is][in]*/ __RPC__in_ecount_full(cb) const BYTE *pb, /*[in]*/ ULONG cb, /*[out]*/ __RPC__out ULONG *pcbWritten) = 0; + + + + + Begins an asynchronous write operation to the stream. + virtual /*[local]*/ HRESULT STDMETHODCALLTYPE BeginWrite(/*[in]*/ _In_reads_bytes_(cb) const BYTE *pb, /*[in]*/ ULONG cb, /*[in]*/ IMFAsyncCallback *pCallback, /*[in]*/ IUnknown *punkState) = 0; + + + + + Completes an asynchronous write operation. + virtual /*[local]*/ HRESULT STDMETHODCALLTYPE EndWrite(/*[in]*/ IMFAsyncResult *pResult, /*[out]*/ _Out_ ULONG *pcbWritten) = 0; + + + + + Moves the current position in the stream by a specified offset. + virtual HRESULT STDMETHODCALLTYPE Seek(/*[in]*/ MFBYTESTREAM_SEEK_ORIGIN SeekOrigin, /*[in]*/ LONGLONG llSeekOffset, /*[in]*/ DWORD dwSeekFlags, /*[out]*/ __RPC__out QWORD *pqwCurrentPosition) = 0; + + + + + Clears any internal buffers used by the stream. + virtual HRESULT STDMETHODCALLTYPE Flush( void) = 0; + + + + + Closes the stream and releases any resources associated with the stream. + virtual HRESULT STDMETHODCALLTYPE Close( void) = 0; + + + + + IMFMediaBuffer + http://msdn.microsoft.com/en-gb/library/windows/desktop/ms696261%28v=vs.85%29.aspx + + + + + Gives the caller access to the memory in the buffer. + + + + + Unlocks a buffer that was previously locked. + + + + + Retrieves the length of the valid data in the buffer. + + + + + Sets the length of the valid data in the buffer. + + + + + Retrieves the allocated size of the buffer. + + + + + Represents a description of a media format. + http://msdn.microsoft.com/en-us/library/windows/desktop/ms704850%28v=vs.85%29.aspx + + + + + Retrieves the value associated with a key. + + + + + Retrieves the data type of the value associated with a key. + + + + + Queries whether a stored attribute value equals a specified PROPVARIANT. + + + + + Compares the attributes on this object with the attributes on another object. + + + + + Retrieves a UINT32 value associated with a key. + + + + + Retrieves a UINT64 value associated with a key. + + + + + Retrieves a double value associated with a key. + + + + + Retrieves a GUID value associated with a key. + + + + + Retrieves the length of a string value associated with a key. + + + + + Retrieves a wide-character string associated with a key. + + + + + Retrieves a wide-character string associated with a key. This method allocates the memory for the string. + + + + + Retrieves the length of a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. This method allocates the memory for the array. + + + + + Retrieves an interface pointer associated with a key. + + + + + Associates an attribute value with a key. + + + + + Removes a key/value pair from the object's attribute list. + + + + + Removes all key/value pairs from the object's attribute list. + + + + + Associates a UINT32 value with a key. + + + + + Associates a UINT64 value with a key. + + + + + Associates a double value with a key. + + + + + Associates a GUID value with a key. + + + + + Associates a wide-character string with a key. + + + + + Associates a byte array with a key. + + + + + Associates an IUnknown pointer with a key. + + + + + Locks the attribute store so that no other thread can access it. + + + + + Unlocks the attribute store. + + + + + Retrieves the number of attributes that are set on this object. + + + + + Retrieves an attribute at the specified index. + + + + + Copies all of the attributes from this object into another attribute store. + + + + + Retrieves the major type of the format. + + + + + Queries whether the media type is a compressed format. + + + + + Compares two media types and determines whether they are identical. + + + + + Retrieves an alternative representation of the media type. + + + + + Frees memory that was allocated by the GetRepresentation method. + + + + + http://msdn.microsoft.com/en-gb/library/windows/desktop/ms702192%28v=vs.85%29.aspx + + + + + Retrieves the value associated with a key. + + + + + Retrieves the data type of the value associated with a key. + + + + + Queries whether a stored attribute value equals a specified PROPVARIANT. + + + + + Compares the attributes on this object with the attributes on another object. + + + + + Retrieves a UINT32 value associated with a key. + + + + + Retrieves a UINT64 value associated with a key. + + + + + Retrieves a double value associated with a key. + + + + + Retrieves a GUID value associated with a key. + + + + + Retrieves the length of a string value associated with a key. + + + + + Retrieves a wide-character string associated with a key. + + + + + Retrieves a wide-character string associated with a key. This method allocates the memory for the string. + + + + + Retrieves the length of a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. + + + + + Retrieves a byte array associated with a key. This method allocates the memory for the array. + + + + + Retrieves an interface pointer associated with a key. + + + + + Associates an attribute value with a key. + + + + + Removes a key/value pair from the object's attribute list. + + + + + Removes all key/value pairs from the object's attribute list. + + + + + Associates a UINT32 value with a key. + + + + + Associates a UINT64 value with a key. + + + + + Associates a double value with a key. + + + + + Associates a GUID value with a key. + + + + + Associates a wide-character string with a key. + + + + + Associates a byte array with a key. + + + + + Associates an IUnknown pointer with a key. + + + + + Locks the attribute store so that no other thread can access it. + + + + + Unlocks the attribute store. + + + + + Retrieves the number of attributes that are set on this object. + + + + + Retrieves an attribute at the specified index. + + + + + Copies all of the attributes from this object into another attribute store. + + + + + Retrieves flags associated with the sample. + + + + + Sets flags associated with the sample. + + + + + Retrieves the presentation time of the sample. + + + + + Sets the presentation time of the sample. + + + + + Retrieves the duration of the sample. + + + + + Sets the duration of the sample. + + + + + Retrieves the number of buffers in the sample. + + + + + Retrieves a buffer from the sample. + + + + + Converts a sample with multiple buffers into a sample with a single buffer. + + + + + Adds a buffer to the end of the list of buffers in the sample. + + + + + Removes a buffer at a specified index from the sample. + + + + + Removes all buffers from the sample. + + + + + Retrieves the total length of the valid data in all of the buffers in the sample. + + + + + Copies the sample data to a buffer. + + + + + IMFSourceReader interface + http://msdn.microsoft.com/en-us/library/windows/desktop/dd374655%28v=vs.85%29.aspx + + + + + Queries whether a stream is selected. + + + + + Selects or deselects one or more streams. + + + + + Gets a format that is supported natively by the media source. + + + + + Gets the current media type for a stream. + + + + + Sets the media type for a stream. + + + + + Seeks to a new position in the media source. + + + + + Reads the next sample from the media source. + + + + + Flushes one or more streams. + + + + + Queries the underlying media source or decoder for an interface. + + + + + Gets an attribute from the underlying media source. + + + + + Contains flags that indicate the status of the IMFSourceReader::ReadSample method + http://msdn.microsoft.com/en-us/library/windows/desktop/dd375773(v=vs.85).aspx + + + + + No Error + + + + + An error occurred. If you receive this flag, do not make any further calls to IMFSourceReader methods. + + + + + The source reader reached the end of the stream. + + + + + One or more new streams were created + + + + + The native format has changed for one or more streams. The native format is the format delivered by the media source before any decoders are inserted. + + + + + The current media has type changed for one or more streams. To get the current media type, call the IMFSourceReader::GetCurrentMediaType method. + + + + + There is a gap in the stream. This flag corresponds to an MEStreamTick event from the media source. + + + + + All transforms inserted by the application have been removed for a particular stream. + + + + + Media Foundation Transform Categories + + + + + MFT_CATEGORY_VIDEO_DECODER + + + + + MFT_CATEGORY_VIDEO_ENCODER + + + + + MFT_CATEGORY_VIDEO_EFFECT + + + + + MFT_CATEGORY_MULTIPLEXER + + + + + MFT_CATEGORY_DEMULTIPLEXER + + + + + MFT_CATEGORY_AUDIO_DECODER + + + + + MFT_CATEGORY_AUDIO_ENCODER + + + + + MFT_CATEGORY_AUDIO_EFFECT + + + + + MFT_CATEGORY_VIDEO_PROCESSOR + + + + + MFT_CATEGORY_OTHER + + + + + Contains information about an input stream on a Media Foundation transform (MFT) + + + + + Maximum amount of time between an input sample and the corresponding output sample, in 100-nanosecond units. + + + + + Bitwise OR of zero or more flags from the _MFT_INPUT_STREAM_INFO_FLAGS enumeration. + + + + + The minimum size of each input buffer, in bytes. + + + + + Maximum amount of input data, in bytes, that the MFT holds to perform lookahead. + + + + + The memory alignment required for input buffers. If the MFT does not require a specific alignment, the value is zero. + + + + + Contains information about an output buffer for a Media Foundation transform. + + + + + Output stream identifier. + + + + + Pointer to the IMFSample interface. + + + + + Before calling ProcessOutput, set this member to zero. + + + + + Before calling ProcessOutput, set this member to NULL. + + + + + Contains information about an output stream on a Media Foundation transform (MFT). + + + + + Bitwise OR of zero or more flags from the _MFT_OUTPUT_STREAM_INFO_FLAGS enumeration. + + + + + Minimum size of each output buffer, in bytes. + + + + + The memory alignment required for output buffers. + + + + + Defines messages for a Media Foundation transform (MFT). + + + + + Requests the MFT to flush all stored data. + + + + + Requests the MFT to drain any stored data. + + + + + Sets or clears the Direct3D Device Manager for DirectX Video Accereration (DXVA). + + + + + Drop samples - requires Windows 7 + + + + + Command Tick - requires Windows 8 + + + + + Notifies the MFT that streaming is about to begin. + + + + + Notifies the MFT that streaming is about to end. + + + + + Notifies the MFT that an input stream has ended. + + + + + Notifies the MFT that the first sample is about to be processed. + + + + + Marks a point in the stream. This message applies only to asynchronous MFTs. Requires Windows 7 + + + + + Contains media type information for registering a Media Foundation transform (MFT). + + + + + The major media type. + + + + + The Media Subtype + + + + + Contains statistics about the performance of the sink writer. + + + + + The size of the structure, in bytes. + + + + + The time stamp of the most recent sample given to the sink writer. + + + + + The time stamp of the most recent sample to be encoded. + + + + + The time stamp of the most recent sample given to the media sink. + + + + + The time stamp of the most recent stream tick. + + + + + The system time of the most recent sample request from the media sink. + + + + + The number of samples received. + + + + + The number of samples encoded. + + + + + The number of samples given to the media sink. + + + + + The number of stream ticks received. + + + + + The amount of data, in bytes, currently waiting to be processed. + + + + + The total amount of data, in bytes, that has been sent to the media sink. + + + + + The number of pending sample requests. + + + + + The average rate, in media samples per 100-nanoseconds, at which the application sent samples to the sink writer. + + + + + The average rate, in media samples per 100-nanoseconds, at which the sink writer sent samples to the encoder + + + + + The average rate, in media samples per 100-nanoseconds, at which the sink writer sent samples to the media sink. + + + + + Contains flags for registering and enumeration Media Foundation transforms (MFTs). + + + + + None + + + + + The MFT performs synchronous data processing in software. + + + + + The MFT performs asynchronous data processing in software. + + + + + The MFT performs hardware-based data processing, using either the AVStream driver or a GPU-based proxy MFT. + + + + + The MFT that must be unlocked by the application before use. + + + + + For enumeration, include MFTs that were registered in the caller's process. + + + + + The MFT is optimized for transcoding rather than playback. + + + + + For enumeration, sort and filter the results. + + + + + Bitwise OR of all the flags, excluding MFT_ENUM_FLAG_SORTANDFILTER. + + + + + Indicates the status of an input stream on a Media Foundation transform (MFT). + + + + + None + + + + + The input stream can receive more data at this time. + + + + + Describes an input stream on a Media Foundation transform (MFT). + + + + + No flags set + + + + + Each media sample (IMFSample interface) of input data must contain complete, unbroken units of data. + + + + + Each media sample that the client provides as input must contain exactly one unit of data, as defined for the MFT_INPUT_STREAM_WHOLE_SAMPLES flag. + + + + + All input samples must be the same size. + + + + + MTF Input Stream Holds buffers + + + + + The MFT does not hold input samples after the IMFTransform::ProcessInput method returns. + + + + + This input stream can be removed by calling IMFTransform::DeleteInputStream. + + + + + This input stream is optional. + + + + + The MFT can perform in-place processing. + + + + + Defines flags for the IMFTransform::ProcessOutput method. + + + + + None + + + + + The MFT can still generate output from this stream without receiving any more input. + + + + + The format has changed on this output stream, or there is a new preferred format for this stream. + + + + + The MFT has removed this output stream. + + + + + There is no sample ready for this stream. + + + + + Indicates whether a Media Foundation transform (MFT) can produce output data. + + + + + None + + + + + There is a sample available for at least one output stream. + + + + + Describes an output stream on a Media Foundation transform (MFT). + + + + + No flags set + + + + + Each media sample (IMFSample interface) of output data from the MFT contains complete, unbroken units of data. + + + + + Each output sample contains exactly one unit of data, as defined for the MFT_OUTPUT_STREAM_WHOLE_SAMPLES flag. + + + + + All output samples are the same size. + + + + + The MFT can discard the output data from this output stream, if requested by the client. + + + + + This output stream is optional. + + + + + The MFT provides the output samples for this stream, either by allocating them internally or by operating directly on the input samples. + + + + + The MFT can either provide output samples for this stream or it can use samples that the client allocates. + + + + + The MFT does not require the client to process the output for this stream. + + + + + The MFT might remove this output stream during streaming. + + + + + Defines flags for processing output samples in a Media Foundation transform (MFT). + + + + + None + + + + + Do not produce output for streams in which the pSample member of the MFT_OUTPUT_DATA_BUFFER structure is NULL. + + + + + Regenerates the last output sample. + + + + + Process Output Status flags + + + + + None + + + + + The Media Foundation transform (MFT) has created one or more new output streams. + + + + + Defines flags for the setting or testing the media type on a Media Foundation transform (MFT). + + + + + None + + + + + Test the proposed media type, but do not set it. + + + + + MIDI In Message Information + + + + + Create a new MIDI In Message EventArgs + + + + + + + The Raw message received from the MIDI In API + + + + + The raw message interpreted as a MidiEvent + + + + + The timestamp in milliseconds for this message + + + + + these will become extension methods once we move to .NET 3.5 + + + + + Checks if the buffer passed in is entirely full of nulls + + + + + Converts to a string containing the buffer described in hex + + + + + Decodes the buffer using the specified encoding, stopping at the first null + + + + + Concatenates the given arrays into a single array. + + The arrays to concatenate + The concatenated resulting array. + + + + Helper to get descriptions + + + + + Describes the Guid by looking for a FieldDescription attribute on the specified class + + + + + WavePosition extension methods + + + + + Get Position as timespan + + + + + Methods for converting between IEEE 80-bit extended double precision + and standard C# double precision. + + + + + Converts a C# double precision number to an 80-bit + IEEE extended double precision number (occupying 10 bytes). + + The double precision number to convert to IEEE extended. + An array of 10 bytes containing the IEEE extended number. + + + + Converts an IEEE 80-bit extended precision number to a + C# double precision number. + + The 80-bit IEEE extended number (as an array of 10 bytes). + A C# double precision number that is a close representation of the IEEE extended number. + + + + General purpose native methods for internal NAudio use + + + + + ASIODriverCapability holds all the information from the ASIODriver. + Use ASIODriverExt to get the Capabilities + + + + + ASIO Sample Type + + + + + Int 16 MSB + + + + + Int 24 MSB (used for 20 bits as well) + + + + + Int 32 MSB + + + + + IEEE 754 32 bit float + + + + + IEEE 754 64 bit double float + + + + + 32 bit data with 16 bit alignment + + + + + 32 bit data with 18 bit alignment + + + + + 32 bit data with 20 bit alignment + + + + + 32 bit data with 24 bit alignment + + + + + Int 16 LSB + + + + + Int 24 LSB + used for 20 bits as well + + + + + Int 32 LSB + + + + + IEEE 754 32 bit float, as found on Intel x86 architecture + + + + + IEEE 754 64 bit double float, as found on Intel x86 architecture + + + + + 32 bit data with 16 bit alignment + + + + + 32 bit data with 18 bit alignment + + + + + 32 bit data with 20 bit alignment + + + + + 32 bit data with 24 bit alignment + + + + + DSD 1 bit data, 8 samples per byte. First sample in Least significant bit. + + + + + DSD 1 bit data, 8 samples per byte. First sample in Most significant bit. + + + + + DSD 8 bit data, 1 sample per byte. No Endianness required. + + + + + Flags for use with acmDriverAdd + + + + + ACM_DRIVERADDF_LOCAL + + + + + ACM_DRIVERADDF_GLOBAL + + + + + ACM_DRIVERADDF_FUNCTION + + + + + ACM_DRIVERADDF_NOTIFYHWND + + + + + ADSR sample provider allowing you to specify attack, decay, sustain and release values + + + + + Like IWaveProvider, but makes it much simpler to put together a 32 bit floating + point mixing engine + + + + + Fill the specified buffer with 32 bit floating point samples + + The buffer to fill with samples. + Offset into buffer + The number of samples to read + the number of samples written to the buffer. + + + + Gets the WaveFormat of this Sample Provider. + + The wave format. + + + + Creates a new AdsrSampleProvider with default values + + + + + Reads audio from this sample provider + + + + + Enters the Release phase + + + + + Attack time in seconds + + + + + Release time in seconds + + + + + The output WaveFormat + + + + + Sample Provider to allow fading in and out + + + + + Creates a new FadeInOutSampleProvider + + The source stream with the audio to be faded in or out + If true, we start faded out + + + + Requests that a fade-in begins (will start on the next call to Read) + + Duration of fade in milliseconds + + + + Requests that a fade-out begins (will start on the next call to Read) + + Duration of fade in milliseconds + + + + Reads samples from this sample provider + + Buffer to read into + Offset within buffer to write to + Number of samples desired + Number of samples read + + + + WaveFormat of this SampleProvider + + + + + Allows any number of inputs to be patched to outputs + Uses could include swapping left and right channels, turning mono into stereo, + feeding different input sources to different soundcard outputs etc + + + + + Creates a multiplexing sample provider, allowing re-patching of input channels to different + output channels + + Input sample providers. Must all be of the same sample rate, but can have any number of channels + Desired number of output channels. + + + + persistent temporary buffer to prevent creating work for garbage collector + + + + + Reads samples from this sample provider + + Buffer to be filled with sample data + Offset into buffer to start writing to, usually 0 + Number of samples required + Number of samples read + + + + Connects a specified input channel to an output channel + + Input Channel index (zero based). Must be less than InputChannelCount + Output Channel index (zero based). Must be less than OutputChannelCount + + + + The output WaveFormat for this SampleProvider + + + + + The number of input channels. Note that this is not the same as the number of input wave providers. If you pass in + one stereo and one mono input provider, the number of input channels is three. + + + + + The number of output channels, as specified in the constructor. + + + + + Allows you to: + 1. insert a pre-delay of silence before the source begins + 2. skip over a certain amount of the beginning of the source + 3. only play a set amount from the source + 4. insert silence at the end after the source is complete + + + + + Creates a new instance of offsetSampleProvider + + The Source Sample Provider to read from + + + + Reads from this sample provider + + Sample buffer + Offset within sample buffer to read to + Number of samples required + Number of samples read + + + + Number of samples of silence to insert before playing source + + + + + Amount of silence to insert before playing + + + + + Number of samples in source to discard + + + + + Amount of audio to skip over from the source before beginning playback + + + + + Number of samples to read from source (if 0, then read it all) + + + + + Amount of audio to take from the source (TimeSpan.Zero means play to end) + + + + + Number of samples of silence to insert after playing source + + + + + Amount of silence to insert after playing source + + + + + The WaveFormat of this SampleProvider + + + + + Converts an IWaveProvider containing 32 bit PCM to an + ISampleProvider + + + + + Helper base class for classes converting to ISampleProvider + + + + + Source Wave Provider + + + + + Source buffer (to avoid constantly creating small buffers during playback) + + + + + Initialises a new instance of SampleProviderConverterBase + + Source Wave provider + + + + Reads samples from the source wave provider + + Sample buffer + Offset into sample buffer + Number of samples required + Number of samples read + + + + Ensure the source buffer exists and is big enough + + Bytes required + + + + Wave format of this wave provider + + + + + Initialises a new instance of Pcm32BitToSampleProvider + + Source Wave Provider + + + + Reads floating point samples from this sample provider + + sample buffer + offset within sample buffer to write to + number of samples required + number of samples provided + + + + Utility class for converting to SampleProvider + + + + + Helper function to go from IWaveProvider to a SampleProvider + Must already be PCM or IEEE float + + The WaveProvider to convert + A sample provider + + + + Converts a sample provider to 16 bit PCM, optionally clipping and adjusting volume along the way + + + + + Generic interface for all WaveProviders. + + + + + Fill the specified buffer with wave data. + + The buffer to fill of wave data. + Offset into buffer + The number of bytes to read + the number of bytes written to the buffer. + + + + Gets the WaveFormat of this WaveProvider. + + The wave format. + + + + Converts from an ISampleProvider (IEEE float) to a 16 bit PCM IWaveProvider. + Number of channels and sample rate remain unchanged. + + The input source provider + + + + Reads bytes from this wave stream + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + + + + + + Volume of this channel. 1.0 = full scale + + + + + Converts a sample provider to 24 bit PCM, optionally clipping and adjusting volume along the way + + + + + Converts from an ISampleProvider (IEEE float) to a 16 bit PCM IWaveProvider. + Number of channels and sample rate remain unchanged. + + The input source provider + + + + Reads bytes from this wave stream, clipping if necessary + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + The Format of this IWaveProvider + + + + + + Volume of this channel. 1.0 = full scale, 0.0 to mute + + + + + Signal Generator + Sin, Square, Triangle, SawTooth, White Noise, Pink Noise, Sweep. + + + Posibility to change ISampleProvider + Example : + --------- + WaveOut _waveOutGene = new WaveOut(); + WaveGenerator wg = new SignalGenerator(); + wg.Type = ... + wg.Frequency = ... + wg ... + _waveOutGene.Init(wg); + _waveOutGene.Play(); + + + + + Initializes a new instance for the Generator (Default :: 44.1Khz, 2 channels, Sinus, Frequency = 440, Gain = 1) + + + + + Initializes a new instance for the Generator (UserDef SampleRate & Channels) + + Desired sample rate + Number of channels + + + + Reads from this provider. + + + + + Private :: Random for WhiteNoise & Pink Noise (Value form -1 to 1) + + Random value from -1 to +1 + + + + The waveformat of this WaveProvider (same as the source) + + + + + Frequency for the Generator. (20.0 - 20000.0 Hz) + Sin, Square, Triangle, SawTooth, Sweep (Start Frequency). + + + + + Return Log of Frequency Start (Read only) + + + + + End Frequency for the Sweep Generator. (Start Frequency in Frequency) + + + + + Return Log of Frequency End (Read only) + + + + + Gain for the Generator. (0.0 to 1.0) + + + + + Channel PhaseReverse + + + + + Type of Generator. + + + + + Length Seconds for the Sweep Generator. + + + + + Signal Generator type + + + + + Pink noise + + + + + White noise + + + + + Sweep + + + + + Sine wave + + + + + Square wave + + + + + Triangle Wave + + + + + Sawtooth wave + + + + + Helper class turning an already 64 bit floating point IWaveProvider + into an ISampleProvider - hopefully not needed for most applications + + + + + Initializes a new instance of the WaveToSampleProvider class + + Source wave provider, must be IEEE float + + + + Reads from this provider + + + + + Fully managed resampling sample provider, based on the WDL Resampler + + + + + Constructs a new resampler + + Source to resample + Desired output sample rate + + + + Reads from this sample provider + + + + + Output WaveFormat + + + + + Useful extension methods to make switching between WaveAndSampleProvider easier + + + + + Converts a WaveProvider into a SampleProvider (only works for PCM) + + WaveProvider to convert + + + + + Allows sending a SampleProvider directly to an IWavePlayer without needing to convert + back to an IWaveProvider + + The WavePlayer + + + + + + Recording using waveIn api with event callbacks. + Use this for recording in non-gui applications + Events are raised as recorded buffers are made available + + + + + Generic interface for wave recording + + + + + Start Recording + + + + + Stop Recording + + + + + Recording WaveFormat + + + + + Indicates recorded data is available + + + + + Indicates that all recorded data has now been received. + + + + + Prepares a Wave input device for recording + + + + + Retrieves the capabilities of a waveIn device + + Device to test + The WaveIn device capabilities + + + + Start recording + + + + + Stop recording + + + + + Dispose pattern + + + + + Microphone Level + + + + + Dispose method + + + + + Indicates recorded data is available + + + + + Indicates that all recorded data has now been received. + + + + + Returns the number of Wave In devices available in the system + + + + + Milliseconds for the buffer. Recommended value is 100ms + + + + + Number of Buffers to use (usually 2 or 3) + + + + + The device number to use + + + + + WaveFormat we are recording in + + + + + Audio Capture using Wasapi + See http://msdn.microsoft.com/en-us/library/dd370800%28VS.85%29.aspx + + + + + Initialises a new instance of the WASAPI capture class + + + + + Initialises a new instance of the WASAPI capture class + + Capture device to use + + + + Initializes a new instance of the class. + + The capture device. + true if sync is done with event. false use sleep. + + + + Gets the default audio capture device + + The default audio capture device + + + + To allow overrides to specify different flags (e.g. loopback) + + + + + Start Recording + + + + + Stop Recording (requests a stop, wait for RecordingStopped event to know it has finished) + + + + + Dispose + + + + + Indicates recorded data is available + + + + + Indicates that all recorded data has now been received. + + + + + Share Mode - set before calling StartRecording + + + + + Recording wave format + + + + + Contains the name and CLSID of a DirectX Media Object + + + + + Initializes a new instance of DmoDescriptor + + + + + Name + + + + + Clsid + + + + + DirectX Media Object Enumerator + + + + + Get audio effect names + + Audio effect names + + + + Get audio encoder names + + Audio encoder names + + + + Get audio decoder names + + Audio decoder names + + + + DMO Guids for use with DMOEnum + dmoreg.h + + + + + MediaErr.h + + + + + DMO_PARTIAL_MEDIATYPE + + + + + defined in Medparam.h + + + + + Windows Media Resampler Props + wmcodecdsp.h + + + + + Range is 1 to 60 + + + + + Specifies the channel matrix. + + + + + Attempting to implement the COM IMediaBuffer interface as a .NET object + Not sure what will happen when I pass this to an unmanaged object + + + + + IMediaBuffer Interface + + + + + Set Length + + Length + HRESULT + + + + Get Max Length + + Max Length + HRESULT + + + + Get Buffer and Length + + Pointer to variable into which to write the Buffer Pointer + Pointer to variable into which to write the Valid Data Length + HRESULT + + + + Creates a new Media Buffer + + Maximum length in bytes + + + + Dispose and free memory for buffer + + + + + Finalizer + + + + + Set length of valid data in the buffer + + length + HRESULT + + + + Gets the maximum length of the buffer + + Max length (output parameter) + HRESULT + + + + Gets buffer and / or length + + Pointer to variable into which buffer pointer should be written + Pointer to variable into which valid data length should be written + HRESULT + + + + Loads data into this buffer + + Data to load + Number of bytes to load + + + + Retrieves the data in the output buffer + + buffer to retrieve into + offset within that buffer + + + + Length of data in the media buffer + + + + + Media Object + + + + + Creates a new Media Object + + Media Object COM interface + + + + Gets the input media type for the specified input stream + + Input stream index + Input type index + DMO Media Type or null if there are no more input types + + + + Gets the DMO Media Output type + + The output stream + Output type index + DMO Media Type or null if no more available + + + + retrieves the media type that was set for an output stream, if any + + Output stream index + DMO Media Type or null if no more available + + + + Enumerates the supported input types + + Input stream index + Enumeration of input types + + + + Enumerates the output types + + Output stream index + Enumeration of supported output types + + + + Querys whether a specified input type is supported + + Input stream index + Media type to check + true if supports + + + + Sets the input type helper method + + Input stream index + Media type + Flags (can be used to test rather than set) + + + + Sets the input type + + Input stream index + Media Type + + + + Sets the input type to the specified Wave format + + Input stream index + Wave format + + + + Requests whether the specified Wave format is supported as an input + + Input stream index + Wave format + true if supported + + + + Helper function to make a DMO Media Type to represent a particular WaveFormat + + + + + Checks if a specified output type is supported + n.b. you may need to set the input type first + + Output stream index + Media type + True if supported + + + + Tests if the specified Wave Format is supported for output + n.b. may need to set the input type first + + Output stream index + Wave format + True if supported + + + + Helper method to call SetOutputType + + + + + Sets the output type + n.b. may need to set the input type first + + Output stream index + Media type to set + + + + Set output type to the specified wave format + n.b. may need to set input type first + + Output stream index + Wave format + + + + Get Input Size Info + + Input Stream Index + Input Size Info + + + + Get Output Size Info + + Output Stream Index + Output Size Info + + + + Process Input + + Input Stream index + Media Buffer + Flags + Timestamp + Duration + + + + Process Output + + Flags + Output buffer count + Output buffers + + + + Gives the DMO a chance to allocate any resources needed for streaming + + + + + Tells the DMO to free any resources needed for streaming + + + + + Gets maximum input latency + + input stream index + Maximum input latency as a ref-time + + + + Flushes all buffered data + + + + + Report a discontinuity on the specified input stream + + Input Stream index + + + + Is this input stream accepting data? + + Input Stream index + true if accepting data + + + + Experimental code, not currently being called + Not sure if it is necessary anyway + + + + + Number of input streams + + + + + Number of output streams + + + + + Media Object Size Info + + + + + Media Object Size Info + + + + + ToString + + + + + Minimum Buffer Size, in bytes + + + + + Max Lookahead + + + + + Alignment + + + + + MP_PARAMINFO + + + + + MP_TYPE + + + + + MPT_INT + + + + + MPT_FLOAT + + + + + MPT_BOOL + + + + + MPT_ENUM + + + + + MPT_MAX + + + + + MP_CURVE_TYPE + + + + + uuids.h, ksuuids.h + + + + + implements IMediaObject (DirectX Media Object) + implements IMFTransform (Media Foundation Transform) + On Windows XP, it is always an MM (if present at all) + + + + + Windows Media MP3 Decoder (as a DMO) + WORK IN PROGRESS - DO NOT USE! + + + + + Creates a new Resampler based on the DMO Resampler + + + + + Dispose code - experimental at the moment + Was added trying to track down why Resampler crashes NUnit + This code not currently being called by ResamplerDmoStream + + + + + Media Object + + + + + BiQuad filter + + + + + Passes a single sample through the filter + + Input sample + Output sample + + + + Set this up as a low pass filter + + Sample Rate + Cut-off Frequency + Bandwidth + + + + Set this up as a peaking EQ + + Sample Rate + Centre Frequency + Bandwidth (Q) + Gain in decibels + + + + Set this as a high pass filter + + + + + Create a low pass filter + + + + + Create a High pass filter + + + + + Create a bandpass filter with constant skirt gain + + + + + Create a bandpass filter with constant peak gain + + + + + Creates a notch filter + + + + + Creaes an all pass filter + + + + + Create a Peaking EQ + + + + + H(s) = A * (s^2 + (sqrt(A)/Q)*s + A)/(A*s^2 + (sqrt(A)/Q)*s + 1) + + + + a "shelf slope" parameter (for shelving EQ only). + When S = 1, the shelf slope is as steep as it can be and remain monotonically + increasing or decreasing gain with frequency. The shelf slope, in dB/octave, + remains proportional to S for all other values for a fixed f0/Fs and dBgain. + Gain in decibels + + + + H(s) = A * (A*s^2 + (sqrt(A)/Q)*s + 1)/(s^2 + (sqrt(A)/Q)*s + A) + + + + + + + + + + Type to represent complex number + + + + + Real Part + + + + + Imaginary Part + + + + + Summary description for FastFourierTransform. + + + + + This computes an in-place complex-to-complex FFT + x and y are the real and imaginary arrays of 2^m points. + + + + + Applies a Hamming Window + + Index into frame + Frame size (e.g. 1024) + Multiplier for Hamming window + + + + Applies a Hann Window + + Index into frame + Frame size (e.g. 1024) + Multiplier for Hann window + + + + Applies a Blackman-Harris Window + + Index into frame + Frame size (e.g. 1024) + Multiplier for Blackmann-Harris window + + + + Summary description for ImpulseResponseConvolution. + + + + + A very simple mono convolution algorithm + + + This will be very slow + + + + + This is actually a downwards normalize for data that will clip + + + + + Represents an entry in a Cakewalk drum map + + + + + Describes this drum map entry + + + + + User customisable note name + + + + + Input MIDI note number + + + + + Output MIDI note number + + + + + Output port + + + + + Output MIDI Channel + + + + + Velocity adjustment + + + + + Velocity scaling - in percent + + + + + Represents a Cakewalk Drum Map file (.map) + + + + + Parses a Cakewalk Drum Map file + + Path of the .map file + + + + Describes this drum map + + + + + The drum mappings in this drum map + + + + + Channel Mode + + + + + Stereo + + + + + Joint Stereo + + + + + Dual Channel + + + + + Mono + + + + + MP3 Frame decompressor using the Windows Media MP3 Decoder DMO object + + + + + Interface for MP3 frame by frame decoder + + + + + Decompress a single MP3 frame + + Frame to decompress + Output buffer + Offset within output buffer + Bytes written to output buffer + + + + Tell the decoder that we have repositioned + + + + + PCM format that we are converting into + + + + + Initializes a new instance of the DMO MP3 Frame decompressor + + + + + + Decompress a single frame of MP3 + + + + + Alerts us that a reposition has occured so the MP3 decoder needs to reset its state + + + + + Dispose of this obejct and clean up resources + + + + + Converted PCM WaveFormat + + + + + An ID3v2 Tag + + + + + Reads an ID3v2 tag from a stream + + + + + Creates a new ID3v2 tag from a collection of key-value pairs. + + A collection of key-value pairs containing the tags to include in the ID3v2 tag. + A new ID3v2 tag + + + + Convert the frame size to a byte array. + + The frame body size. + + + + + Creates an ID3v2 frame for the given key-value pair. + + + + + + + + Gets the Id3v2 Header size. The size is encoded so that only 7 bits per byte are actually used. + + + + + + + Creates the Id3v2 tag header and returns is as a byte array. + + The Id3v2 frames that will be included in the file. This is used to calculate the ID3v2 tag size. + + + + + Creates the Id3v2 tag for the given key-value pairs and returns it in the a stream. + + + + + + + Raw data from this tag + + + + + Represents an MP3 Frame + + + + + Reads an MP3 frame from a stream + + input stream + A valid MP3 frame, or null if none found + + + Reads an MP3Frame from a stream + http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm has some good info + also see http://www.codeproject.com/KB/audio-video/mpegaudioinfo.aspx + + A valid MP3 frame, or null if none found + + + + Constructs an MP3 frame + + + + + checks if the four bytes represent a valid header, + if they are, will parse the values into Mp3Frame + + + + + Sample rate of this frame + + + + + Frame length in bytes + + + + + Bit Rate + + + + + Raw frame data (includes header bytes) + + + + + MPEG Version + + + + + MPEG Layer + + + + + Channel Mode + + + + + The number of samples in this frame + + + + + The channel extension bits + + + + + The bitrate index (directly from the header) + + + + + Whether the Copyright bit is set + + + + + Whether a CRC is present + + + + + Not part of the MP3 frame itself - indicates where in the stream we found this header + + + + + MP3 Frame Decompressor using ACM + + + + + Creates a new ACM frame decompressor + + The MP3 source format + + + + Decompresses a frame + + The MP3 frame + destination buffer + Offset within destination buffer + Bytes written into destination buffer + + + + Resets the MP3 Frame Decompressor after a reposition operation + + + + + Disposes of this MP3 frame decompressor + + + + + Finalizer ensuring that resources get released properly + + + + + Output format (PCM) + + + + + MPEG Layer flags + + + + + Reserved + + + + + Layer 3 + + + + + Layer 2 + + + + + Layer 1 + + + + + MPEG Version Flags + + + + + Version 2.5 + + + + + Reserved + + + + + Version 2 + + + + + Version 1 + + + + + Represents a Xing VBR header + + + + + Load Xing Header + + Frame + Xing Header + + + + Sees if a frame contains a Xing header + + + + + Number of frames + + + + + Number of bytes + + + + + VBR Scale property + + + + + The MP3 frame + + + + + Soundfont generator + + + + + + + + + + Gets the generator type + + + + + Generator amount as an unsigned short + + + + + Generator amount as a signed short + + + + + Low byte amount + + + + + High byte amount + + + + + Instrument + + + + + Sample Header + + + + + base class for structures that can read themselves + + + + + Generator types + + + + Start address offset + + + End address offset + + + Start loop address offset + + + End loop address offset + + + Start address coarse offset + + + Modulation LFO to pitch + + + Vibrato LFO to pitch + + + Modulation envelope to pitch + + + Initial filter cutoff frequency + + + Initial filter Q + + + Modulation LFO to filter Cutoff frequency + + + Modulation envelope to filter cutoff frequency + + + End address coarse offset + + + Modulation LFO to volume + + + Unused + + + Chorus effects send + + + Reverb effects send + + + Pan + + + Unused + + + Unused + + + Unused + + + Delay modulation LFO + + + Frequency modulation LFO + + + Delay vibrato LFO + + + Frequency vibrato LFO + + + Delay modulation envelope + + + Attack modulation envelope + + + Hold modulation envelope + + + Decay modulation envelope + + + Sustain modulation envelop + + + Release modulation envelope + + + Key number to modulation envelope hold + + + Key number to modulation envelope decay + + + Delay volume envelope + + + Attack volume envelope + + + Hold volume envelope + + + Decay volume envelope + + + Sustain volume envelope + + + Release volume envelope + + + Key number to volume envelope hold + + + Key number to volume envelope decay + + + Instrument + + + Reserved + + + Key range + + + Velocity range + + + Start loop address coarse offset + + + Key number + + + Velocity + + + Initial attenuation + + + Reserved + + + End loop address coarse offset + + + Coarse tune + + + Fine tune + + + Sample ID + + + Sample modes + + + Reserved + + + Scale tuning + + + Exclusive class + + + Overriding root key + + + Unused + + + Unused + + + + A soundfont info chunk + + + + + + + + + + SoundFont Version + + + + + WaveTable sound engine + + + + + Bank name + + + + + Data ROM + + + + + Creation Date + + + + + Author + + + + + Target Product + + + + + Copyright + + + + + Comments + + + + + Tools + + + + + ROM Version + + + + + SoundFont instrument + + + + + + + + + + instrument name + + + + + Zones + + + + + Instrument Builder + + + + + Transform Types + + + + + Linear + + + + + Modulator + + + + + + + + + + Source Modulation data type + + + + + Destination generator type + + + + + Amount + + + + + Source Modulation Amount Type + + + + + Source Transform Type + + + + + Controller Sources + + + + + No Controller + + + + + Note On Velocity + + + + + Note On Key Number + + + + + Poly Pressure + + + + + Channel Pressure + + + + + Pitch Wheel + + + + + Pitch Wheel Sensitivity + + + + + Source Types + + + + + Linear + + + + + Concave + + + + + Convex + + + + + Switch + + + + + Modulator Type + + + + + + + + + + + A SoundFont Preset + + + + + + + + + + Preset name + + + + + Patch Number + + + + + Bank number + + + + + Zones + + + + + Class to read the SoundFont file presets chunk + + + + + + + + + + The Presets contained in this chunk + + + + + The instruments contained in this chunk + + + + + The sample headers contained in this chunk + + + + + just reads a chunk ID at the current position + + chunk ID + + + + reads a chunk at the current position + + + + + creates a new riffchunk from current position checking that we're not + at the end of this chunk first + + the new chunk + + + + useful for chunks that just contain a string + + chunk as string + + + + A SoundFont Sample Header + + + + + The sample name + + + + + Start offset + + + + + End offset + + + + + Start loop point + + + + + End loop point + + + + + Sample Rate + + + + + Original pitch + + + + + Pitch correction + + + + + Sample Link + + + + + SoundFont Sample Link Type + + + + + + + + + + SoundFont sample modes + + + + + No loop + + + + + Loop Continuously + + + + + Reserved no loop + + + + + Loop and continue + + + + + Sample Link Type + + + + + Mono Sample + + + + + Right Sample + + + + + Left Sample + + + + + Linked Sample + + + + + ROM Mono Sample + + + + + ROM Right Sample + + + + + ROM Left Sample + + + + + ROM Linked Sample + + + + + SoundFont Version Structure + + + + + Major Version + + + + + Minor Version + + + + + Builds a SoundFont version + + + + + Reads a SoundFont Version structure + + + + + Writes a SoundFont Version structure + + + + + Gets the length of this structure + + + + + Represents a SoundFont + + + + + Loads a SoundFont from a file + + Filename of the SoundFont + + + + Loads a SoundFont from a stream + + stream + + + + + + + + + The File Info Chunk + + + + + The Presets + + + + + The Instruments + + + + + The Sample Headers + + + + + The Sample Data + + + + + A SoundFont zone + + + + + + + + + + Modulators for this Zone + + + + + Generators for this Zone + + + + + Summary description for Fader. + + + + + Required designer variable. + + + + + Creates a new Fader control + + + + + Clean up any resources being used. + + + + + + + + + + + + + + + + + + + + + + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + Minimum value of this fader + + + + + Maximum value of this fader + + + + + Current value of this fader + + + + + Fader orientation + + + + + Pan slider control + + + + + Required designer variable. + + + + + Creates a new PanSlider control + + + + + Clean up any resources being used. + + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + + + + + + + + + + + + + + + + + True when pan value changed + + + + + The current Pan setting + + + + + Control that represents a potentiometer + TODO list: + Optional Log scale + Optional reverse scale + Keyboard control + Optional bitmap mode + Optional complete draw mode + Tooltip support + + + + + Creates a new pot control + + + + + Draws the control + + + + + Handles the mouse down event to allow changing value by dragging + + + + + Handles the mouse up event to allow changing value by dragging + + + + + Handles the mouse down event to allow changing value by dragging + + + + + Required designer variable. + + + + + Clean up any resources being used. + + true if managed resources should be disposed; otherwise, false. + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + Value changed event + + + + + Minimum Value of the Pot + + + + + Maximum Value of the Pot + + + + + The current value of the pot + + + + + Implements a rudimentary volume meter + + + + + Basic volume meter + + + + + On Fore Color Changed + + + + + Paints the volume meter + + + + + Required designer variable. + + + + + Clean up any resources being used. + + true if managed resources should be disposed; otherwise, false. + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + Current Value + + + + + Minimum decibels + + + + + Maximum decibels + + + + + Meter orientation + + + + + VolumeSlider control + + + + + Required designer variable. + + + + + Creates a new VolumeSlider control + + + + + Clean up any resources being used. + + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + + + + + + + + + + + + + + + + Volume changed event + + + + + The volume for this control + + + + + Windows Forms control for painting audio waveforms + + + + + Constructs a new instance of the WaveFormPainter class + + + + + On Resize + + + + + On ForeColor Changed + + + + + + Add Max Value + + + + + + On Paint + + + + + Required designer variable. + + + + + Clean up any resources being used. + + true if managed resources should be disposed; otherwise, false. + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + Control for viewing waveforms + + + + + Required designer variable. + + + + + Creates a new WaveViewer control + + + + + Clean up any resources being used. + + + + + + + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + sets the associated wavestream + + + + + The zoom level, in samples per pixel + + + + + Start position (currently in bytes) + + + + + Represents a MIDI Channel AfterTouch Event. + + + + + Represents an individual MIDI event + + + + The MIDI command code + + + + Creates a MidiEvent from a raw message received using + the MME MIDI In APIs + + The short MIDI message + A new MIDI Event + + + + Constructs a MidiEvent from a BinaryStream + + The binary stream of MIDI data + The previous MIDI event (pass null for first event) + A new MidiEvent + + + + Converts this MIDI event to a short message (32 bit integer) that + can be sent by the Windows MIDI out short message APIs + Cannot be implemented for all MIDI messages + + A short message + + + + Default constructor + + + + + Creates a MIDI event with specified parameters + + Absolute time of this event + MIDI channel number + MIDI command code + + + + Whether this is a note off event + + + + + Whether this is a note on event + + + + + Determines if this is an end track event + + + + + Displays a summary of the MIDI event + + A string containing a brief description of this MIDI event + + + + Utility function that can read a variable length integer from a binary stream + + The binary stream + The integer read + + + + Writes a variable length integer to a binary stream + + Binary stream + The value to write + + + + Exports this MIDI event's data + Overriden in derived classes, but they should call this version + + Absolute time used to calculate delta. + Is updated ready for the next delta calculation + Stream to write to + + + + The MIDI Channel Number for this event (1-16) + + + + + The Delta time for this event + + + + + The absolute time for this event + + + + + The command code for this event + + + + + Creates a new ChannelAfterTouchEvent from raw MIDI data + + A binary reader + + + + Creates a new Channel After-Touch Event + + Absolute time + Channel + After-touch pressure + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The aftertouch pressure value + + + + + Represents a MIDI control change event + + + + + Reads a control change event from a MIDI stream + + Binary reader on the MIDI stream + + + + Creates a control change event + + Time + MIDI Channel Number + The MIDI Controller + Controller value + + + + Describes this control change event + + A string describing this event + + + + + + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The controller number + + + + + The controller value + + + + + Represents a MIDI key signature event event + + + + + Represents a MIDI meta event + + + + + Empty constructor + + + + + Custom constructor for use by derived types, who will manage the data themselves + + Meta event type + Meta data length + Absolute time + + + + Reads a meta-event from a stream + + A binary reader based on the stream of MIDI data + A new MetaEvent object + + + + Describes this Meta event + + String describing the metaevent + + + + + + + + + Gets the type of this meta event + + + + + Reads a new track sequence number event from a MIDI stream + + The MIDI stream + the data length + + + + Creates a new Key signature event with the specified data + + + + + Describes this event + + String describing the event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Number of sharps or flats + + + + + Major or Minor key + + + + + MIDI MetaEvent Type + + + + Track sequence number + + + Text event + + + Copyright + + + Sequence track name + + + Track instrument name + + + Lyric + + + Marker + + + Cue point + + + Program (patch) name + + + Device (port) name + + + MIDI Channel (not official?) + + + MIDI Port (not official?) + + + End track + + + Set tempo + + + SMPTE offset + + + Time signature + + + Key signature + + + Sequencer specific + + + + MIDI command codes + + + + Note Off + + + Note On + + + Key After-touch + + + Control change + + + Patch change + + + Channel after-touch + + + Pitch wheel change + + + Sysex message + + + Eox (comes at end of a sysex message) + + + Timing clock (used when synchronization is required) + + + Start sequence + + + Continue sequence + + + Stop sequence + + + Auto-Sensing + + + Meta-event + + + + MidiController enumeration + http://www.midi.org/techspecs/midimessages.php#3 + + + + Bank Select (MSB) + + + Modulation (MSB) + + + Breath Controller + + + Foot controller (MSB) + + + Main volume + + + Pan + + + Expression + + + Bank Select LSB + + + Sustain + + + Portamento On/Off + + + Sostenuto On/Off + + + Soft Pedal On/Off + + + Legato Footswitch + + + Reset all controllers + + + All notes off + + + + A helper class to manage collection of MIDI events + It has the ability to organise them in tracks + + + + + Creates a new Midi Event collection + + Initial file type + Delta Ticks Per Quarter Note + + + + Gets events on a specified track + + Track number + The list of events + + + + Adds a new track + + The new track event list + + + + Adds a new track + + Initial events to add to the new track + The new track event list + + + + Removes a track + + Track number to remove + + + + Clears all events + + + + + Adds an event to the appropriate track depending on file type + + The event to be added + The original (or desired) track number + When adding events in type 0 mode, the originalTrack parameter + is ignored. If in type 1 mode, it will use the original track number to + store the new events. If the original track was 0 and this is a channel based + event, it will create new tracks if necessary and put it on the track corresponding + to its channel number + + + + Sorts, removes empty tracks and adds end track markers + + + + + Gets an enumerator for the lists of track events + + + + + Gets an enumerator for the lists of track events + + + + + The number of tracks + + + + + The absolute time that should be considered as time zero + Not directly used here, but useful for timeshifting applications + + + + + The number of ticks per quarter note + + + + + Gets events on a specific track + + Track number + The list of events + + + + The MIDI file type + + + + + Utility class for comparing MidiEvent objects + + + + + Compares two MidiEvents + Sorts by time, with EndTrack always sorted to the end + + + + + Class able to read a MIDI file + + + + + Opens a MIDI file for reading + + Name of MIDI file + + + + Opens a MIDI file for reading + + Name of MIDI file + If true will error on non-paired note events + + + + Describes the MIDI file + + A string describing the MIDI file and its events + + + + Exports a MIDI file + + Filename to export to + Events to export + + + + MIDI File format + + + + + The collection of events in this MIDI file + + + + + Number of tracks in this MIDI file + + + + + Delta Ticks Per Quarter Note + + + + + Represents a MIDI in device + + + + + Opens a specified MIDI in device + + The device number + + + + Closes this MIDI in device + + + + + Closes this MIDI in device + + + + + Start the MIDI in device + + + + + Stop the MIDI in device + + + + + Reset the MIDI in device + + + + + Gets the MIDI in device info + + + + + Closes the MIDI out device + + True if called from Dispose + + + + Cleanup + + + + + Called when a MIDI message is received + + + + + An invalid MIDI message + + + + + Gets the number of MIDI input devices available in the system + + + + + MIDI In Device Capabilities + + + + + wMid + + + + + wPid + + + + + vDriverVersion + + + + + Product Name + + + + + Support - Reserved + + + + + Gets the manufacturer of this device + + + + + Gets the product identifier (manufacturer specific) + + + + + Gets the product name + + + + + MIM_OPEN + + + + + MIM_CLOSE + + + + + MIM_DATA + + + + + MIM_LONGDATA + + + + + MIM_ERROR + + + + + MIM_LONGERROR + + + + + MIM_MOREDATA + + + + + MOM_OPEN + + + + + MOM_CLOSE + + + + + MOM_DONE + + + + + Represents a MIDI message + + + + + Creates a new MIDI message + + Status + Data parameter 1 + Data parameter 2 + + + + Creates a new MIDI message from a raw message + + A packed MIDI message from an MMIO function + + + + Creates a Note On message + + Note number + Volume + MIDI channel + A new MidiMessage object + + + + Creates a Note Off message + + Note number + Volume + MIDI channel (1-16) + A new MidiMessage object + + + + Creates a patch change message + + The patch number + The MIDI channel number (1-16) + A new MidiMessageObject + + + + Creates a Control Change message + + The controller number to change + The value to set the controller to + The MIDI channel number (1-16) + A new MidiMessageObject + + + + Returns the raw MIDI message data + + + + + Represents a MIDI out device + + + + + Gets the MIDI Out device info + + + + + Opens a specified MIDI out device + + The device number + + + + Closes this MIDI out device + + + + + Closes this MIDI out device + + + + + Resets the MIDI out device + + + + + Sends a MIDI out message + + Message + Parameter 1 + Parameter 2 + + + + Sends a MIDI message to the MIDI out device + + The message to send + + + + Closes the MIDI out device + + True if called from Dispose + + + + Send a long message, for example sysex. + + The bytes to send. + + + + Cleanup + + + + + Gets the number of MIDI devices available in the system + + + + + Gets or sets the volume for this MIDI out device + + + + + class representing the capabilities of a MIDI out device + MIDIOUTCAPS: http://msdn.microsoft.com/en-us/library/dd798467%28VS.85%29.aspx + + + + + Queries whether a particular channel is supported + + Channel number to test + True if the channel is supported + + + + Gets the manufacturer of this device + + + + + Gets the product identifier (manufacturer specific) + + + + + Gets the product name + + + + + Returns the number of supported voices + + + + + Gets the polyphony of the device + + + + + Returns true if the device supports all channels + + + + + Returns true if the device supports patch caching + + + + + Returns true if the device supports separate left and right volume + + + + + Returns true if the device supports MIDI stream out + + + + + Returns true if the device supports volume control + + + + + Returns the type of technology used by this MIDI out device + + + + + MIDICAPS_VOLUME + + + + + separate left-right volume control + MIDICAPS_LRVOLUME + + + + + MIDICAPS_CACHE + + + + + MIDICAPS_STREAM + driver supports midiStreamOut directly + + + + + Represents the different types of technology used by a MIDI out device + + from mmsystem.h + + + The device is a MIDI port + + + The device is a MIDI synth + + + The device is a square wave synth + + + The device is an FM synth + + + The device is a MIDI mapper + + + The device is a WaveTable synth + + + The device is a software synth + + + + Represents a note MIDI event + + + + + Reads a NoteEvent from a stream of MIDI data + + Binary Reader for the stream + + + + Creates a MIDI Note Event with specified parameters + + Absolute time of this event + MIDI channel number + MIDI command code + MIDI Note Number + MIDI Note Velocity + + + + + + + + + Describes the Note Event + + Note event as a string + + + + + + + + + The MIDI note number + + + + + The note velocity + + + + + The note name + + + + + Represents a MIDI note on event + + + + + Reads a new Note On event from a stream of MIDI data + + Binary reader on the MIDI data stream + + + + Creates a NoteOn event with specified parameters + + Absolute time of this event + MIDI channel number + MIDI note number + MIDI note velocity + MIDI note duration + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The associated Note off event + + + + + Get or set the Note Number, updating the off event at the same time + + + + + Get or set the channel, updating the off event at the same time + + + + + The duration of this note + + + There must be a note off event + + + + + Represents a MIDI patch change event + + + + + Gets the default MIDI instrument names + + + + + Reads a new patch change event from a MIDI stream + + Binary reader for the MIDI stream + + + + Creates a new patch change event + + Time of the event + Channel number + Patch number + + + + Describes this patch change event + + String describing the patch change event + + + + Gets as a short message for sending with the midiOutShortMsg API + + short message + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The Patch Number + + + + + Represents a MIDI pitch wheel change event + + + + + Reads a pitch wheel change event from a MIDI stream + + The MIDI stream to read from + + + + Creates a new pitch wheel change event + + Absolute event time + Channel + Pitch wheel value + + + + Describes this pitch wheel change event + + String describing this pitch wheel change event + + + + Gets a short message + + Integer to sent as short message + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Pitch Wheel Value 0 is minimum, 0x2000 (8192) is default, 0x4000 (16384) is maximum + + + + + Represents a Sequencer Specific event + + + + + Reads a new sequencer specific event from a MIDI stream + + The MIDI stream + The data length + + + + Creates a new Sequencer Specific event + + The sequencer specific data + Absolute time of this event + + + + Describes this MIDI text event + + A string describing this event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The contents of this sequencer specific + + + + + Reads a new time signature event from a MIDI stream + + The MIDI stream + The data length + + + + Describes this time signature event + + A string describing this event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Hours + + + + + Minutes + + + + + Seconds + + + + + Frames + + + + + SubFrames + + + + + Represents a MIDI sysex message + + + + + Reads a sysex message from a MIDI stream + + Stream of MIDI data + a new sysex message + + + + Describes this sysex message + + A string describing the sysex message + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Represents a MIDI tempo event + + + + + Reads a new tempo event from a MIDI stream + + The MIDI stream + the data length + + + + Creates a new tempo event with specified settings + + Microseconds per quarter note + Absolute time + + + + Describes this tempo event + + String describing the tempo event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Microseconds per quarter note + + + + + Tempo + + + + + Represents a MIDI text event + + + + + Reads a new text event from a MIDI stream + + The MIDI stream + The data length + + + + Creates a new TextEvent + + The text in this type + MetaEvent type (must be one that is + associated with text data) + Absolute time of this event + + + + Describes this MIDI text event + + A string describing this event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + The contents of this text event + + + + + Represents a MIDI time signature event + + + + + Reads a new time signature event from a MIDI stream + + The MIDI stream + The data length + + + + Creates a new TimeSignatureEvent + + Time at which to create this event + Numerator + Denominator + Ticks in Metronome Click + No of 32nd Notes in Quarter Click + + + + Creates a new time signature event with the specified parameters + + + + + Describes this time signature event + + A string describing this event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Numerator (number of beats in a bar) + + + + + Denominator (Beat unit), + 1 means 2, 2 means 4 (crochet), 3 means 8 (quaver), 4 means 16 and 5 means 32 + + + + + Ticks in a metronome click + + + + + Number of 32nd notes in a quarter note + + + + + The time signature + + + + + Represents a MIDI track sequence number event event + + + + + Reads a new track sequence number event from a MIDI stream + + The MIDI stream + the data length + + + + Describes this event + + String describing the event + + + + Calls base class export first, then exports the data + specific to this event + MidiEvent.Export + + + + + Boolean mixer control + + + + + Represents a mixer control + + + + + Mixer Handle + + + + + Number of Channels + + + + + Mixer Handle Type + + + + + Gets all the mixer controls + + Mixer Handle + Mixer Line + Mixer Handle Type + + + + + Gets a specified Mixer Control + + Mixer Handle + Line ID + Control ID + Number of Channels + Flags to use (indicates the meaning of mixerHandle) + + + + + Gets the control details + + + + + Gets the control details + + + + + + Returns true if this is a boolean control + + Control type + + + + Determines whether a specified mixer control type is a list text control + + + + + String representation for debug purposes + + + + + Mixer control name + + + + + Mixer control type + + + + + Is this a boolean control + + + + + True if this is a list text control + + + + + True if this is a signed control + + + + + True if this is an unsigned control + + + + + True if this is a custom control + + + + + Gets the details for this control + + memory pointer + + + + The current value of the control + + + + + Custom Mixer control + + + + + Get the data for this custom control + + pointer to memory to receive data + + + + List text mixer control + + + + + Get the details for this control + + Memory location to read to + + + Represents a Windows mixer device + + + Connects to the specified mixer + The index of the mixer to use. + This should be between zero and NumberOfDevices - 1 + + + Retrieve the specified MixerDestination object + The ID of the destination to use. + Should be between 0 and DestinationCount - 1 + + + The number of mixer devices available + + + The number of destinations this mixer supports + + + The name of this mixer device + + + The manufacturer code for this mixer device + + + The product identifier code for this mixer device + + + + A way to enumerate the destinations + + + + + A way to enumerate all available devices + + + + + Mixer control types + + + + Custom + + + Boolean meter + + + Signed meter + + + Peak meter + + + Unsigned meter + + + Boolean + + + On Off + + + Mute + + + Mono + + + Loudness + + + Stereo Enhance + + + Button + + + Decibels + + + Signed + + + Unsigned + + + Percent + + + Slider + + + Pan + + + Q-sound pan + + + Fader + + + Volume + + + Bass + + + Treble + + + Equaliser + + + Single Select + + + Mux + + + Multiple select + + + Mixer + + + Micro time + + + Milli time + + + + Represents a mixer line (source or destination) + + + + + Creates a new mixer destination + + Mixer Handle + Destination Index + Mixer Handle Type + + + + Creates a new Mixer Source For a Specified Source + + Mixer Handle + Destination Index + Source Index + Flag indicating the meaning of mixerHandle + + + + Creates a new Mixer Source + + Wave In Device + + + + Gets the specified source + + + + + Describes this Mixer Line (for diagnostic purposes) + + + + + Mixer Line Name + + + + + Mixer Line short name + + + + + The line ID + + + + + Component Type + + + + + Mixer destination type description + + + + + Number of channels + + + + + Number of sources + + + + + Number of controls + + + + + Is this destination active + + + + + Is this destination disconnected + + + + + Is this destination a source + + + + + Enumerator for the controls on this Mixer Limne + + + + + Enumerator for the sources on this Mixer Line + + + + + The name of the target output device + + + + + Mixer Interop Flags + + + + + MIXER_OBJECTF_HANDLE = 0x80000000; + + + + + MIXER_OBJECTF_MIXER = 0x00000000; + + + + + MIXER_OBJECTF_HMIXER + + + + + MIXER_OBJECTF_WAVEOUT + + + + + MIXER_OBJECTF_HWAVEOUT + + + + + MIXER_OBJECTF_WAVEIN + + + + + MIXER_OBJECTF_HWAVEIN + + + + + MIXER_OBJECTF_MIDIOUT + + + + + MIXER_OBJECTF_HMIDIOUT + + + + + MIXER_OBJECTF_MIDIIN + + + + + MIXER_OBJECTF_HMIDIIN + + + + + MIXER_OBJECTF_AUX + + + + + MIXER_GETCONTROLDETAILSF_VALUE = 0x00000000; + MIXER_SETCONTROLDETAILSF_VALUE = 0x00000000; + + + + + MIXER_GETCONTROLDETAILSF_LISTTEXT = 0x00000001; + MIXER_SETCONTROLDETAILSF_LISTTEXT = 0x00000001; + + + + + MIXER_GETCONTROLDETAILSF_QUERYMASK = 0x0000000F; + MIXER_SETCONTROLDETAILSF_QUERYMASK = 0x0000000F; + MIXER_GETLINECONTROLSF_QUERYMASK = 0x0000000F; + + + + + MIXER_GETLINECONTROLSF_ALL = 0x00000000; + + + + + MIXER_GETLINECONTROLSF_ONEBYID = 0x00000001; + + + + + MIXER_GETLINECONTROLSF_ONEBYTYPE = 0x00000002; + + + + + MIXER_GETLINEINFOF_DESTINATION = 0x00000000; + + + + + MIXER_GETLINEINFOF_SOURCE = 0x00000001; + + + + + MIXER_GETLINEINFOF_LINEID = 0x00000002; + + + + + MIXER_GETLINEINFOF_COMPONENTTYPE = 0x00000003; + + + + + MIXER_GETLINEINFOF_TARGETTYPE = 0x00000004; + + + + + MIXER_GETLINEINFOF_QUERYMASK = 0x0000000F; + + + + + Mixer Line Flags + + + + + Audio line is active. An active line indicates that a signal is probably passing + through the line. + + + + + Audio line is disconnected. A disconnected line's associated controls can still be + modified, but the changes have no effect until the line is connected. + + + + + Audio line is an audio source line associated with a single audio destination line. + If this flag is not set, this line is an audio destination line associated with zero + or more audio source lines. + + + + + BOUNDS structure + + + + + dwMinimum / lMinimum / reserved 0 + + + + + dwMaximum / lMaximum / reserved 1 + + + + + reserved 2 + + + + + reserved 3 + + + + + reserved 4 + + + + + reserved 5 + + + + + METRICS structure + + + + + cSteps / reserved[0] + + + + + cbCustomData / reserved[1], number of bytes for control details + + + + + reserved 2 + + + + + reserved 3 + + + + + reserved 4 + + + + + reserved 5 + + + + + MIXERCONTROL struct + http://msdn.microsoft.com/en-us/library/dd757293%28VS.85%29.aspx + + + + + Mixer Line Component type enumeration + + + + + Audio line is a destination that cannot be defined by one of the standard component types. A mixer device is required to use this component type for line component types that have not been defined by Microsoft Corporation. + MIXERLINE_COMPONENTTYPE_DST_UNDEFINED + + + + + Audio line is a digital destination (for example, digital input to a DAT or CD audio device). + MIXERLINE_COMPONENTTYPE_DST_DIGITAL + + + + + Audio line is a line level destination (for example, line level input from a CD audio device) that will be the final recording source for the analog-to-digital converter (ADC). Because most audio cards for personal computers provide some sort of gain for the recording audio source line, the mixer device will use the MIXERLINE_COMPONENTTYPE_DST_WAVEIN type. + MIXERLINE_COMPONENTTYPE_DST_LINE + + + + + Audio line is a destination used for a monitor. + MIXERLINE_COMPONENTTYPE_DST_MONITOR + + + + + Audio line is an adjustable (gain and/or attenuation) destination intended to drive speakers. This is the typical component type for the audio output of audio cards for personal computers. + MIXERLINE_COMPONENTTYPE_DST_SPEAKERS + + + + + Audio line is an adjustable (gain and/or attenuation) destination intended to drive headphones. Most audio cards use the same audio destination line for speakers and headphones, in which case the mixer device simply uses the MIXERLINE_COMPONENTTYPE_DST_SPEAKERS type. + MIXERLINE_COMPONENTTYPE_DST_HEADPHONES + + + + + Audio line is a destination that will be routed to a telephone line. + MIXERLINE_COMPONENTTYPE_DST_TELEPHONE + + + + + Audio line is a destination that will be the final recording source for the waveform-audio input (ADC). This line typically provides some sort of gain or attenuation. This is the typical component type for the recording line of most audio cards for personal computers. + MIXERLINE_COMPONENTTYPE_DST_WAVEIN + + + + + Audio line is a destination that will be the final recording source for voice input. This component type is exactly like MIXERLINE_COMPONENTTYPE_DST_WAVEIN but is intended specifically for settings used during voice recording/recognition. Support for this line is optional for a mixer device. Many mixer devices provide only MIXERLINE_COMPONENTTYPE_DST_WAVEIN. + MIXERLINE_COMPONENTTYPE_DST_VOICEIN + + + + + Audio line is a source that cannot be defined by one of the standard component types. A mixer device is required to use this component type for line component types that have not been defined by Microsoft Corporation. + MIXERLINE_COMPONENTTYPE_SRC_UNDEFINED + + + + + Audio line is a digital source (for example, digital output from a DAT or audio CD). + MIXERLINE_COMPONENTTYPE_SRC_DIGITAL + + + + + Audio line is a line-level source (for example, line-level input from an external stereo) that can be used as an optional recording source. Because most audio cards for personal computers provide some sort of gain for the recording source line, the mixer device will use the MIXERLINE_COMPONENTTYPE_SRC_AUXILIARY type. + MIXERLINE_COMPONENTTYPE_SRC_LINE + + + + + Audio line is a microphone recording source. Most audio cards for personal computers provide at least two types of recording sources: an auxiliary audio line and microphone input. A microphone audio line typically provides some sort of gain. Audio cards that use a single input for use with a microphone or auxiliary audio line should use the MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE component type. + MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE + + + + + Audio line is a source originating from the output of an internal synthesizer. Most audio cards for personal computers provide some sort of MIDI synthesizer (for example, an Adlib®-compatible or OPL/3 FM synthesizer). + MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER + + + + + Audio line is a source originating from the output of an internal audio CD. This component type is provided for audio cards that provide an audio source line intended to be connected to an audio CD (or CD-ROM playing an audio CD). + MIXERLINE_COMPONENTTYPE_SRC_COMPACTDISC + + + + + Audio line is a source originating from an incoming telephone line. + MIXERLINE_COMPONENTTYPE_SRC_TELEPHONE + + + + + Audio line is a source originating from personal computer speaker. Several audio cards for personal computers provide the ability to mix what would typically be played on the internal speaker with the output of an audio card. Some audio cards support the ability to use this output as a recording source. + MIXERLINE_COMPONENTTYPE_SRC_PCSPEAKER + + + + + Audio line is a source originating from the waveform-audio output digital-to-analog converter (DAC). Most audio cards for personal computers provide this component type as a source to the MIXERLINE_COMPONENTTYPE_DST_SPEAKERS destination. Some cards also allow this source to be routed to the MIXERLINE_COMPONENTTYPE_DST_WAVEIN destination. + MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT + + + + + Audio line is a source originating from the auxiliary audio line. This line type is intended as a source with gain or attenuation that can be routed to the MIXERLINE_COMPONENTTYPE_DST_SPEAKERS destination and/or recorded from the MIXERLINE_COMPONENTTYPE_DST_WAVEIN destination. + MIXERLINE_COMPONENTTYPE_SRC_AUXILIARY + + + + + Audio line is an analog source (for example, analog output from a video-cassette tape). + MIXERLINE_COMPONENTTYPE_SRC_ANALOG + + + + + Represents a signed mixer control + + + + + Gets details for this contrl + + + + + String Representation for debugging purposes + + + + + + The value of the control + + + + + Minimum value for this control + + + + + Maximum value for this control + + + + + Value of the control represented as a percentage + + + + + Represents an unsigned mixer control + + + + + Gets the details for this control + + + + + String Representation for debugging purposes + + + + + The control value + + + + + The control's minimum value + + + + + The control's maximum value + + + + + Value of the control represented as a percentage + + + + + Helper methods for working with audio buffers + + + + + Ensures the buffer is big enough + + + + + + + + Ensures the buffer is big enough + + + + + + + + An encoding for use with file types that have one byte per character + + + + + The one and only instance of this class + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A very basic circular buffer implementation + + + + + Create a new circular buffer + + Max buffer size in bytes + + + + Write data to the buffer + + Data to write + Offset into data + Number of bytes to write + number of bytes written + + + + Read from the buffer + + Buffer to read into + Offset into read buffer + Bytes to read + Number of bytes actually read + + + + Resets the buffer + + + + + Advances the buffer, discarding bytes + + Bytes to advance + + + + Maximum length of this circular buffer + + + + + Number of bytes currently stored in the circular buffer + + + + + A util class for conversions + + + + + linear to dB conversion + + linear value + decibel value + + + + dB to linear conversion + + decibel value + linear value + + + + HResult + + + + + S_OK + + + + + S_FALSE + + + + + E_INVALIDARG (from winerror.h) + + + + + MAKE_HRESULT macro + + + + + Helper to deal with the fact that in Win Store apps, + the HResult property name has changed + + COM Exception + The HResult + + + + Pass-through stream that ignores Dispose + Useful for dealing with MemoryStreams that you want to re-use + + + + + Creates a new IgnoreDisposeStream + + The source stream + + + + Flushes the underlying stream + + + + + Reads from the underlying stream + + + + + Seeks on the underlying stream + + + + + Sets the length of the underlying stream + + + + + Writes to the underlying stream + + + + + Dispose - by default (IgnoreDispose = true) will do nothing, + leaving the underlying stream undisposed + + + + + The source stream all other methods fall through to + + + + + If true the Dispose will be ignored, if false, will pass through to the SourceStream + Set to true by default + + + + + Can Read + + + + + Can Seek + + + + + Can write to the underlying stream + + + + + Gets the length of the underlying stream + + + + + Gets or sets the position of the underlying stream + + + + + In-place and stable implementation of MergeSort + + + + + MergeSort a list of comparable items + + + + + MergeSort a list + + + + + A thread-safe Progress Log Control + + + + + Creates a new progress log control + + + + + Log a message + + + + + Clear the log + + + + + Required designer variable. + + + + + Clean up any resources being used. + + true if managed resources should be disposed; otherwise, false. + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + The contents of the log as text + + + + + Audio Endpoint Volume + + + + + Volume Step Up + + + + + Volume Step Down + + + + + Creates a new Audio endpoint volume + + IAudioEndpointVolume COM interface + + + + Dispose + + + + + Finalizer + + + + + On Volume Notification + + + + + Volume Range + + + + + Hardware Support + + + + + Step Information + + + + + Channels + + + + + Master Volume Level + + + + + Master Volume Level Scalar + + + + + Mute + + + + + Audio Meter Information + + + + + Peak Values + + + + + Hardware Support + + + + + Master Peak Value + + + + + Device State + + + + + DEVICE_STATE_ACTIVE + + + + + DEVICE_STATE_DISABLED + + + + + DEVICE_STATE_NOTPRESENT + + + + + DEVICE_STATE_UNPLUGGED + + + + + DEVICE_STATEMASK_ALL + + + + + Endpoint Hardware Support + + + + + Volume + + + + + Mute + + + + + Meter + + + + + is defined in WTypes.h + + + + + The EDataFlow enumeration defines constants that indicate the direction + in which audio data flows between an audio endpoint device and an application + + + + + Audio rendering stream. + Audio data flows from the application to the audio endpoint device, which renders the stream. + + + + + Audio capture stream. Audio data flows from the audio endpoint device that captures the stream, + to the application + + + + + Audio rendering or capture stream. Audio data can flow either from the application to the audio + endpoint device, or from the audio endpoint device to the application. + + + + + Windows CoreAudio IAudioClient interface + Defined in AudioClient.h + + + + + The GetBufferSize method retrieves the size (maximum capacity) of the endpoint buffer. + + + + + The GetService method accesses additional services from the audio client object. + + The interface ID for the requested service. + Pointer to a pointer variable into which the method writes the address of an instance of the requested interface. + + + + defined in MMDeviceAPI.h + + + + + IMMNotificationClient + + + + + Device State Changed + + + + + Device Added + + + + + Device Removed + + + + + Default Device Changed + + + + + Property Value Changed + + + + + + + is defined in propsys.h + + + + + implements IMMDeviceEnumerator + + + + + MMDevice STGM enumeration + + + + + PROPERTYKEY is defined in wtypes.h + + + + + Format ID + + + + + Property ID + + + + + + + + + + + from Propidl.h. + http://msdn.microsoft.com/en-us/library/aa380072(VS.85).aspx + contains a union so we have to do an explicit layout + + + + + Creates a new PropVariant containing a long value + + + + + Helper method to gets blob data + + + + + Interprets a blob as an array of structs + + + + + allows freeing up memory, might turn this into a Dispose method? + + + + + Gets the type of data in this PropVariant + + + + + Property value + + + + + The ERole enumeration defines constants that indicate the role + that the system has assigned to an audio endpoint device + + + + + Games, system notification sounds, and voice commands. + + + + + Music, movies, narration, and live music recording + + + + + Voice communications (talking to another person). + + + + + MM Device + + + + + To string + + + + + Audio Client + + + + + Audio Meter Information + + + + + Audio Endpoint Volume + + + + + AudioSessionManager instance + + + + + Properties + + + + + Friendly name for the endpoint + + + + + Friendly name of device + + + + + Icon path of device + + + + + Device ID + + + + + Data Flow + + + + + Device State + + + + + MM Device Enumerator + + + + + Creates a new MM Device Enumerator + + + + + Enumerate Audio Endpoints + + Desired DataFlow + State Mask + Device Collection + + + + Get Default Endpoint + + Data Flow + Role + Device + + + + Check to see if a default audio end point exists without needing an exception. + + Data Flow + Role + True if one exists, and false if one does not exist. + + + + Get device by ID + + Device ID + Device + + + + Registers a call back for Device Events + + Object implementing IMMNotificationClient type casted as IMMNotificationClient interface + + + + + Unregisters a call back for Device Events + + Object implementing IMMNotificationClient type casted as IMMNotificationClient interface + + + + + Property Store class, only supports reading properties at the moment. + + + + + Contains property guid + + Looks for a specific key + True if found + + + + Gets property key at sepecified index + + Index + Property key + + + + Gets property value at specified index + + Index + Property value + + + + Creates a new property store + + IPropertyStore COM interface + + + + Property Count + + + + + Gets property by index + + Property index + The property + + + + Indexer by guid + + Property Key + Property or null if not found + + + + Property Store Property + + + + + Property Key + + + + + Property Value + + + + + Main ASIODriver Class. To use this class, you need to query first the GetASIODriverNames() and + then use the GetASIODriverByName to instantiate the correct ASIODriver. + This is the first ASIODriver binding fully implemented in C#! + + Contributor: Alexandre Mutel - email: alexandre_mutel at yahoo.fr + + + + + Gets the ASIO driver names installed. + + a list of driver names. Use this name to GetASIODriverByName + + + + Instantiate a ASIODriver given its name. + + The name of the driver + an ASIODriver instance + + + + Instantiate the ASIO driver by GUID. + + The GUID. + an ASIODriver instance + + + + Inits the ASIODriver.. + + The sys handle. + + + + + Gets the name of the driver. + + + + + + Gets the driver version. + + + + + + Gets the error message. + + + + + + Starts this instance. + + + + + Stops this instance. + + + + + Gets the channels. + + The num input channels. + The num output channels. + + + + Gets the latencies (n.b. does not throw an exception) + + The input latency. + The output latency. + + + + Gets the size of the buffer. + + Size of the min. + Size of the max. + Size of the preferred. + The granularity. + + + + Determines whether this instance can use the specified sample rate. + + The sample rate. + + true if this instance [can sample rate] the specified sample rate; otherwise, false. + + + + + Gets the sample rate. + + + + + + Sets the sample rate. + + The sample rate. + + + + Gets the clock sources. + + The clocks. + The num sources. + + + + Sets the clock source. + + The reference. + + + + Gets the sample position. + + The sample pos. + The time stamp. + + + + Gets the channel info. + + The channel number. + if set to true [true for input info]. + + + + + Creates the buffers. + + The buffer infos. + The num channels. + Size of the buffer. + The callbacks. + + + + Disposes the buffers. + + + + + Controls the panel. + + + + + Futures the specified selector. + + The selector. + The opt. + + + + Notifies OutputReady to the ASIODriver. + + + + + + Releases this instance. + + + + + Handles the exception. Throws an exception based on the error. + + The error to check. + Method name + + + + Inits the vTable method from GUID. This is a tricky part of this class. + + The ASIO GUID. + + + + Internal VTable structure to store all the delegates to the C++ COM method. + + + + + Callback used by the ASIODriverExt to get wave data + + + + + ASIODriverExt is a simplified version of the ASIODriver. It provides an easier + way to access the capabilities of the Driver and implement the callbacks necessary + for feeding the driver. + Implementation inspired from Rob Philpot's with a managed C++ ASIO wrapper BlueWave.Interop.Asio + http://www.codeproject.com/KB/mcpp/Asio.Net.aspx + + Contributor: Alexandre Mutel - email: alexandre_mutel at yahoo.fr + + + + + Initializes a new instance of the class based on an already + instantiated ASIODriver instance. + + A ASIODriver already instantiated. + + + + Allows adjustment of which is the first output channel we write to + + Output Channel offset + Input Channel offset + + + + Starts playing the buffers. + + + + + Stops playing the buffers. + + + + + Shows the control panel. + + + + + Releases this instance. + + + + + Determines whether the specified sample rate is supported. + + The sample rate. + + true if [is sample rate supported]; otherwise, false. + + + + + Sets the sample rate. + + The sample rate. + + + + Creates the buffers for playing. + + The number of outputs channels. + The number of input channel. + if set to true [use max buffer size] else use Prefered size + + + + Builds the capabilities internally. + + + + + Callback called by the ASIODriver on fill buffer demand. Redirect call to external callback. + + Index of the double buffer. + if set to true [direct process]. + + + + Callback called by the ASIODriver on event "Samples rate changed". + + The sample rate. + + + + Asio message call back. + + The selector. + The value. + The message. + The opt. + + + + + Buffers switch time info call back. + + The asio time param. + Index of the double buffer. + if set to true [direct process]. + + + + + Gets the driver used. + + The ASIOdriver. + + + + Gets or sets the fill buffer callback. + + The fill buffer callback. + + + + Gets the capabilities of the ASIODriver. + + The capabilities. + + + + This class stores convertors for different interleaved WaveFormat to ASIOSampleType separate channel + format. + + + + + Selects the sample convertor based on the input WaveFormat and the output ASIOSampleTtype. + + The wave format. + The type. + + + + + Optimized convertor for 2 channels SHORT + + + + + Generic convertor for SHORT + + + + + Optimized convertor for 2 channels FLOAT + + + + + Generic convertor SHORT + + + + + Optimized convertor for 2 channels SHORT + + + + + Generic convertor for SHORT + + + + + Optimized convertor for 2 channels FLOAT + + + + + Generic convertor SHORT + + + + + Generic converter 24 LSB + + + + + Generic convertor for float + + + + + ASIO common Exception. + + + + + Gets the name of the error. + + The error. + the name of the error + + + + Represents an installed ACM Driver + + + + + Helper function to determine whether a particular codec is installed + + The short name of the function + Whether the codec is installed + + + + Attempts to add a new ACM driver from a file + + Full path of the .acm or dll file containing the driver + Handle to the driver + + + + Removes a driver previously added using AddLocalDriver + + Local driver to remove + + + + Show Format Choose Dialog + + Owner window handle, can be null + Window title + Enumeration flags. None to get everything + Enumeration format. Only needed with certain enumeration flags + The selected format + Textual description of the selected format + Textual description of the selected format tag + True if a format was selected + + + + Finds a Driver by its short name + + Short Name + The driver, or null if not found + + + + Gets a list of the ACM Drivers installed + + + + + The callback for acmDriverEnum + + + + + Creates a new ACM Driver object + + Driver handle + + + + ToString + + + + + Gets all the supported formats for a given format tag + + Format tag + Supported formats + + + + Opens this driver + + + + + Closes this driver + + + + + Dispose + + + + + Gets the maximum size needed to store a WaveFormat for ACM interop functions + + + + + The short name of this driver + + + + + The full name of this driver + + + + + The driver ID + + + + + The list of FormatTags for this ACM Driver + + + + + Interop structure for ACM driver details (ACMDRIVERDETAILS) + http://msdn.microsoft.com/en-us/library/dd742889%28VS.85%29.aspx + + + + + ACMDRIVERDETAILS_SHORTNAME_CHARS + + + + + ACMDRIVERDETAILS_LONGNAME_CHARS + + + + + ACMDRIVERDETAILS_COPYRIGHT_CHARS + + + + + ACMDRIVERDETAILS_LICENSING_CHARS + + + + + ACMDRIVERDETAILS_FEATURES_CHARS + + + + + DWORD cbStruct + + + + + FOURCC fccType + + + + + FOURCC fccComp + + + + + WORD wMid; + + + + + WORD wPid + + + + + DWORD vdwACM + + + + + DWORD vdwDriver + + + + + DWORD fdwSupport; + + + + + DWORD cFormatTags + + + + + DWORD cFilterTags + + + + + HICON hicon + + + + + TCHAR szShortName[ACMDRIVERDETAILS_SHORTNAME_CHARS]; + + + + + TCHAR szLongName[ACMDRIVERDETAILS_LONGNAME_CHARS]; + + + + + TCHAR szCopyright[ACMDRIVERDETAILS_COPYRIGHT_CHARS]; + + + + + TCHAR szLicensing[ACMDRIVERDETAILS_LICENSING_CHARS]; + + + + + TCHAR szFeatures[ACMDRIVERDETAILS_FEATURES_CHARS]; + + + + + Flags indicating what support a particular ACM driver has + + + + ACMDRIVERDETAILS_SUPPORTF_CODEC - Codec + + + ACMDRIVERDETAILS_SUPPORTF_CONVERTER - Converter + + + ACMDRIVERDETAILS_SUPPORTF_FILTER - Filter + + + ACMDRIVERDETAILS_SUPPORTF_HARDWARE - Hardware + + + ACMDRIVERDETAILS_SUPPORTF_ASYNC - Async + + + ACMDRIVERDETAILS_SUPPORTF_LOCAL - Local + + + ACMDRIVERDETAILS_SUPPORTF_DISABLED - Disabled + + + + ACM_DRIVERENUMF_NOLOCAL, Only global drivers should be included in the enumeration + + + + + ACM_DRIVERENUMF_DISABLED, Disabled ACM drivers should be included in the enumeration + + + + + ACM Format + + + + + Format Index + + + + + Format Tag + + + + + Support Flags + + + + + WaveFormat + + + + + WaveFormat Size + + + + + Format Description + + + + + ACMFORMATCHOOSE + http://msdn.microsoft.com/en-us/library/dd742911%28VS.85%29.aspx + + + + + DWORD cbStruct; + + + + + DWORD fdwStyle; + + + + + HWND hwndOwner; + + + + + LPWAVEFORMATEX pwfx; + + + + + DWORD cbwfx; + + + + + LPCTSTR pszTitle; + + + + + TCHAR szFormatTag[ACMFORMATTAGDETAILS_FORMATTAG_CHARS]; + + + + + TCHAR szFormat[ACMFORMATDETAILS_FORMAT_CHARS]; + + + + + LPTSTR pszName; + n.b. can be written into + + + + + DWORD cchName + Should be at least 128 unless name is zero + + + + + DWORD fdwEnum; + + + + + LPWAVEFORMATEX pwfxEnum; + + + + + HINSTANCE hInstance; + + + + + LPCTSTR pszTemplateName; + + + + + LPARAM lCustData; + + + + + ACMFORMATCHOOSEHOOKPROC pfnHook; + + + + + None + + + + + ACMFORMATCHOOSE_STYLEF_SHOWHELP + + + + + ACMFORMATCHOOSE_STYLEF_ENABLEHOOK + + + + + ACMFORMATCHOOSE_STYLEF_ENABLETEMPLATE + + + + + ACMFORMATCHOOSE_STYLEF_ENABLETEMPLATEHANDLE + + + + + ACMFORMATCHOOSE_STYLEF_INITTOWFXSTRUCT + + + + + ACMFORMATCHOOSE_STYLEF_CONTEXTHELP + + + + + ACMFORMATDETAILS + http://msdn.microsoft.com/en-us/library/dd742913%28VS.85%29.aspx + + + + + ACMFORMATDETAILS_FORMAT_CHARS + + + + + DWORD cbStruct; + + + + + DWORD dwFormatIndex; + + + + + DWORD dwFormatTag; + + + + + DWORD fdwSupport; + + + + + LPWAVEFORMATEX pwfx; + + + + + DWORD cbwfx; + + + + + TCHAR szFormat[ACMFORMATDETAILS_FORMAT_CHARS]; + + + + + Format Enumeration Flags + + + + + None + + + + + ACM_FORMATENUMF_CONVERT + The WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will only enumerate destination formats that can be converted from the given pwfx format. + + + + + ACM_FORMATENUMF_HARDWARE + The enumerator should only enumerate formats that are supported as native input or output formats on one or more of the installed waveform-audio devices. This flag provides a way for an application to choose only formats native to an installed waveform-audio device. This flag must be used with one or both of the ACM_FORMATENUMF_INPUT and ACM_FORMATENUMF_OUTPUT flags. Specifying both ACM_FORMATENUMF_INPUT and ACM_FORMATENUMF_OUTPUT will enumerate only formats that can be opened for input or output. This is true regardless of whether this flag is specified. + + + + + ACM_FORMATENUMF_INPUT + Enumerator should enumerate only formats that are supported for input (recording). + + + + + ACM_FORMATENUMF_NCHANNELS + The nChannels member of the WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will enumerate only a format that conforms to this attribute. + + + + + ACM_FORMATENUMF_NSAMPLESPERSEC + The nSamplesPerSec member of the WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will enumerate only a format that conforms to this attribute. + + + + + ACM_FORMATENUMF_OUTPUT + Enumerator should enumerate only formats that are supported for output (playback). + + + + + ACM_FORMATENUMF_SUGGEST + The WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will enumerate all suggested destination formats for the given pwfx format. This mechanism can be used instead of the acmFormatSuggest function to allow an application to choose the best suggested format for conversion. The dwFormatIndex member will always be set to zero on return. + + + + + ACM_FORMATENUMF_WBITSPERSAMPLE + The wBitsPerSample member of the WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will enumerate only a format that conforms to this attribute. + + + + + ACM_FORMATENUMF_WFORMATTAG + The wFormatTag member of the WAVEFORMATEX structure pointed to by the pwfx member of the ACMFORMATDETAILS structure is valid. The enumerator will enumerate only a format that conforms to this attribute. The dwFormatTag member of the ACMFORMATDETAILS structure must be equal to the wFormatTag member. + + + + + ACM_FORMATSUGGESTF_WFORMATTAG + + + + + ACM_FORMATSUGGESTF_NCHANNELS + + + + + ACM_FORMATSUGGESTF_NSAMPLESPERSEC + + + + + ACM_FORMATSUGGESTF_WBITSPERSAMPLE + + + + + ACM_FORMATSUGGESTF_TYPEMASK + + + + + ACM Format Tag + + + + + Format Tag Index + + + + + Format Tag + + + + + Format Size + + + + + Support Flags + + + + + Standard Formats Count + + + + + Format Description + + + + + ACMFORMATTAGDETAILS_FORMATTAG_CHARS + + + + + DWORD cbStruct; + + + + + DWORD dwFormatTagIndex; + + + + + DWORD dwFormatTag; + + + + + DWORD cbFormatSize; + + + + + DWORD fdwSupport; + + + + + DWORD cStandardFormats; + + + + + TCHAR szFormatTag[ACMFORMATTAGDETAILS_FORMATTAG_CHARS]; + + + + + Interop definitions for Windows ACM (Audio Compression Manager) API + + + + + http://msdn.microsoft.com/en-us/library/dd742916%28VS.85%29.aspx + MMRESULT acmFormatSuggest( + HACMDRIVER had, + LPWAVEFORMATEX pwfxSrc, + LPWAVEFORMATEX pwfxDst, + DWORD cbwfxDst, + DWORD fdwSuggest); + + + + + http://msdn.microsoft.com/en-us/library/dd742928%28VS.85%29.aspx + MMRESULT acmStreamOpen( + LPHACMSTREAM phas, + HACMDRIVER had, + LPWAVEFORMATEX pwfxSrc, + LPWAVEFORMATEX pwfxDst, + LPWAVEFILTER pwfltr, + DWORD_PTR dwCallback, + DWORD_PTR dwInstance, + DWORD fdwOpen + + + + + A version with pointers for troubleshooting + + + + + http://msdn.microsoft.com/en-us/library/dd742910%28VS.85%29.aspx + UINT ACMFORMATCHOOSEHOOKPROC acmFormatChooseHookProc( + HWND hwnd, + UINT uMsg, + WPARAM wParam, + LPARAM lParam + + + + ACM_METRIC_COUNT_DRIVERS + + + ACM_METRIC_COUNT_CODECS + + + ACM_METRIC_COUNT_CONVERTERS + + + ACM_METRIC_COUNT_FILTERS + + + ACM_METRIC_COUNT_DISABLED + + + ACM_METRIC_COUNT_HARDWARE + + + ACM_METRIC_COUNT_LOCAL_DRIVERS + + + ACM_METRIC_COUNT_LOCAL_CODECS + + + ACM_METRIC_COUNT_LOCAL_CONVERTERS + + + ACM_METRIC_COUNT_LOCAL_FILTERS + + + ACM_METRIC_COUNT_LOCAL_DISABLED + + + ACM_METRIC_HARDWARE_WAVE_INPUT + + + ACM_METRIC_HARDWARE_WAVE_OUTPUT + + + ACM_METRIC_MAX_SIZE_FORMAT + + + ACM_METRIC_MAX_SIZE_FILTER + + + ACM_METRIC_DRIVER_SUPPORT + + + ACM_METRIC_DRIVER_PRIORITY + + + + AcmStream encapsulates an Audio Compression Manager Stream + used to convert audio from one format to another + + + + + Creates a new ACM stream to convert one format to another. Note that + not all conversions can be done in one step + + The source audio format + The destination audio format + + + + Creates a new ACM stream to convert one format to another, using a + specified driver identified and wave filter + + the driver identifier + the source format + the wave filter + + + + Returns the number of output bytes for a given number of input bytes + + Number of input bytes + Number of output bytes + + + + Returns the number of source bytes for a given number of destination bytes + + Number of destination bytes + Number of source bytes + + + + Suggests an appropriate PCM format that the compressed format can be converted + to in one step + + The compressed format + The PCM format + + + + Report that we have repositioned in the source stream + + + + + Converts the contents of the SourceBuffer into the DestinationBuffer + + The number of bytes in the SourceBuffer + that need to be converted + The number of source bytes actually converted + The number of converted bytes in the DestinationBuffer + + + + Converts the contents of the SourceBuffer into the DestinationBuffer + + The number of bytes in the SourceBuffer + that need to be converted + The number of converted bytes in the DestinationBuffer + + + + Frees resources associated with this ACM Stream + + + + + Frees resources associated with this ACM Stream + + + + + Frees resources associated with this ACM Stream + + + + + Returns the Source Buffer. Fill this with data prior to calling convert + + + + + Returns the Destination buffer. This will contain the converted data + after a successful call to Convert + + + + + ACM_STREAMCONVERTF_BLOCKALIGN + + + + + ACM_STREAMCONVERTF_START + + + + + ACM_STREAMCONVERTF_END + + + + + ACMSTREAMHEADER_STATUSF_DONE + + + + + ACMSTREAMHEADER_STATUSF_PREPARED + + + + + ACMSTREAMHEADER_STATUSF_INQUEUE + + + + + Interop structure for ACM stream headers. + ACMSTREAMHEADER + http://msdn.microsoft.com/en-us/library/dd742926%28VS.85%29.aspx + + + + + ACM_STREAMOPENF_QUERY, ACM will be queried to determine whether it supports the given conversion. A conversion stream will not be opened, and no handle will be returned in the phas parameter. + + + + + ACM_STREAMOPENF_ASYNC, Stream conversion should be performed asynchronously. If this flag is specified, the application can use a callback function to be notified when the conversion stream is opened and closed and after each buffer is converted. In addition to using a callback function, an application can examine the fdwStatus member of the ACMSTREAMHEADER structure for the ACMSTREAMHEADER_STATUSF_DONE flag. + + + + + ACM_STREAMOPENF_NONREALTIME, ACM will not consider time constraints when converting the data. By default, the driver will attempt to convert the data in real time. For some formats, specifying this flag might improve the audio quality or other characteristics. + + + + + CALLBACK_TYPEMASK, callback type mask + + + + + CALLBACK_NULL, no callback + + + + + CALLBACK_WINDOW, dwCallback is a HWND + + + + + CALLBACK_TASK, dwCallback is a HTASK + + + + + CALLBACK_FUNCTION, dwCallback is a FARPROC + + + + + CALLBACK_THREAD, thread ID replaces 16 bit task + + + + + CALLBACK_EVENT, dwCallback is an EVENT Handle + + + + + ACM_STREAMSIZEF_SOURCE + + + + + ACM_STREAMSIZEF_DESTINATION + + + + + Summary description for WaveFilter. + + + + + cbStruct + + + + + dwFilterTag + + + + + fdwFilter + + + + + reserved + + + + + Manufacturer codes from mmreg.h + + + + Microsoft Corporation + + + Creative Labs, Inc + + + Media Vision, Inc. + + + Fujitsu Corp. + + + Artisoft, Inc. + + + Turtle Beach, Inc. + + + IBM Corporation + + + Vocaltec LTD. + + + Roland + + + DSP Solutions, Inc. + + + NEC + + + ATI + + + Wang Laboratories, Inc + + + Tandy Corporation + + + Voyetra + + + Antex Electronics Corporation + + + ICL Personal Systems + + + Intel Corporation + + + Advanced Gravis + + + Video Associates Labs, Inc. + + + InterActive Inc + + + Yamaha Corporation of America + + + Everex Systems, Inc + + + Echo Speech Corporation + + + Sierra Semiconductor Corp + + + Computer Aided Technologies + + + APPS Software International + + + DSP Group, Inc + + + microEngineering Labs + + + Computer Friends, Inc. + + + ESS Technology + + + Audio, Inc. + + + Motorola, Inc. + + + Canopus, co., Ltd. + + + Seiko Epson Corporation + + + Truevision + + + Aztech Labs, Inc. + + + Videologic + + + SCALACS + + + Korg Inc. + + + Audio Processing Technology + + + Integrated Circuit Systems, Inc. + + + Iterated Systems, Inc. + + + Metheus + + + Logitech, Inc. + + + Winnov, Inc. + + + NCR Corporation + + + EXAN + + + AST Research Inc. + + + Willow Pond Corporation + + + Sonic Foundry + + + Vitec Multimedia + + + MOSCOM Corporation + + + Silicon Soft, Inc. + + + Supermac + + + Audio Processing Technology + + + Speech Compression + + + Ahead, Inc. + + + Dolby Laboratories + + + OKI + + + AuraVision Corporation + + + Ing C. Olivetti & C., S.p.A. + + + I/O Magic Corporation + + + Matsushita Electric Industrial Co., LTD. + + + Control Resources Limited + + + Xebec Multimedia Solutions Limited + + + New Media Corporation + + + Natural MicroSystems + + + Lyrrus Inc. + + + Compusic + + + OPTi Computers Inc. + + + Adlib Accessories Inc. + + + Compaq Computer Corp. + + + Dialogic Corporation + + + InSoft, Inc. + + + M.P. Technologies, Inc. + + + Weitek + + + Lernout & Hauspie + + + Quanta Computer Inc. + + + Apple Computer, Inc. + + + Digital Equipment Corporation + + + Mark of the Unicorn + + + Workbit Corporation + + + Ositech Communications Inc. + + + miro Computer Products AG + + + Cirrus Logic + + + ISOLUTION B.V. + + + Horizons Technology, Inc + + + Computer Concepts Ltd + + + Voice Technologies Group, Inc. + + + Radius + + + Rockwell International + + + Co. XYZ for testing + + + Opcode Systems + + + Voxware Inc + + + Northern Telecom Limited + + + APICOM + + + Grande Software + + + ADDX + + + Wildcat Canyon Software + + + Rhetorex Inc + + + Brooktree Corporation + + + ENSONIQ Corporation + + + FAST Multimedia AG + + + NVidia Corporation + + + OKSORI Co., Ltd. + + + DiAcoustics, Inc. + + + Gulbransen, Inc. + + + Kay Elemetrics, Inc. + + + Crystal Semiconductor Corporation + + + Splash Studios + + + Quarterdeck Corporation + + + TDK Corporation + + + Digital Audio Labs, Inc. + + + Seer Systems, Inc. + + + PictureTel Corporation + + + AT&T Microelectronics + + + Osprey Technologies, Inc. + + + Mediatrix Peripherals + + + SounDesignS M.C.S. Ltd. + + + A.L. Digital Ltd. + + + Spectrum Signal Processing, Inc. + + + Electronic Courseware Systems, Inc. + + + AMD + + + Core Dynamics + + + CANAM Computers + + + Softsound, Ltd. + + + Norris Communications, Inc. + + + Danka Data Devices + + + EuPhonics + + + Precept Software, Inc. + + + Crystal Net Corporation + + + Chromatic Research, Inc + + + Voice Information Systems, Inc + + + Vienna Systems + + + Connectix Corporation + + + Gadget Labs LLC + + + Frontier Design Group LLC + + + Viona Development GmbH + + + Casio Computer Co., LTD + + + Diamond Multimedia + + + S3 + + + Fraunhofer + + + + Summary description for MmException. + + + + + Creates a new MmException + + The result returned by the Windows API call + The name of the Windows API that failed + + + + Helper function to automatically raise an exception on failure + + The result of the API call + The API function name + + + + Returns the Windows API result + + + + + Windows multimedia error codes from mmsystem.h. + + + + no error, MMSYSERR_NOERROR + + + unspecified error, MMSYSERR_ERROR + + + device ID out of range, MMSYSERR_BADDEVICEID + + + driver failed enable, MMSYSERR_NOTENABLED + + + device already allocated, MMSYSERR_ALLOCATED + + + device handle is invalid, MMSYSERR_INVALHANDLE + + + no device driver present, MMSYSERR_NODRIVER + + + memory allocation error, MMSYSERR_NOMEM + + + function isn't supported, MMSYSERR_NOTSUPPORTED + + + error value out of range, MMSYSERR_BADERRNUM + + + invalid flag passed, MMSYSERR_INVALFLAG + + + invalid parameter passed, MMSYSERR_INVALPARAM + + + handle being used simultaneously on another thread (eg callback),MMSYSERR_HANDLEBUSY + + + specified alias not found, MMSYSERR_INVALIDALIAS + + + bad registry database, MMSYSERR_BADDB + + + registry key not found, MMSYSERR_KEYNOTFOUND + + + registry read error, MMSYSERR_READERROR + + + registry write error, MMSYSERR_WRITEERROR + + + registry delete error, MMSYSERR_DELETEERROR + + + registry value not found, MMSYSERR_VALNOTFOUND + + + driver does not call DriverCallback, MMSYSERR_NODRIVERCB + + + more data to be returned, MMSYSERR_MOREDATA + + + unsupported wave format, WAVERR_BADFORMAT + + + still something playing, WAVERR_STILLPLAYING + + + header not prepared, WAVERR_UNPREPARED + + + device is synchronous, WAVERR_SYNC + + + Conversion not possible (ACMERR_NOTPOSSIBLE) + + + Busy (ACMERR_BUSY) + + + Header Unprepared (ACMERR_UNPREPARED) + + + Cancelled (ACMERR_CANCELED) + + + invalid line (MIXERR_INVALLINE) + + + invalid control (MIXERR_INVALCONTROL) + + + invalid value (MIXERR_INVALVALUE) + + + + WaveHeader interop structure (WAVEHDR) + http://msdn.microsoft.com/en-us/library/dd743837%28VS.85%29.aspx + + + + pointer to locked data buffer (lpData) + + + length of data buffer (dwBufferLength) + + + used for input only (dwBytesRecorded) + + + for client's use (dwUser) + + + assorted flags (dwFlags) + + + loop control counter (dwLoops) + + + PWaveHdr, reserved for driver (lpNext) + + + reserved for driver + + + + Wave Header Flags enumeration + + + + + WHDR_BEGINLOOP + This buffer is the first buffer in a loop. This flag is used only with output buffers. + + + + + WHDR_DONE + Set by the device driver to indicate that it is finished with the buffer and is returning it to the application. + + + + + WHDR_ENDLOOP + This buffer is the last buffer in a loop. This flag is used only with output buffers. + + + + + WHDR_INQUEUE + Set by Windows to indicate that the buffer is queued for playback. + + + + + WHDR_PREPARED + Set by Windows to indicate that the buffer has been prepared with the waveInPrepareHeader or waveOutPrepareHeader function. + + + + + WASAPI Loopback Capture + based on a contribution from "Pygmy" - http://naudio.codeplex.com/discussions/203605 + + + + + Initialises a new instance of the WASAPI capture class + + + + + Initialises a new instance of the WASAPI capture class + + Capture device to use + + + + Gets the default audio loopback capture device + + The default audio loopback capture device + + + + Specify loopback + + + + + Recording wave format + + + + + Allows recording using the Windows waveIn APIs + Events are raised as recorded buffers are made available + + + + + Prepares a Wave input device for recording + + + + + Creates a WaveIn device using the specified window handle for callbacks + + A valid window handle + + + + Prepares a Wave input device for recording + + + + + Retrieves the capabilities of a waveIn device + + Device to test + The WaveIn device capabilities + + + + Called when we get a new buffer of recorded data + + + + + Start recording + + + + + Stop recording + + + + + Dispose pattern + + + + + Microphone Level + + + + + Dispose method + + + + + Indicates recorded data is available + + + + + Indicates that all recorded data has now been received. + + + + + Returns the number of Wave In devices available in the system + + + + + Milliseconds for the buffer. Recommended value is 100ms + + + + + Number of Buffers to use (usually 2 or 3) + + + + + The device number to use + + + + + WaveFormat we are recording in + + + + + WaveInCapabilities structure (based on WAVEINCAPS2 from mmsystem.h) + http://msdn.microsoft.com/en-us/library/ms713726(VS.85).aspx + + + + + wMid + + + + + wPid + + + + + vDriverVersion + + + + + Product Name (szPname) + + + + + Supported formats (bit flags) dwFormats + + + + + Supported channels (1 for mono 2 for stereo) (wChannels) + Seems to be set to -1 on a lot of devices + + + + + wReserved1 + + + + + Checks to see if a given SupportedWaveFormat is supported + + The SupportedWaveFormat + true if supported + + + + Number of channels supported + + + + + The product name + + + + + The device name Guid (if provided) + + + + + The product name Guid (if provided) + + + + + The manufacturer guid (if provided) + + + + + The device name from the registry if supported + + + + + Event Args for WaveInStream event + + + + + Creates new WaveInEventArgs + + + + + Buffer containing recorded data. Note that it might not be completely + full. + + + + + The number of recorded bytes in Buffer. + + + + + MME Wave function interop + + + + + CALLBACK_NULL + No callback + + + + + CALLBACK_FUNCTION + dwCallback is a FARPROC + + + + + CALLBACK_EVENT + dwCallback is an EVENT handle + + + + + CALLBACK_WINDOW + dwCallback is a HWND + + + + + CALLBACK_THREAD + callback is a thread ID + + + + + WIM_OPEN + + + + + WIM_CLOSE + + + + + WIM_DATA + + + + + WOM_CLOSE + + + + + WOM_DONE + + + + + WOM_OPEN + + + + + WaveOutCapabilities structure (based on WAVEOUTCAPS2 from mmsystem.h) + http://msdn.microsoft.com/library/default.asp?url=/library/en-us/multimed/htm/_win32_waveoutcaps_str.asp + + + + + wMid + + + + + wPid + + + + + vDriverVersion + + + + + Product Name (szPname) + + + + + Supported formats (bit flags) dwFormats + + + + + Supported channels (1 for mono 2 for stereo) (wChannels) + Seems to be set to -1 on a lot of devices + + + + + wReserved1 + + + + + Optional functionality supported by the device + + + + + Checks to see if a given SupportedWaveFormat is supported + + The SupportedWaveFormat + true if supported + + + + Number of channels supported + + + + + Whether playback control is supported + + + + + The product name + + + + + The device name Guid (if provided) + + + + + The product name Guid (if provided) + + + + + The manufacturer guid (if provided) + + + + + Supported wave formats for WaveOutCapabilities + + + + + 11.025 kHz, Mono, 8-bit + + + + + 11.025 kHz, Stereo, 8-bit + + + + + 11.025 kHz, Mono, 16-bit + + + + + 11.025 kHz, Stereo, 16-bit + + + + + 22.05 kHz, Mono, 8-bit + + + + + 22.05 kHz, Stereo, 8-bit + + + + + 22.05 kHz, Mono, 16-bit + + + + + 22.05 kHz, Stereo, 16-bit + + + + + 44.1 kHz, Mono, 8-bit + + + + + 44.1 kHz, Stereo, 8-bit + + + + + 44.1 kHz, Mono, 16-bit + + + + + 44.1 kHz, Stereo, 16-bit + + + + + 44.1 kHz, Mono, 8-bit + + + + + 44.1 kHz, Stereo, 8-bit + + + + + 44.1 kHz, Mono, 16-bit + + + + + 44.1 kHz, Stereo, 16-bit + + + + + 48 kHz, Mono, 8-bit + + + + + 48 kHz, Stereo, 8-bit + + + + + 48 kHz, Mono, 16-bit + + + + + 48 kHz, Stereo, 16-bit + + + + + 96 kHz, Mono, 8-bit + + + + + 96 kHz, Stereo, 8-bit + + + + + 96 kHz, Mono, 16-bit + + + + + 96 kHz, Stereo, 16-bit + + + + + Flags indicating what features this WaveOut device supports + + + + supports pitch control (WAVECAPS_PITCH) + + + supports playback rate control (WAVECAPS_PLAYBACKRATE) + + + supports volume control (WAVECAPS_VOLUME) + + + supports separate left-right volume control (WAVECAPS_LRVOLUME) + + + (WAVECAPS_SYNC) + + + (WAVECAPS_SAMPLEACCURATE) + + + + Sample provider interface to make WaveChannel32 extensible + Still a bit ugly, hence internal at the moment - and might even make these into + bit depth converting WaveProviders + + + + + A sample provider mixer, allowing inputs to be added and removed + + + + + Creates a new MixingSampleProvider, with no inputs, but a specified WaveFormat + + The WaveFormat of this mixer. All inputs must be in this format + + + + Creates a new MixingSampleProvider, based on the given inputs + + Mixer inputs - must all have the same waveformat, and must + all be of the same WaveFormat. There must be at least one input + + + + Adds a WaveProvider as a Mixer input. + Must be PCM or IEEE float already + + IWaveProvider mixer input + + + + Adds a new mixer input + + Mixer input + + + + Removes a mixer input + + Mixer input to remove + + + + Removes all mixer inputs + + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples required + Number of samples read + + + + When set to true, the Read method always returns the number + of samples requested, even if there are no inputs, or if the + current inputs reach their end. Setting this to true effectively + makes this a never-ending sample provider, so take care if you plan + to write it out to a file. + + + + + The output WaveFormat of this sample provider + + + + + Converts a mono sample provider to stereo, with a customisable pan strategy + + + + + Initialises a new instance of the PanningSampleProvider + + Source sample provider, must be mono + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples desired + Number of samples read + + + + Pan value, must be between -1 (left) and 1 (right) + + + + + The pan strategy currently in use + + + + + The WaveFormat of this sample provider + + + + + Pair of floating point values, representing samples or multipliers + + + + + Left value + + + + + Right value + + + + + Required Interface for a Panning Strategy + + + + + Gets the left and right multipliers for a given pan value + + Pan value from -1 to 1 + Left and right multipliers in a stereo sample pair + + + + Simplistic "balance" control - treating the mono input as if it was stereo + In the centre, both channels full volume. Opposite channel decays linearly + as balance is turned to to one side + + + + + Gets the left and right channel multipliers for this pan value + + Pan value, between -1 and 1 + Left and right multipliers + + + + Square Root Pan, thanks to Yuval Naveh + + + + + Gets the left and right channel multipliers for this pan value + + Pan value, between -1 and 1 + Left and right multipliers + + + + Sinus Pan, thanks to Yuval Naveh + + + + + Gets the left and right channel multipliers for this pan value + + Pan value, between -1 and 1 + Left and right multipliers + + + + Linear Pan + + + + + Gets the left and right channel multipliers for this pan value + + Pan value, between -1 and 1 + Left and right multipliers + + + + GSM 610 + + + + + Represents a Wave file format + + + + format type + + + number of channels + + + sample rate + + + for buffer estimation + + + block size of data + + + number of bits per sample of mono data + + + number of following bytes + + + + Creates a new PCM 44.1Khz stereo 16 bit format + + + + + Creates a new 16 bit wave format with the specified sample + rate and channel count + + Sample Rate + Number of channels + + + + Gets the size of a wave buffer equivalent to the latency in milliseconds. + + The milliseconds. + + + + + Creates a WaveFormat with custom members + + The encoding + Sample Rate + Number of channels + Average Bytes Per Second + Block Align + Bits Per Sample + + + + + Creates an A-law wave format + + Sample Rate + Number of Channels + Wave Format + + + + Creates a Mu-law wave format + + Sample Rate + Number of Channels + Wave Format + + + + Creates a new PCM format with the specified sample rate, bit depth and channels + + + + + Creates a new 32 bit IEEE floating point wave format + + sample rate + number of channels + + + + Helper function to retrieve a WaveFormat structure from a pointer + + WaveFormat structure + + + + + Helper function to marshal WaveFormat to an IntPtr + + WaveFormat + IntPtr to WaveFormat structure (needs to be freed by callee) + + + + Reads in a WaveFormat (with extra data) from a fmt chunk (chunk identifier and + length should already have been read) + + Binary reader + Format chunk length + A WaveFormatExtraData + + + + Reads a new WaveFormat object from a stream + + A binary reader that wraps the stream + + + + Reports this WaveFormat as a string + + String describing the wave format + + + + Compares with another WaveFormat object + + Object to compare to + True if the objects are the same + + + + Provides a Hashcode for this WaveFormat + + A hashcode + + + + Writes this WaveFormat object to a stream + + the output stream + + + + Returns the encoding type used + + + + + Returns the number of channels (1=mono,2=stereo etc) + + + + + Returns the sample rate (samples per second) + + + + + Returns the average number of bytes used per second + + + + + Returns the block alignment + + + + + Returns the number of bits per sample (usually 16 or 32, sometimes 24 or 8) + Can be 0 for some codecs + + + + + Returns the number of extra bytes used by this waveformat. Often 0, + except for compressed formats which store extra data after the WAVEFORMATEX header + + + + + Creates a GSM 610 WaveFormat + For now hardcoded to 13kbps + + + + + Writes this structure to a BinaryWriter + + + + + Samples per block + + + + + IMA/DVI ADPCM Wave Format + Work in progress + + + + + parameterless constructor for Marshalling + + + + + Creates a new IMA / DVI ADPCM Wave Format + + Sample Rate + Number of channels + Bits Per Sample + + + + MP3 WaveFormat, MPEGLAYER3WAVEFORMAT from mmreg.h + + + + + Wave format ID (wID) + + + + + Padding flags (fdwFlags) + + + + + Block Size (nBlockSize) + + + + + Frames per block (nFramesPerBlock) + + + + + Codec Delay (nCodecDelay) + + + + + Creates a new MP3 WaveFormat + + + + + Wave Format Padding Flags + + + + + MPEGLAYER3_FLAG_PADDING_ISO + + + + + MPEGLAYER3_FLAG_PADDING_ON + + + + + MPEGLAYER3_FLAG_PADDING_OFF + + + + + Wave Format ID + + + + MPEGLAYER3_ID_UNKNOWN + + + MPEGLAYER3_ID_MPEG + + + MPEGLAYER3_ID_CONSTANTFRAMESIZE + + + + DSP Group TrueSpeech + + + + + DSP Group TrueSpeech WaveFormat + + + + + Writes this structure to a BinaryWriter + + + + + Microsoft ADPCM + See http://icculus.org/SDL_sound/downloads/external_documentation/wavecomp.htm + + + + + Empty constructor needed for marshalling from a pointer + + + + + Microsoft ADPCM + + Sample Rate + Channels + + + + Serializes this wave format + + Binary writer + + + + String Description of this WaveFormat + + + + + Samples per block + + + + + Number of coefficients + + + + + Coefficients + + + + + Custom marshaller for WaveFormat structures + + + + + Gets the instance of this marshaller + + + + + + + Clean up managed data + + + + + Clean up native data + + + + + + Get native data size + + + + + Marshal managed to native + + + + + Marshal Native to Managed + + + + + Summary description for WaveFormatEncoding. + + + + WAVE_FORMAT_UNKNOWN, Microsoft Corporation + + + WAVE_FORMAT_PCM Microsoft Corporation + + + WAVE_FORMAT_ADPCM Microsoft Corporation + + + WAVE_FORMAT_IEEE_FLOAT Microsoft Corporation + + + WAVE_FORMAT_VSELP Compaq Computer Corp. + + + WAVE_FORMAT_IBM_CVSD IBM Corporation + + + WAVE_FORMAT_ALAW Microsoft Corporation + + + WAVE_FORMAT_MULAW Microsoft Corporation + + + WAVE_FORMAT_DTS Microsoft Corporation + + + WAVE_FORMAT_DRM Microsoft Corporation + + + WAVE_FORMAT_WMAVOICE9 + + + WAVE_FORMAT_OKI_ADPCM OKI + + + WAVE_FORMAT_DVI_ADPCM Intel Corporation + + + WAVE_FORMAT_IMA_ADPCM Intel Corporation + + + WAVE_FORMAT_MEDIASPACE_ADPCM Videologic + + + WAVE_FORMAT_SIERRA_ADPCM Sierra Semiconductor Corp + + + WAVE_FORMAT_G723_ADPCM Antex Electronics Corporation + + + WAVE_FORMAT_DIGISTD DSP Solutions, Inc. + + + WAVE_FORMAT_DIGIFIX DSP Solutions, Inc. + + + WAVE_FORMAT_DIALOGIC_OKI_ADPCM Dialogic Corporation + + + WAVE_FORMAT_MEDIAVISION_ADPCM Media Vision, Inc. + + + WAVE_FORMAT_CU_CODEC Hewlett-Packard Company + + + WAVE_FORMAT_YAMAHA_ADPCM Yamaha Corporation of America + + + WAVE_FORMAT_SONARC Speech Compression + + + WAVE_FORMAT_DSPGROUP_TRUESPEECH DSP Group, Inc + + + WAVE_FORMAT_ECHOSC1 Echo Speech Corporation + + + WAVE_FORMAT_AUDIOFILE_AF36, Virtual Music, Inc. + + + WAVE_FORMAT_APTX Audio Processing Technology + + + WAVE_FORMAT_AUDIOFILE_AF10, Virtual Music, Inc. + + + WAVE_FORMAT_PROSODY_1612, Aculab plc + + + WAVE_FORMAT_LRC, Merging Technologies S.A. + + + WAVE_FORMAT_DOLBY_AC2, Dolby Laboratories + + + WAVE_FORMAT_GSM610, Microsoft Corporation + + + WAVE_FORMAT_MSNAUDIO, Microsoft Corporation + + + WAVE_FORMAT_ANTEX_ADPCME, Antex Electronics Corporation + + + WAVE_FORMAT_CONTROL_RES_VQLPC, Control Resources Limited + + + WAVE_FORMAT_DIGIREAL, DSP Solutions, Inc. + + + WAVE_FORMAT_DIGIADPCM, DSP Solutions, Inc. + + + WAVE_FORMAT_CONTROL_RES_CR10, Control Resources Limited + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WAVE_FORMAT_MPEG, Microsoft Corporation + + + + + + + + + WAVE_FORMAT_MPEGLAYER3, ISO/MPEG Layer3 Format Tag + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WAVE_FORMAT_GSM + + + WAVE_FORMAT_G729 + + + WAVE_FORMAT_G723 + + + WAVE_FORMAT_ACELP + + + + WAVE_FORMAT_RAW_AAC1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Windows Media Audio, WAVE_FORMAT_WMAUDIO2, Microsoft Corporation + + + + + Windows Media Audio Professional WAVE_FORMAT_WMAUDIO3, Microsoft Corporation + + + + + Windows Media Audio Lossless, WAVE_FORMAT_WMAUDIO_LOSSLESS + + + + + Windows Media Audio Professional over SPDIF WAVE_FORMAT_WMASPDIF (0x0164) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Advanced Audio Coding (AAC) audio in Audio Data Transport Stream (ADTS) format. + The format block is a WAVEFORMATEX structure with wFormatTag equal to WAVE_FORMAT_MPEG_ADTS_AAC. + + + The WAVEFORMATEX structure specifies the core AAC-LC sample rate and number of channels, + prior to applying spectral band replication (SBR) or parametric stereo (PS) tools, if present. + No additional data is required after the WAVEFORMATEX structure. + + http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + + + + Source wmCodec.h + + + + MPEG-4 audio transport stream with a synchronization layer (LOAS) and a multiplex layer (LATM). + The format block is a WAVEFORMATEX structure with wFormatTag equal to WAVE_FORMAT_MPEG_LOAS. + + + The WAVEFORMATEX structure specifies the core AAC-LC sample rate and number of channels, + prior to applying spectral SBR or PS tools, if present. + No additional data is required after the WAVEFORMATEX structure. + + http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + + + NOKIA_MPEG_ADTS_AAC + Source wmCodec.h + + + NOKIA_MPEG_RAW_AAC + Source wmCodec.h + + + VODAFONE_MPEG_ADTS_AAC + Source wmCodec.h + + + VODAFONE_MPEG_RAW_AAC + Source wmCodec.h + + + + High-Efficiency Advanced Audio Coding (HE-AAC) stream. + The format block is an HEAACWAVEFORMAT structure. + + http://msdn.microsoft.com/en-us/library/dd317599%28VS.85%29.aspx + + + WAVE_FORMAT_DVM + + + WAVE_FORMAT_VORBIS1 "Og" Original stream compatible + + + WAVE_FORMAT_VORBIS2 "Pg" Have independent header + + + WAVE_FORMAT_VORBIS3 "Qg" Have no codebook header + + + WAVE_FORMAT_VORBIS1P "og" Original stream compatible + + + WAVE_FORMAT_VORBIS2P "pg" Have independent headere + + + WAVE_FORMAT_VORBIS3P "qg" Have no codebook header + + + WAVE_FORMAT_EXTENSIBLE + + + + + + + WaveFormatExtensible + http://www.microsoft.com/whdc/device/audio/multichaud.mspx + + + + + Parameterless constructor for marshalling + + + + + Creates a new WaveFormatExtensible for PCM or IEEE + + + + + WaveFormatExtensible for PCM or floating point can be awkward to work with + This creates a regular WaveFormat structure representing the same audio format + + + + + + Serialize + + + + + + String representation + + + + + SubFormat (may be one of AudioMediaSubtypes) + + + + + This class used for marshalling from unmanaged code + + + + + parameterless constructor for marshalling + + + + + Reads this structure from a BinaryReader + + + + + Writes this structure to a BinaryWriter + + + + + Allows the extra data to be read + + + + + The WMA wave format. + May not be much use because WMA codec is a DirectShow DMO not an ACM + + + + + This class writes audio data to a .aif file on disk + + + + + Creates an Aiff file by reading all the data from a WaveProvider + BEWARE: the WaveProvider MUST return 0 from its Read method when it is finished, + or the Aiff File will grow indefinitely. + + The filename to use + The source WaveProvider + + + + AiffFileWriter that actually writes to a stream + + Stream to be written to + Wave format to use + + + + Creates a new AiffFileWriter + + The filename to write to + The Wave Format of the output data + + + + Read is not supported for a AiffFileWriter + + + + + Seek is not supported for a AiffFileWriter + + + + + SetLength is not supported for AiffFileWriter + + + + + + Appends bytes to the AiffFile (assumes they are already in the correct format) + + the buffer containing the wave data + the offset from which to start writing + the number of bytes to write + + + + Writes a single sample to the Aiff file + + the sample to write (assumed floating point with 1.0f as max value) + + + + Writes 32 bit floating point samples to the Aiff file + They will be converted to the appropriate bit depth depending on the WaveFormat of the AIF file + + The buffer containing the floating point samples + The offset from which to start writing + The number of floating point samples to write + + + + Writes 16 bit samples to the Aiff file + + The buffer containing the 16 bit samples + The offset from which to start writing + The number of 16 bit samples to write + + + + Ensures data is written to disk + + + + + Actually performs the close,making sure the header contains the correct data + + True if called from Dispose + + + + Updates the header with file size information + + + + + Finaliser - should only be called if the user forgot to close this AiffFileWriter + + + + + The aiff file name or null if not applicable + + + + + Number of bytes of audio in the data chunk + + + + + WaveFormat of this aiff file + + + + + Returns false: Cannot read from a AiffFileWriter + + + + + Returns true: Can write to a AiffFileWriter + + + + + Returns false: Cannot seek within a AiffFileWriter + + + + + Gets the Position in the AiffFile (i.e. number of bytes written so far) + + + + + Raised when ASIO data has been recorded. + It is important to handle this as quickly as possible as it is in the buffer callback + + + + + Initialises a new instance of AsioAudioAvailableEventArgs + + Pointers to the ASIO buffers for each channel + Pointers to the ASIO buffers for each channel + Number of samples in each buffer + Audio format within each buffer + + + + Converts all the recorded audio into a buffer of 32 bit floating point samples, interleaved by channel + + The samples as 32 bit floating point, interleaved + + + + Gets as interleaved samples, allocating a float array + + The samples as 32 bit floating point values + + + + Pointer to a buffer per input channel + + + + + Pointer to a buffer per output channel + Allows you to write directly to the output buffers + If you do so, set SamplesPerBuffer = true, + and make sure all buffers are written to with valid data + + + + + Set to true if you have written to the output buffers + If so, AsioOut will not read from its source + + + + + Number of samples in each buffer + + + + + Audio format within each buffer + Most commonly this will be one of, Int32LSB, Int16LSB, Int24LSB or Float32LSB + + + + + ASIO Out Player. New implementation using an internal C# binding. + + This implementation is only supporting Short16Bit and Float32Bit formats and is optimized + for 2 outputs channels . + SampleRate is supported only if ASIODriver is supporting it + + This implementation is probably the first ASIODriver binding fully implemented in C#! + + Original Contributor: Mark Heath + New Contributor to C# binding : Alexandre Mutel - email: alexandre_mutel at yahoo.fr + + + + + Represents the interface to a device that can play a WaveFile + + + + + Begin playback + + + + + Stop playback + + + + + Pause Playback + + + + + Initialise playback + + The waveprovider to be played + + + + Current playback state + + + + + The volume 1.0 is full scale + + + + + Indicates that playback has gone into a stopped state due to + reaching the end of the input stream or an error has been encountered during playback + + + + + Initializes a new instance of the class with the first + available ASIO Driver. + + + + + Initializes a new instance of the class with the driver name. + + Name of the device. + + + + Opens an ASIO output device + + Device number (zero based) + + + + Releases unmanaged resources and performs other cleanup operations before the + is reclaimed by garbage collection. + + + + + Dispose + + + + + Gets the names of the installed ASIO Driver. + + an array of driver names + + + + Determines whether ASIO is supported. + + + true if ASIO is supported; otherwise, false. + + + + + Inits the driver from the asio driver name. + + Name of the driver. + + + + Shows the control panel + + + + + Starts playback + + + + + Stops playback + + + + + Pauses playback + + + + + Initialises to play + + Source wave provider + + + + Initialises to play, with optional recording + + Source wave provider - set to null for record only + Number of channels to record + Specify sample rate here if only recording, ignored otherwise + + + + driver buffer update callback to fill the wave buffer. + + The input channels. + The output channels. + + + + Get the input channel name + + channel index (zero based) + channel name + + + + Get the output channel name + + channel index (zero based) + channel name + + + + Playback Stopped + + + + + When recording, fires whenever recorded audio is available + + + + + Gets the latency (in ms) of the playback driver + + + + + Playback State + + + + + Driver Name + + + + + The number of output channels we are currently using for playback + (Must be less than or equal to DriverOutputChannelCount) + + + + + The number of input channels we are currently recording from + (Must be less than or equal to DriverInputChannelCount) + + + + + The maximum number of input channels this ASIO driver supports + + + + + The maximum number of output channels this ASIO driver supports + + + + + By default the first channel on the input WaveProvider is sent to the first ASIO output. + This option sends it to the specified channel number. + Warning: make sure you don't set it higher than the number of available output channels - + the number of source channels. + n.b. Future NAudio may modify this + + + + + Input channel offset (used when recording), allowing you to choose to record from just one + specific input rather than them all + + + + + Sets the volume (1.0 is unity gain) + Not supported for ASIO Out. Set the volume on the input stream instead + + + + + A wave file writer that adds cue support + + + + + This class writes WAV data to a .wav file on disk + + + + + Creates a 16 bit Wave File from an ISampleProvider + BEWARE: the source provider must not return data indefinitely + + The filename to write to + The source sample provider + + + + Creates a Wave file by reading all the data from a WaveProvider + BEWARE: the WaveProvider MUST return 0 from its Read method when it is finished, + or the Wave File will grow indefinitely. + + The filename to use + The source WaveProvider + + + + WaveFileWriter that actually writes to a stream + + Stream to be written to + Wave format to use + + + + Creates a new WaveFileWriter + + The filename to write to + The Wave Format of the output data + + + + Read is not supported for a WaveFileWriter + + + + + Seek is not supported for a WaveFileWriter + + + + + SetLength is not supported for WaveFileWriter + + + + + + Appends bytes to the WaveFile (assumes they are already in the correct format) + + the buffer containing the wave data + the offset from which to start writing + the number of bytes to write + + + + Appends bytes to the WaveFile (assumes they are already in the correct format) + + the buffer containing the wave data + the offset from which to start writing + the number of bytes to write + + + + Writes a single sample to the Wave file + + the sample to write (assumed floating point with 1.0f as max value) + + + + Writes 32 bit floating point samples to the Wave file + They will be converted to the appropriate bit depth depending on the WaveFormat of the WAV file + + The buffer containing the floating point samples + The offset from which to start writing + The number of floating point samples to write + + + + Writes 16 bit samples to the Wave file + + The buffer containing the 16 bit samples + The offset from which to start writing + The number of 16 bit samples to write + + + + Writes 16 bit samples to the Wave file + + The buffer containing the 16 bit samples + The offset from which to start writing + The number of 16 bit samples to write + + + + Ensures data is written to disk + + + + + Actually performs the close,making sure the header contains the correct data + + True if called from Dispose + + + + Updates the header with file size information + + + + + Finaliser - should only be called if the user forgot to close this WaveFileWriter + + + + + The wave file name or null if not applicable + + + + + Number of bytes of audio in the data chunk + + + + + WaveFormat of this wave file + + + + + Returns false: Cannot read from a WaveFileWriter + + + + + Returns true: Can write to a WaveFileWriter + + + + + Returns false: Cannot seek within a WaveFileWriter + + + + + Gets the Position in the WaveFile (i.e. number of bytes written so far) + + + + + Writes a wave file, including a cues chunk + + + + + Adds a cue to the Wave file + + Sample position + Label text + + + + Updates the header, and writes the cues out + + + + + Media Foundation Encoder class allows you to use Media Foundation to encode an IWaveProvider + to any supported encoding format + + + + + Queries the available bitrates for a given encoding output type, sample rate and number of channels + + Audio subtype - a value from the AudioSubtypes class + The sample rate of the PCM to encode + The number of channels of the PCM to encode + An array of available bitrates in average bits per second + + + + Gets all the available media types for a particular + + Audio subtype - a value from the AudioSubtypes class + An array of available media types that can be encoded with this subtype + + + + Helper function to simplify encoding Window Media Audio + Should be supported on Vista and above (not tested) + + Input provider, must be PCM + Output file path, should end with .wma + Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type + + + + Helper function to simplify encoding to MP3 + By default, will only be available on Windows 8 and above + + Input provider, must be PCM + Output file path, should end with .mp3 + Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type + + + + Helper function to simplify encoding to AAC + By default, will only be available on Windows 7 and above + + Input provider, must be PCM + Output file path, should end with .mp4 (or .aac on Windows 8) + Desired bitrate. Use GetEncodeBitrates to find the possibilities for your input type + + + + Tries to find the encoding media type with the closest bitrate to that specified + + Audio subtype, a value from AudioSubtypes + Your encoder input format (used to check sample rate and channel count) + Your desired bitrate + The closest media type, or null if none available + + + + Creates a new encoder that encodes to the specified output media type + + Desired output media type + + + + Encodes a file + + Output filename (container type is deduced from the filename) + Input provider (should be PCM, some encoders will also allow IEEE float) + + + + Disposes this instance + + + + + + Disposes this instance + + + + + Finalizer + + + + + Media Type helper class, simplifying working with IMFMediaType + (will probably change in the future, to inherit from an attributes class) + Currently does not release the COM object, so you must do that yourself + + + + + Wraps an existing IMFMediaType object + + The IMFMediaType object + + + + Creates and wraps a new IMFMediaType object + + + + + Creates and wraps a new IMFMediaType object based on a WaveFormat + + WaveFormat + + + + Tries to get a UINT32 value, returning a default value if it doesn't exist + + Attribute key + Default value + Value or default if key doesn't exist + + + + The Sample Rate (valid for audio media types) + + + + + The number of Channels (valid for audio media types) + + + + + The number of bits per sample (n.b. not always valid for compressed audio types) + + + + + The average bytes per second (valid for audio media types) + + + + + The Media Subtype. For audio, is a value from the AudioSubtypes class + + + + + The Major type, e.g. audio or video (from the MediaTypes class) + + + + + Access to the actual IMFMediaType object + Use to pass to MF APIs or Marshal.ReleaseComObject when you are finished with it + + + + + Stopped Event Args + + + + + Initializes a new instance of StoppedEventArgs + + An exception to report (null if no exception) + + + + An exception. Will be null if the playback or record operation stopped + + + + + IWaveBuffer interface use to store wave datas. + Data can be manipulated with arrays (,, + , ) that are pointing to the same memory buffer. + This is a requirement for all subclasses. + + Use the associated Count property based on the type of buffer to get the number of data in the + buffer. + + for the standard implementation using C# unions. + + + + + Gets the byte buffer. + + The byte buffer. + + + + Gets the float buffer. + + The float buffer. + + + + Gets the short buffer. + + The short buffer. + + + + Gets the int buffer. + + The int buffer. + + + + Gets the max size in bytes of the byte buffer.. + + Maximum number of bytes in the buffer. + + + + Gets the byte buffer count. + + The byte buffer count. + + + + Gets the float buffer count. + + The float buffer count. + + + + Gets the short buffer count. + + The short buffer count. + + + + Gets the int buffer count. + + The int buffer count. + + + + Interface for IWavePlayers that can report position + + + + + Position (in terms of bytes played - does not necessarily) + + Position in bytes + + + + Gets a instance indicating the format the hardware is using. + + + + + NativeDirectSoundOut using DirectSound COM interop. + Contact author: Alexandre Mutel - alexandre_mutel at yahoo.fr + Modified by: Graham "Gee" Plumb + + + + + Initializes a new instance of the class. + + + + + Initializes a new instance of the class. + + + + + Initializes a new instance of the class. + + + + + Initializes a new instance of the class. + (40ms seems to work under Vista). + + The latency. + Selected device + + + + Releases unmanaged resources and performs other cleanup operations before the + is reclaimed by garbage collection. + + + + + Begin playback + + + + + Stop playback + + + + + Pause Playback + + + + + Gets the current position in bytes from the wave output device. + (n.b. this is not the same thing as the position within your reader + stream) + + Position in bytes + + + + Initialise playback + + The waveprovider to be played + + + + Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + + + + + Determines whether the SecondaryBuffer is lost. + + + true if [is buffer lost]; otherwise, false. + + + + + Convert ms to bytes size according to WaveFormat + + The ms + number of byttes + + + + Processes the samples in a separate thread. + + + + + Stop playback + + + + + Feeds the SecondaryBuffer with the WaveStream + + number of bytes to feed + + + + Instanciate DirectSound from the DLL + + The GUID. + The direct sound. + The p unk outer. + + + + DirectSound default playback device GUID + + + + + DirectSound default capture device GUID + + + + + DirectSound default device for voice playback + + + + + DirectSound default device for voice capture + + + + + The DirectSoundEnumerate function enumerates the DirectSound drivers installed in the system. + + callback function + User context + + + + Gets the HANDLE of the desktop window. + + HANDLE of the Desktop window + + + + Playback Stopped + + + + + Gets the DirectSound output devices in the system + + + + + Gets the current position from the wave output device. + + + + + Current playback state + + + + + + The volume 1.0 is full scale + + + + + + IDirectSound interface + + + + + IDirectSoundBuffer interface + + + + + IDirectSoundNotify interface + + + + + The DSEnumCallback function is an application-defined callback function that enumerates the DirectSound drivers. + The system calls this function in response to the application's call to the DirectSoundEnumerate or DirectSoundCaptureEnumerate function. + + Address of the GUID that identifies the device being enumerated, or NULL for the primary device. This value can be passed to the DirectSoundCreate8 or DirectSoundCaptureCreate8 function to create a device object for that driver. + Address of a null-terminated string that provides a textual description of the DirectSound device. + Address of a null-terminated string that specifies the module name of the DirectSound driver corresponding to this device. + Address of application-defined data. This is the pointer passed to DirectSoundEnumerate or DirectSoundCaptureEnumerate as the lpContext parameter. + Returns TRUE to continue enumerating drivers, or FALSE to stop. + + + + Class for enumerating DirectSound devices + + + + + The device identifier + + + + + Device description + + + + + Device module name + + + + + Playback State + + + + + Stopped + + + + + Playing + + + + + Paused + + + + + Support for playback using Wasapi + + + + + WASAPI Out using default audio endpoint + + ShareMode - shared or exclusive + Desired latency in milliseconds + + + + WASAPI Out using default audio endpoint + + ShareMode - shared or exclusive + true if sync is done with event. false use sleep. + Desired latency in milliseconds + + + + Creates a new WASAPI Output + + Device to use + + true if sync is done with event. false use sleep. + + + + + Gets the current position in bytes from the wave output device. + (n.b. this is not the same thing as the position within your reader + stream) + + Position in bytes + + + + Begin Playback + + + + + Stop playback and flush buffers + + + + + Stop playback without flushing buffers + + + + + Initialize for playing the specified wave stream + + IWaveProvider to play + + + + Dispose + + + + + Playback Stopped + + + + + Gets a instance indicating the format the hardware is using. + + + + + Playback State + + + + + Volume + + + + + Retrieve the AudioStreamVolume object for this audio stream + + + This returns the AudioStreamVolume object ONLY for shared audio streams. + + + This is thrown when an exclusive audio stream is being used. + + + + + WaveBuffer class use to store wave datas. Data can be manipulated with arrays + (,,, ) that are pointing to the + same memory buffer. Use the associated Count property based on the type of buffer to get the number of + data in the buffer. + Implicit casting is now supported to float[], byte[], int[], short[]. + You must not use Length on returned arrays. + + n.b. FieldOffset is 8 now to allow it to work natively on 64 bit + + + + + Number of Bytes + + + + + Initializes a new instance of the class. + + The number of bytes. The size of the final buffer will be aligned on 4 Bytes (upper bound) + + + + Initializes a new instance of the class binded to a specific byte buffer. + + A byte buffer to bound the WaveBuffer to. + + + + Binds this WaveBuffer instance to a specific byte buffer. + + A byte buffer to bound the WaveBuffer to. + + + + Performs an implicit conversion from to . + + The wave buffer. + The result of the conversion. + + + + Performs an implicit conversion from to . + + The wave buffer. + The result of the conversion. + + + + Performs an implicit conversion from to . + + The wave buffer. + The result of the conversion. + + + + Performs an implicit conversion from to . + + The wave buffer. + The result of the conversion. + + + + Clears the associated buffer. + + + + + Copy this WaveBuffer to a destination buffer up to ByteBufferCount bytes. + + + + + Checks the validity of the count parameters. + + Name of the arg. + The value. + The size of value. + + + + Gets the byte buffer. + + The byte buffer. + + + + Gets the float buffer. + + The float buffer. + + + + Gets the short buffer. + + The short buffer. + + + + Gets the int buffer. + + The int buffer. + + + + Gets the max size in bytes of the byte buffer.. + + Maximum number of bytes in the buffer. + + + + Gets or sets the byte buffer count. + + The byte buffer count. + + + + Gets or sets the float buffer count. + + The float buffer count. + + + + Gets or sets the short buffer count. + + The short buffer count. + + + + Gets or sets the int buffer count. + + The int buffer count. + + + + Wave Callback Info + + + + + Sets up a new WaveCallbackInfo for function callbacks + + + + + Sets up a new WaveCallbackInfo to use a New Window + IMPORTANT: only use this on the GUI thread + + + + + Sets up a new WaveCallbackInfo to use an existing window + IMPORTANT: only use this on the GUI thread + + + + + Callback Strategy + + + + + Window Handle (if applicable) + + + + + Wave Callback Strategy + + + + + Use a function + + + + + Create a new window (should only be done if on GUI thread) + + + + + Use an existing window handle + + + + + Use an event handle + + + + + Represents a wave out device + + + + + Retrieves the capabilities of a waveOut device + + Device to test + The WaveOut device capabilities + + + + Creates a default WaveOut device + Will use window callbacks if called from a GUI thread, otherwise function + callbacks + + + + + Creates a WaveOut device using the specified window handle for callbacks + + A valid window handle + + + + Opens a WaveOut device + + + + + Initialises the WaveOut device + + WaveProvider to play + + + + Start playing the audio from the WaveStream + + + + + Pause the audio + + + + + Resume playing after a pause from the same position + + + + + Stop and reset the WaveOut device + + + + + Gets the current position in bytes from the wave output device. + (n.b. this is not the same thing as the position within your reader + stream - it calls directly into waveOutGetPosition) + + Position in bytes + + + + Closes this WaveOut device + + + + + Closes the WaveOut device and disposes of buffers + + True if called from Dispose + + + + Finalizer. Only called when user forgets to call Dispose + + + + + Indicates playback has stopped automatically + + + + + Returns the number of Wave Out devices available in the system + + + + + Gets or sets the desired latency in milliseconds + Should be set before a call to Init + + + + + Gets or sets the number of buffers used + Should be set before a call to Init + + + + + Gets or sets the device number + Should be set before a call to Init + This must be between 0 and DeviceCount - 1. + + + + + Gets a instance indicating the format the hardware is using. + + + + + Playback State + + + + + Volume for this device 1.0 is full scale + + + + + Alternative WaveOut class, making use of the Event callback + + + + + Opens a WaveOut device + + + + + Initialises the WaveOut device + + WaveProvider to play + + + + Start playing the audio from the WaveStream + + + + + Pause the audio + + + + + Resume playing after a pause from the same position + + + + + Stop and reset the WaveOut device + + + + + Gets the current position in bytes from the wave output device. + (n.b. this is not the same thing as the position within your reader + stream - it calls directly into waveOutGetPosition) + + Position in bytes + + + + Closes this WaveOut device + + + + + Closes the WaveOut device and disposes of buffers + + True if called from Dispose + + + + Finalizer. Only called when user forgets to call Dispose + + + + + Indicates playback has stopped automatically + + + + + Gets or sets the desired latency in milliseconds + Should be set before a call to Init + + + + + Gets or sets the number of buffers used + Should be set before a call to Init + + + + + Gets or sets the device number + Should be set before a call to Init + This must be between 0 and DeviceCount - 1. + + + + + Gets a instance indicating the format the hardware is using. + + + + + Playback State + + + + + Obsolete property + + + + + Simple SampleProvider that passes through audio unchanged and raises + an event every n samples with the maximum sample value from the period + for metering purposes + + + + + Initialises a new instance of MeteringSampleProvider that raises 10 stream volume + events per second + + Source sample provider + + + + Initialises a new instance of MeteringSampleProvider + + source sampler provider + Number of samples between notifications + + + + Reads samples from this Sample Provider + + Sample buffer + Offset into sample buffer + Number of samples required + Number of samples read + + + + Number of Samples per notification + + + + + Raised periodically to inform the user of the max volume + + + + + The WaveFormat of this sample provider + + + + + Event args for aggregated stream volume + + + + + Max sample values array (one for each channel) + + + + + Simple class that raises an event on every sample + + + + + An interface for WaveStreams which can report notification of individual samples + + + + + A sample has been detected + + + + + Initializes a new instance of NotifyingSampleProvider + + Source Sample Provider + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples desired + Number of samples read + + + + WaveFormat + + + + + Sample notifier + + + + + Very simple sample provider supporting adjustable gain + + + + + Initializes a new instance of VolumeSampleProvider + + Source Sample Provider + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples desired + Number of samples read + + + + WaveFormat + + + + + Allows adjusting the volume, 1.0f = full volume + + + + + Helper class for when you need to convert back to an IWaveProvider from + an ISampleProvider. Keeps it as IEEE float + + + + + Initializes a new instance of the WaveProviderFloatToWaveProvider class + + Source wave provider + + + + Reads from this provider + + + + + The waveformat of this WaveProvider (same as the source) + + + + + Provides a buffered store of samples + Read method will return queued samples or fill buffer with zeroes + Now backed by a circular buffer + + + + + Creates a new buffered WaveProvider + + WaveFormat + + + + Adds samples. Takes a copy of buffer, so that buffer can be reused if necessary + + + + + Reads from this WaveProvider + Will always return count bytes, since we will zero-fill the buffer if not enough available + + + + + Discards all audio from the buffer + + + + + Buffer length in bytes + + + + + Buffer duration + + + + + If true, when the buffer is full, start throwing away data + if false, AddSamples will throw an exception when buffer is full + + + + + The number of buffered bytes + + + + + Buffered Duration + + + + + Gets the WaveFormat + + + + + No nonsense mono to stereo provider, no volume adjustment, + just copies input to left and right. + + + + + Initializes a new instance of MonoToStereoSampleProvider + + Source sample provider + + + + Reads samples from this provider + + Sample buffer + Offset into sample buffer + Number of samples required + Number of samples read + + + + WaveFormat of this provider + + + + + The Media Foundation Resampler Transform + + + + + An abstract base class for simplifying working with Media Foundation Transforms + You need to override the method that actually creates and configures the transform + + + + + The Source Provider + + + + + The Output WaveFormat + + + + + Constructs a new MediaFoundationTransform wrapper + Will read one second at a time + + The source provider for input data to the transform + The desired output format + + + + To be implemented by overriding classes. Create the transform object, set up its input and output types, + and configure any custom properties in here + + An object implementing IMFTrasform + + + + Disposes this MediaFoundation transform + + + + + Disposes this Media Foundation Transform + + + + + Destructor + + + + + Reads data out of the source, passing it through the transform + + Output buffer + Offset within buffer to write to + Desired byte count + Number of bytes read + + + + Attempts to read from the transform + Some useful info here: + http://msdn.microsoft.com/en-gb/library/windows/desktop/aa965264%28v=vs.85%29.aspx#process_data + + + + + + Indicate that the source has been repositioned and completely drain out the transforms buffers + + + + + The output WaveFormat of this Media Foundation Transform + + + + + Creates the Media Foundation Resampler, allowing modifying of sample rate, bit depth and channel count + + Source provider, must be PCM + Output format, must also be PCM + + + + Creates a resampler with a specified target output sample rate + + Source provider + Output sample rate + + + + Creates and configures the actual Resampler transform + + A newly created and configured resampler MFT + + + + Disposes this resampler + + + + + Gets or sets the Resampler quality. n.b. set the quality before starting to resample. + 1 is lowest quality (linear interpolation) and 60 is best quality + + + + + WaveProvider that can mix together multiple 32 bit floating point input provider + All channels must have the same number of inputs and same sample rate + n.b. Work in Progress - not tested yet + + + + + Creates a new MixingWaveProvider32 + + + + + Creates a new 32 bit MixingWaveProvider32 + + inputs - must all have the same format. + Thrown if the input streams are not 32 bit floating point, + or if they have different formats to each other + + + + Add a new input to the mixer + + The wave input to add + + + + Remove an input from the mixer + + waveProvider to remove + + + + Reads bytes from this wave stream + + buffer to read into + offset into buffer + number of bytes required + Number of bytes read. + Thrown if an invalid number of bytes requested + + + + Actually performs the mixing + + + + + The number of inputs to this mixer + + + + + + + + + + Allows any number of inputs to be patched to outputs + Uses could include swapping left and right channels, turning mono into stereo, + feeding different input sources to different soundcard outputs etc + + + + + Creates a multiplexing wave provider, allowing re-patching of input channels to different + output channels + + Input wave providers. Must all be of the same format, but can have any number of channels + Desired number of output channels. + + + + persistent temporary buffer to prevent creating work for garbage collector + + + + + Reads data from this WaveProvider + + Buffer to be filled with sample data + Offset to write to within buffer, usually 0 + Number of bytes required + Number of bytes read + + + + Connects a specified input channel to an output channel + + Input Channel index (zero based). Must be less than InputChannelCount + Output Channel index (zero based). Must be less than OutputChannelCount + + + + The WaveFormat of this WaveProvider + + + + + The number of input channels. Note that this is not the same as the number of input wave providers. If you pass in + one stereo and one mono input provider, the number of input channels is three. + + + + + The number of output channels, as specified in the constructor. + + + + + Takes a stereo 16 bit input and turns it mono, allowing you to select left or right channel only or mix them together + + + + + Creates a new mono waveprovider based on a stereo input + + Stereo 16 bit PCM input + + + + Reads bytes from this WaveProvider + + + + + 1.0 to mix the mono source entirely to the left channel + + + + + 1.0 to mix the mono source entirely to the right channel + + + + + Output Wave Format + + + + + Converts from mono to stereo, allowing freedom to route all, some, or none of the incoming signal to left or right channels + + + + + Creates a new stereo waveprovider based on a mono input + + Mono 16 bit PCM input + + + + Reads bytes from this WaveProvider + + + + + 1.0 to copy the mono stream to the left channel without adjusting volume + + + + + 1.0 to copy the mono stream to the right channel without adjusting volume + + + + + Output Wave Format + + + + + Helper class allowing us to modify the volume of a 16 bit stream without converting to IEEE float + + + + + Constructs a new VolumeWaveProvider16 + + Source provider, must be 16 bit PCM + + + + Read bytes from this WaveProvider + + Buffer to read into + Offset within buffer to read to + Bytes desired + Bytes read + + + + Gets or sets volume. + 1.0 is full scale, 0.0 is silence, anything over 1.0 will amplify but potentially clip + + + + + WaveFormat of this WaveProvider + + + + + Converts IEEE float to 16 bit PCM, optionally clipping and adjusting volume along the way + + + + + Creates a new WaveFloatTo16Provider + + the source provider + + + + Reads bytes from this wave stream + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + + + + + + Volume of this channel. 1.0 = full scale + + + + + Converts 16 bit PCM to IEEE float, optionally adjusting volume along the way + + + + + Creates a new Wave16toFloatProvider + + the source provider + + + + Reads bytes from this wave stream + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + + + + + + Volume of this channel. 1.0 = full scale + + + + + Buffered WaveProvider taking source data from WaveIn + + + + + Creates a new WaveInProvider + n.b. Should make sure the WaveFormat is set correctly on IWaveIn before calling + + The source of wave data + + + + Reads data from the WaveInProvider + + + + + The WaveFormat + + + + + Base class for creating a 16 bit wave provider + + + + + Initializes a new instance of the WaveProvider16 class + defaulting to 44.1kHz mono + + + + + Initializes a new instance of the WaveProvider16 class with the specified + sample rate and number of channels + + + + + Allows you to specify the sample rate and channels for this WaveProvider + (should be initialised before you pass it to a wave player) + + + + + Implements the Read method of IWaveProvider by delegating to the abstract + Read method taking a short array + + + + + Method to override in derived classes + Supply the requested number of samples into the buffer + + + + + The Wave Format + + + + + Base class for creating a 32 bit floating point wave provider + Can also be used as a base class for an ISampleProvider that can + be plugged straight into anything requiring an IWaveProvider + + + + + Initializes a new instance of the WaveProvider32 class + defaulting to 44.1kHz mono + + + + + Initializes a new instance of the WaveProvider32 class with the specified + sample rate and number of channels + + + + + Allows you to specify the sample rate and channels for this WaveProvider + (should be initialised before you pass it to a wave player) + + + + + Implements the Read method of IWaveProvider by delegating to the abstract + Read method taking a float array + + + + + Method to override in derived classes + Supply the requested number of samples into the buffer + + + + + The Wave Format + + + + + Helper class turning an already 32 bit floating point IWaveProvider + into an ISampleProvider - hopefully not needed for most applications + + + + + Initializes a new instance of the WaveToSampleProvider class + + Source wave provider, must be IEEE float + + + + Reads from this provider + + + + + Utility class to intercept audio from an IWaveProvider and + save it to disk + + + + + Constructs a new WaveRecorder + + The location to write the WAV file to + The Source Wave Provider + + + + Read simply returns what the source returns, but writes to disk along the way + + + + + Closes the WAV file + + + + + The WaveFormat + + + + A read-only stream of AIFF data based on an aiff file + with an associated WaveFormat + originally contributed to NAudio by Giawa + + + + + Base class for all WaveStream classes. Derives from stream. + + + + + Flush does not need to do anything + See + + + + + An alternative way of repositioning. + See + + + + + Sets the length of the WaveStream. Not Supported. + + + + + + Writes to the WaveStream. Not Supported. + + + + + Moves forward or backwards the specified number of seconds in the stream + + Number of seconds to move, can be negative + + + + Whether the WaveStream has non-zero sample data at the current position for the + specified count + + Number of bytes to read + + + + Retrieves the WaveFormat for this stream + + + + + We can read from this stream + + + + + We can seek within this stream + + + + + We can't write to this stream + + + + + The block alignment for this wavestream. Do not modify the Position + to anything that is not a whole multiple of this value + + + + + The current position in the stream in Time format + + + + + Total length in real-time of the stream (may be an estimate for compressed files) + + + + Supports opening a AIF file + The AIF is of similar nastiness to the WAV format. + This supports basic reading of uncompressed PCM AIF files, + with 8, 16, 24 and 32 bit PCM data. + + + + + Creates an Aiff File Reader based on an input stream + + The input stream containing a AIF file including header + + + + Ensures valid AIFF header and then finds data offset. + + The stream, positioned at the start of audio data + The format found + The position of the data chunk + The length of the data chunk + Additional chunks found + + + + Cleans up the resources associated with this AiffFileReader + + + + + Reads bytes from the AIFF File + + + + + + + + + + + + + + + + Number of Samples (if possible to calculate) + + + + + Position in the AIFF file + + + + + + AIFF Chunk + + + + + Chunk Name + + + + + Chunk Length + + + + + Chunk start + + + + + Creates a new AIFF Chunk + + + + + AudioFileReader simplifies opening an audio file in NAudio + Simply pass in the filename, and it will attempt to open the + file and set up a conversion path that turns into PCM IEEE float. + ACM codecs will be used for conversion. + It provides a volume property and implements both WaveStream and + ISampleProvider, making it possibly the only stage in your audio + pipeline necessary for simple playback scenarios + + + + + Initializes a new instance of AudioFileReader + + The file to open + + + + Creates the reader stream, supporting all filetypes in the core NAudio library, + and ensuring we are in PCM format + + File Name + + + + Reads from this wave stream + + Audio buffer + Offset into buffer + Number of bytes required + Number of bytes read + + + + Reads audio from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples required + Number of samples read + + + + Helper to convert source to dest bytes + + + + + Helper to convert dest to source bytes + + + + + Disposes this AudioFileReader + + True if called from Dispose + + + + WaveFormat of this stream + + + + + Length of this stream (in bytes) + + + + + Position of this stream (in bytes) + + + + + Gets or Sets the Volume of this AudioFileReader. 1.0f is full volume + + + + + Helper stream that lets us read from compressed audio files with large block alignment + as though we could read any amount and reposition anywhere + + + + + Creates a new BlockAlignReductionStream + + the input stream + + + + Disposes this WaveStream + + + + + Reads data from this stream + + + + + + + + + Block alignment of this stream + + + + + Wave Format + + + + + Length of this Stream + + + + + Current position within stream + + + + + Holds information on a cue: a labeled position within a Wave file + + + + + Creates a Cue based on a sample position and label + + + + + + + Cue position in samples + + + + + Label of the cue + + + + + Holds a list of cues + + + The specs for reading and writing cues from the cue and list RIFF chunks + are from http://www.sonicspot.com/guide/wavefiles.html and http://www.wotsit.org/ + ------------------------------ + The cues are stored like this: + ------------------------------ + struct CuePoint + { + Int32 dwIdentifier; + Int32 dwPosition; + Int32 fccChunk; + Int32 dwChunkStart; + Int32 dwBlockStart; + Int32 dwSampleOffset; + } + + struct CueChunk + { + Int32 chunkID; + Int32 chunkSize; + Int32 dwCuePoints; + CuePoint[] points; + } + ------------------------------ + Labels look like this: + ------------------------------ + struct ListHeader + { + Int32 listID; /* 'list' */ + Int32 chunkSize; /* includes the Type ID below */ + Int32 typeID; /* 'adtl' */ + } + + struct LabelChunk + { + Int32 chunkID; + Int32 chunkSize; + Int32 dwIdentifier; + Char[] dwText; /* Encoded with extended ASCII */ + } LabelChunk; + + + + + Creates an empty cue list + + + + + Adds an item to the list + + Cue + + + + Creates a cue list from the cue RIFF chunk and the list RIFF chunk + + The data contained in the cue chunk + The data contained in the list chunk + + + + Gets the cues as the concatenated cue and list RIFF chunks. + + RIFF chunks containing the cue data + + + + Checks if the cue and list chunks exist and if so, creates a cue list + + + + + Gets sample positions for the embedded cues + + Array containing the cue positions + + + + Gets labels for the embedded cues + + Array containing the labels + + + + Number of cues + + + + + Accesses the cue at the specified index + + + + + + + A wave file reader supporting cue reading + + + + This class supports the reading of WAV files, + providing a repositionable WaveStream that returns the raw data + contained in the WAV file + + + + Supports opening a WAV file + The WAV file format is a real mess, but we will only + support the basic WAV file format which actually covers the vast + majority of WAV files out there. For more WAV file format information + visit www.wotsit.org. If you have a WAV file that can't be read by + this class, email it to the NAudio project and we will probably + fix this reader to support it + + + + + Creates a Wave File Reader based on an input stream + + The input stream containing a WAV file including header + + + + Gets the data for the specified chunk + + + + + Cleans up the resources associated with this WaveFileReader + + + + + Reads bytes from the Wave File + + + + + + Attempts to read the next sample or group of samples as floating point normalised into the range -1.0f to 1.0f + + An array of samples, 1 for mono, 2 for stereo etc. Null indicates end of file reached + + + + + Attempts to read a sample into a float. n.b. only applicable for uncompressed formats + Will normalise the value read into the range -1.0f to 1.0f if it comes from a PCM encoding + + False if the end of the WAV data chunk was reached + + + + Gets a list of the additional chunks found in this file + + + + + + + + + + This is the length of audio data contained in this WAV file, in bytes + (i.e. the byte length of the data chunk, not the length of the WAV file itself) + + + + + + Number of Samples (if possible to calculate) + This currently does not take into account number of channels, so + divide again by number of channels if you want the number of + audio 'frames' + + + + + Position in the WAV data chunk. + + + + + + Loads a wavefile and supports reading cues + + + + + + Cue List (can be null if cues not present) + + + + + Sample event arguments + + + + + Constructor + + + + + Left sample + + + + + Right sample + + + + + Class for reading any file that Media Foundation can play + Will only work in Windows Vista and above + Automatically converts to PCM + If it is a video file with multiple audio streams, it will pick out the first audio stream + + + + + Creates a new MediaFoundationReader based on the supplied file + + Filename (can also be a URL e.g. http:// mms:// file://) + + + + Creates a new MediaFoundationReader based on the supplied file + + Filename + Advanced settings + + + + Creates the reader (overridable by ) + + + + + Reads from this wave stream + + Buffer to read into + Offset in buffer + Bytes required + Number of bytes read; 0 indicates end of stream + + + + Cleans up after finishing with this reader + + true if called from Dispose + + + + WaveFormat of this stream (n.b. this is after converting to PCM) + + + + + The bytesRequired of this stream in bytes (n.b may not be accurate) + + + + + Current position within this stream + + + + + WaveFormat has changed + + + + + Allows customisation of this reader class + + + + + Sets up the default settings for MediaFoundationReader + + + + + Allows us to request IEEE float output (n.b. no guarantee this will be accepted) + + + + + If true, the reader object created in the constructor is used in Read + Should only be set to true if you are working entirely on an STA thread, or + entirely with MTA threads. + + + + + If true, the reposition does not happen immediately, but waits until the + next call to read to be processed. + + + + + Class for reading from MP3 files + + + + Supports opening a MP3 file + + + Supports opening a MP3 file + MP3 File name + Factory method to build a frame decompressor + + + + Opens MP3 from a stream rather than a file + Will not dispose of this stream itself + + The incoming stream containing MP3 data + + + + Opens MP3 from a stream rather than a file + Will not dispose of this stream itself + + The incoming stream containing MP3 data + Factory method to build a frame decompressor + + + + Creates an ACM MP3 Frame decompressor. This is the default with NAudio + + A WaveFormat object based + + + + + Gets the total length of this file in milliseconds. + + + + + Reads the next mp3 frame + + Next mp3 frame, or null if EOF + + + + Reads the next mp3 frame + + Next mp3 frame, or null if EOF + + + + Reads decompressed PCM data from our MP3 file. + + + + + Disposes this WaveStream + + + + + The MP3 wave format (n.b. NOT the output format of this stream - see the WaveFormat property) + + + + + ID3v2 tag if present + + + + + ID3v1 tag if present + + + + + This is the length in bytes of data available to be read out from the Read method + (i.e. the decompressed MP3 length) + n.b. this may return 0 for files whose length is unknown + + + + + + + + + + + + + + + Xing header if present + + + + + Function that can create an MP3 Frame decompressor + + A WaveFormat object describing the MP3 file format + An MP3 Frame decompressor + + + + Converts an IWaveProvider containing 16 bit PCM to an + ISampleProvider + + + + + Initialises a new instance of Pcm16BitToSampleProvider + + Source wave provider + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Samples required + Number of samples read + + + + Converts an IWaveProvider containing 24 bit PCM to an + ISampleProvider + + + + + Initialises a new instance of Pcm24BitToSampleProvider + + Source Wave Provider + + + + Reads floating point samples from this sample provider + + sample buffer + offset within sample buffer to write to + number of samples required + number of samples provided + + + + Converts an IWaveProvider containing 8 bit PCM to an + ISampleProvider + + + + + Initialises a new instance of Pcm8BitToSampleProvider + + Source wave provider + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples to read + Number of samples read + + + + WaveStream that simply passes on data from its source stream + (e.g. a MemoryStream) + + + + + Initialises a new instance of RawSourceWaveStream + + The source stream containing raw audio + The waveformat of the audio in the source stream + + + + Reads data from the stream + + + + + The WaveFormat of this stream + + + + + The length in bytes of this stream (if supported) + + + + + The current position in this stream + + + + + Wave Stream for converting between sample rates + + + + + WaveStream to resample using the DMO Resampler + + Input Stream + Desired Output Format + + + + Reads data from input stream + + buffer + offset into buffer + Bytes required + Number of bytes read + + + + Dispose + + True if disposing (not from finalizer) + + + + Stream Wave Format + + + + + Stream length in bytes + + + + + Stream position in bytes + + + + + Holds information about a RIFF file chunk + + + + + Creates a RiffChunk object + + + + + The chunk identifier + + + + + The chunk identifier converted to a string + + + + + The chunk length + + + + + The stream position this chunk is located at + + + + + A simple compressor + + + + + Create a new simple compressor stream + + Source stream + + + + Determine whether the stream has the required amount of data. + + Number of bytes of data required from the stream. + Flag indicating whether the required amount of data is avialable. + + + + Reads bytes from this stream + + Buffer to read into + Offset in array to read into + Number of bytes to read + Number of bytes read + + + + Disposes this stream + + true if the user called this + + + + Make-up Gain + + + + + Threshold + + + + + Ratio + + + + + Attack time + + + + + Release time + + + + + Turns gain on or off + + + + + Returns the stream length + + + + + Gets or sets the current position in the stream + + + + + Gets the WaveFormat of this stream + + + + + Gets the block alignment for this stream + + + + + WaveStream that converts 32 bit audio back down to 16 bit, clipping if necessary + + + + + Creates a new Wave32To16Stream + + the source stream + + + + Reads bytes from this wave stream + + Destination buffer + Offset into destination buffer + + Number of bytes read. + + + + Conversion to 16 bit and clipping + + + + + Disposes this WaveStream + + + + + Sets the volume for this stream. 1.0f is full scale + + + + + + + + + + Returns the stream length + + + + + Gets or sets the current position in the stream + + + + + + + + + + Clip indicator. Can be reset. + + + + + Represents Channel for the WaveMixerStream + 32 bit output and 16 bit input + It's output is always stereo + The input stream can be panned + + + + + Creates a new WaveChannel32 + + the source stream + stream volume (1 is 0dB) + pan control (-1 to 1) + + + + Creates a WaveChannel32 with default settings + + The source stream + + + + Reads bytes from this wave stream + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + Determines whether this channel has any data to play + to allow optimisation to not read, but bump position forward + + + + + Disposes this WaveStream + + + + + Raise the sample event (no check for null because it has already been done) + + + + + Gets the block alignment for this WaveStream + + + + + Returns the stream length + + + + + Gets or sets the current position in the stream + + + + + If true, Read always returns the number of bytes requested + + + + + + + + + + Volume of this channel. 1.0 = full scale + + + + + Pan of this channel (from -1 to 1) + + + + + Sample + + + + + Utility class that takes an IWaveProvider input at any bit depth + and exposes it as an ISampleProvider. Can turn mono inputs into stereo, + and allows adjusting of volume + (The eventual successor to WaveChannel32) + This class also serves as an example of how you can link together several simple + Sample Providers to form a more useful class. + + + + + Initialises a new instance of SampleChannel + + Source wave provider, must be PCM or IEEE + + + + Initialises a new instance of SampleChannel + + Source wave provider, must be PCM or IEEE + force mono inputs to become stereo + + + + Reads samples from this sample provider + + Sample buffer + Offset into sample buffer + Number of samples desired + Number of samples read + + + + The WaveFormat of this Sample Provider + + + + + Allows adjusting the volume, 1.0f = full volume + + + + + Raised periodically to inform the user of the max volume + (before the volume meter) + + + + + WaveStream that passes through an ACM Codec + + + + + Creates a stream that can convert to PCM + + The source stream + A PCM stream + + + + Create a new WaveFormat conversion stream + + Desired output format + Source stream + + + + Converts source bytes to destination bytes + + + + + Converts destination bytes to source bytes + + + + + Reads bytes from this stream + + Buffer to read into + Offset in buffer to read into + Number of bytes to read + Number of bytes read + + + + Disposes this stream + + true if the user called this + + + + Returns the stream length + + + + + Gets or sets the current position in the stream + + + + + Gets the WaveFormat of this stream + + + + + A buffer of Wave samples + + + + + creates a new wavebuffer + + WaveIn device to write to + Buffer size in bytes + + + + Place this buffer back to record more audio + + + + + Finalizer for this wave buffer + + + + + Releases resources held by this WaveBuffer + + + + + Releases resources held by this WaveBuffer + + + + + Provides access to the actual record buffer (for reading only) + + + + + Indicates whether the Done flag is set on this buffer + + + + + Indicates whether the InQueue flag is set on this buffer + + + + + Number of bytes recorded + + + + + The buffer size in bytes + + + + + WaveStream that can mix together multiple 32 bit input streams + (Normally used with stereo input channels) + All channels must have the same number of inputs + + + + + Creates a new 32 bit WaveMixerStream + + + + + Creates a new 32 bit WaveMixerStream + + An Array of WaveStreams - must all have the same format. + Use WaveChannel is designed for this purpose. + Automatically stop when all inputs have been read + Thrown if the input streams are not 32 bit floating point, + or if they have different formats to each other + + + + Add a new input to the mixer + + The wave input to add + + + + Remove a WaveStream from the mixer + + waveStream to remove + + + + Reads bytes from this wave stream + + buffer to read into + offset into buffer + number of bytes required + Number of bytes read. + Thrown if an invalid number of bytes requested + + + + Actually performs the mixing + + + + + Disposes this WaveStream + + + + + The number of inputs to this mixer + + + + + Automatically stop when all inputs have been read + + + + + + + + + + Length of this Wave Stream (in bytes) + + + + + + Position within this Wave Stream (in bytes) + + + + + + + + + + + Simply shifts the input stream in time, optionally + clipping its start and end. + (n.b. may include looping in the future) + + + + + Creates a new WaveOffsetStream + + the source stream + the time at which we should start reading from the source stream + amount to trim off the front of the source stream + length of time to play from source stream + + + + Creates a WaveOffsetStream with default settings (no offset or pre-delay, + and whole length of source stream) + + The source stream + + + + Reads bytes from this wave stream + + The destination buffer + Offset into the destination buffer + Number of bytes read + Number of bytes read. + + + + Determines whether this channel has any data to play + to allow optimisation to not read, but bump position forward + + + + + Disposes this WaveStream + + + + + The length of time before which no audio will be played + + + + + An offset into the source stream from which to start playing + + + + + Length of time to read from the source stream + + + + + Gets the block alignment for this WaveStream + + + + + Returns the stream length + + + + + Gets or sets the current position in the stream + + + + + + + + + + A buffer of Wave samples for streaming to a Wave Output device + + + + + creates a new wavebuffer + + WaveOut device to write to + Buffer size in bytes + Stream to provide more data + Lock to protect WaveOut API's from being called on >1 thread + + + + Finalizer for this wave buffer + + + + + Releases resources held by this WaveBuffer + + + + + Releases resources held by this WaveBuffer + + + + this is called by the WAVE callback and should be used to refill the buffer + + + + Whether the header's in queue flag is set + + + + + The buffer size in bytes + + + + + DMO Input Data Buffer Flags + + + + + None + + + + + DMO_INPUT_DATA_BUFFERF_SYNCPOINT + + + + + DMO_INPUT_DATA_BUFFERF_TIME + + + + + DMO_INPUT_DATA_BUFFERF_TIMELENGTH + + + + + http://msdn.microsoft.com/en-us/library/aa929922.aspx + DMO_MEDIA_TYPE + + + + + Gets the structure as a Wave format (if it is one) + + + + + Sets this object up to point to a wave format + + Wave format structure + + + + Major type + + + + + Major type name + + + + + Subtype + + + + + Subtype name + + + + + Fixed size samples + + + + + Sample size + + + + + Format type + + + + + Format type name + + + + + DMO Output Data Buffer + + + + + Creates a new DMO Output Data Buffer structure + + Maximum buffer size + + + + Dispose + + + + + Retrives the data in this buffer + + Buffer to receive data + Offset into buffer + + + + Media Buffer + + + + + Length of data in buffer + + + + + Status Flags + + + + + Timestamp + + + + + Duration + + + + + Is more data available + If true, ProcessOuput should be called again + + + + + DMO Output Data Buffer Flags + + + + + None + + + + + DMO_OUTPUT_DATA_BUFFERF_SYNCPOINT + + + + + DMO_OUTPUT_DATA_BUFFERF_TIME + + + + + DMO_OUTPUT_DATA_BUFFERF_TIMELENGTH + + + + + DMO_OUTPUT_DATA_BUFFERF_INCOMPLETE + + + + + DMO Process Output Flags + + + + + None + + + + + DMO_PROCESS_OUTPUT_DISCARD_WHEN_NO_BUFFER + + + + + defined in mediaobj.h + + + + + From wmcodecsdp.h + Implements: + - IMediaObject + - IMFTransform (Media foundation - we will leave this for now as there is loads of MF stuff) + - IPropertyStore + - IWMResamplerProps + Can resample PCM or IEEE + + + + + DMO Resampler + + + + + Creates a new Resampler based on the DMO Resampler + + + + + Dispose code - experimental at the moment + Was added trying to track down why Resampler crashes NUnit + This code not currently being called by ResamplerDmoStream + + + + + Media Object + + + + diff --git a/MusicPlayer/NAudio/license.txt b/MusicPlayer/NAudio/license.txt new file mode 100644 index 0000000..622a544 --- /dev/null +++ b/MusicPlayer/NAudio/license.txt @@ -0,0 +1,31 @@ +Microsoft Public License (Ms-PL) + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. + +1. Definitions + +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. + +A "contributor" is any person that distributes its contribution under this license. + +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights + +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations + +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. \ No newline at end of file diff --git a/MusicPlayer/NAudio/readme.txt b/MusicPlayer/NAudio/readme.txt new file mode 100644 index 0000000..141a392 --- /dev/null +++ b/MusicPlayer/NAudio/readme.txt @@ -0,0 +1,86 @@ +NAudio is an open source .NET audio library written by Mark Heath (mark.heath@gmail.com) +For more information, visit http://naudio.codeplex.com + +THANKS +====== +The following list includes some of the people who have contributed in various ways to NAudio, such as code contributions, +bug fixes, documentation, helping out on the forums and even donations. I haven't finished compiling this list yet, so +if your name should be on it but isn't please let me know and I will include it. Also, some people I only know by their forum +id, so if you want me to put your full name here, please also get in touch. + +in alphabetical order: +Alan Jordan +Alexandre Mutel +Alexander Binkert +AmandaTarafaMas +balistof +biermeester +borman11 +bradb +Brandon Hansen (kg6ypi) +csechet +ChunkWare Music Software +CKing +DaMacc +Du10 +eejake52 +Florian Rosmann (filoe) +Giawa +Harald Petrovitsch +Hfuy +Iain McCowan +Idael Cardaso +ioctlLR +Jamie Michael Ewins +jannera +jbaker8935 +jcameron23 +JoeGaggler +jonahoffmann +jontdelorme +Justin Frankel +K24A3 +Kassoul +kevinxxx +kzych +LionCash +Lustild +Lucian Wischik (ljw1004) +ManuN +MeelMarcel +Michael Chadwick +Michael Feld +Michael J +Michael Lehenbauer +milligan22963 +myrkle +nelsonkidd +Nigel Redmon +Nikolaos Georgiou +Owen Skriloff +owoudenb +painmailer +PPavan +Pygmy +Ray Molenkamp +Roadz +Robert Bristow-Johnson +Scott Fleischman +Simon Clark +Sirish Bajpai +sporn +Steve Underwood +Ted Murphy +Tiny Simple Tools +Tobias Fleming +TomBogle +Tony Cabello +Tony Sistemas +TuneBlade +topher3683 +volmart +Vladimir Rokovanov +Ville Koskinen +Wyatt Rice +Yuval Naveh +Zsb diff --git a/MusicPlayer/NAudioPlayer.cs b/MusicPlayer/NAudioPlayer.cs new file mode 100644 index 0000000..c5b625e --- /dev/null +++ b/MusicPlayer/NAudioPlayer.cs @@ -0,0 +1,7 @@ +namespace MusicPlayer +{ + internal class NAudioPlayer : AudioPlayer + { + + } +} \ No newline at end of file diff --git a/MusicPlayer/Properties/AssemblyInfo.cs b/MusicPlayer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..fe5a75e --- /dev/null +++ b/MusicPlayer/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MusicPlayer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MusicPlayer")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("573a1557-c916-4abe-bd52-94760d19cc29")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MusicPlayer/SoundTouch/SoundTouch.cs b/MusicPlayer/SoundTouch/SoundTouch.cs new file mode 100644 index 0000000..cceed79 --- /dev/null +++ b/MusicPlayer/SoundTouch/SoundTouch.cs @@ -0,0 +1,194 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Collection_Editor.code.Modules.MusicPlayer.SoundTouch; + +namespace VarispeedDemo.SoundTouch +{ + class SoundTouch : IDisposable + { + private IntPtr handle; + private string versionString; + private readonly bool is64Bit; + private IntPtr ptr; + + public SoundTouch() + { + is64Bit = Marshal.SizeOf(ptr) == 8; + + handle = is64Bit ? SoundTouchInterop64.soundtouch_createInstance() : + SoundTouchInterop32.soundtouch_createInstance(); + } + + public string VersionString + { + get + { + if (versionString == null) + { + var s = new StringBuilder(100); + if (is64Bit) + SoundTouchInterop64.soundtouch_getVersionString2(s, s.Capacity); + else + SoundTouchInterop32.soundtouch_getVersionString2(s, s.Capacity); + versionString = s.ToString(); + } + return versionString; + } + } + + public void SetPitchOctaves(float pitchOctaves) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setPitchOctaves(handle, pitchOctaves); + else + SoundTouchInterop32.soundtouch_setPitchOctaves(handle, pitchOctaves); + } + + public void SetSampleRate(int sampleRate) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setSampleRate(handle, (uint) sampleRate); + else + SoundTouchInterop32.soundtouch_setSampleRate(handle, (uint)sampleRate); + } + + public void SetChannels(int channels) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setChannels(handle, (uint) channels); + else + SoundTouchInterop32.soundtouch_setChannels(handle, (uint)channels); + } + + private void DestroyInstance() + { + if (handle != IntPtr.Zero) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_destroyInstance(handle); + else + SoundTouchInterop32.soundtouch_destroyInstance(handle); + handle = IntPtr.Zero; + } + } + + public void Dispose() + { + DestroyInstance(); + GC.SuppressFinalize(this); + } + + ~SoundTouch() + { + DestroyInstance(); + } + + public void PutSamples(float[] samples, int numSamples) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_putSamples(handle, samples, numSamples); + else + SoundTouchInterop32.soundtouch_putSamples(handle, samples, numSamples); + } + + public int ReceiveSamples(float[] outBuffer, int maxSamples) + { + if (is64Bit) + return (int)SoundTouchInterop64.soundtouch_receiveSamples(handle, outBuffer, (uint)maxSamples); + return (int)SoundTouchInterop32.soundtouch_receiveSamples(handle, outBuffer, (uint)maxSamples); + } + + public bool IsEmpty + { + get + { + if (is64Bit) + return SoundTouchInterop64.soundtouch_isEmpty(handle) != 0; + return SoundTouchInterop32.soundtouch_isEmpty(handle) != 0; + } + } + + public int NumberOfSamplesAvailable + { + get + { + if (is64Bit) + return (int)SoundTouchInterop64.soundtouch_numSamples(handle); + return (int)SoundTouchInterop32.soundtouch_numSamples(handle); + } + } + + public int NumberOfUnprocessedSamples + { + get + { + if (is64Bit) + return SoundTouchInterop64.soundtouch_numUnprocessedSamples(handle); + return SoundTouchInterop32.soundtouch_numUnprocessedSamples(handle); + } + } + + public void Flush() + { + if (is64Bit) + SoundTouchInterop64.soundtouch_flush(handle); + else + SoundTouchInterop32.soundtouch_flush(handle); + } + + public void Clear() + { + if (is64Bit) + SoundTouchInterop64.soundtouch_clear(handle); + else + SoundTouchInterop32.soundtouch_clear(handle); + } + + public void SetRate(float newRate) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setRate(handle, newRate); + else + SoundTouchInterop32.soundtouch_setRate(handle, newRate); + } + + public void SetTempo(float newTempo) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setTempo(handle, newTempo); + else + SoundTouchInterop32.soundtouch_setTempo(handle, newTempo); + } + + public int GetUseAntiAliasing() + { + if (is64Bit) + return SoundTouchInterop64.soundtouch_getSetting(handle, SoundTouchSettings.UseAaFilter); + return SoundTouchInterop32.soundtouch_getSetting(handle, SoundTouchSettings.UseAaFilter); + } + + public void SetUseAntiAliasing(bool useAntiAliasing) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setSetting(handle, SoundTouchSettings.UseAaFilter, useAntiAliasing ? 1 : 0); + else + SoundTouchInterop32.soundtouch_setSetting(handle, SoundTouchSettings.UseAaFilter, useAntiAliasing ? 1 : 0); + } + + public void SetUseQuickSeek(bool useQuickSeek) + { + if (is64Bit) + SoundTouchInterop64.soundtouch_setSetting(handle, SoundTouchSettings.UseQuickSeek, useQuickSeek ? 1 : 0); + else + SoundTouchInterop32.soundtouch_setSetting(handle, SoundTouchSettings.UseQuickSeek, useQuickSeek ? 1 : 0); + } + + public int GetUseQuickSeek() + { + if (is64Bit) + return SoundTouchInterop64.soundtouch_getSetting(handle, SoundTouchSettings.UseQuickSeek); + return SoundTouchInterop32.soundtouch_getSetting(handle, SoundTouchSettings.UseQuickSeek); + } + } +} diff --git a/MusicPlayer/SoundTouch/SoundTouchInterop32.cs b/MusicPlayer/SoundTouch/SoundTouchInterop32.cs new file mode 100644 index 0000000..48edc30 --- /dev/null +++ b/MusicPlayer/SoundTouch/SoundTouchInterop32.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Collection_Editor.code.Modules.MusicPlayer.SoundTouch +{ + class SoundTouchInterop32 + { + private const string SoundTouchDllName = "SoundTouch.dll"; + + /// + /// Create a new instance of SoundTouch processor. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr soundtouch_createInstance(); + + /// + /// Destroys a SoundTouch processor instance. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_destroyInstance(IntPtr h); + + /// + /// Get SoundTouch library version string - alternative function for + /// environments that can't properly handle character string as return value + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_getVersionString2(StringBuilder versionString, int bufferSize); + + /// + /// Get SoundTouch library version Id + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint soundtouch_getVersionId(); + + /// + /// Sets new rate control value. Normal rate = 1.0, smaller values + /// represent slower rate, larger faster rates. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setRate(IntPtr h, float newRate); + + /// + /// Sets new tempo control value. Normal tempo = 1.0, smaller values + /// represent slower tempo, larger faster tempo. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setTempo(IntPtr h, float newTempo); + + /// + /// Sets new rate control value as a difference in percents compared + /// to the original rate (-50 .. +100 %); + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setRateChange(IntPtr h, float newRate); + + /// + /// Sets new tempo control value as a difference in percents compared + /// to the original tempo (-50 .. +100 %); + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setTempoChange(IntPtr h, float newTempo); + + /// + /// Sets new pitch control value. Original pitch = 1.0, smaller values + /// represent lower pitches, larger values higher pitch. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setPitch(IntPtr h, float newPitch); + + /// + /// Sets pitch change in octaves compared to the original pitch + /// (-1.00 .. +1.00); + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setPitchOctaves(IntPtr h, float newPitch); + + /// + /// Sets pitch change in semi-tones compared to the original pitch + /// (-12 .. +12); + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setPitchSemiTones(IntPtr h, float newPitch); + + /// + /// Sets the number of channels, 1 = mono, 2 = stereo + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setChannels(IntPtr h, uint numChannels); + + /// + /// Sets sample rate. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_setSampleRate(IntPtr h, uint srate); + + /// + /// Flushes the last samples from the processing pipeline to the output. + /// Clears also the internal processing buffers. + /// + /// Note: This function is meant for extracting the last samples of a sound + /// stream. This function may introduce additional blank samples in the end + /// of the sound stream, and thus it's not recommended to call this function + /// in the middle of a sound stream. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_flush(IntPtr h); + + /// + /// Adds 'numSamples' pcs of samples from the 'samples' memory position into + /// the input of the object. Notice that sample rate _has_to_ be set before + /// calling this function, otherwise throws a runtime_error exception. + /// + /// Handle + /// Pointer to sample buffer. + /// Number of samples in buffer. Notice that in case of stereo-sound a single sample contains data for both channels. + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_putSamples(IntPtr h, [MarshalAs(UnmanagedType.LPArray)] float[] samples, int numSamples); + + /// + /// Clears all the samples in the object's output and internal processing + /// buffers. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern void soundtouch_clear(IntPtr h); + + /// + /// Changes a setting controlling the processing system behaviour. See the + /// 'SETTING_...' defines for available setting ID's. + /// + /// Handle + /// Setting ID number, see SETTING_... defines. + /// New setting value. + /// 'TRUE' if the setting was succesfully changed + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern bool soundtouch_setSetting(IntPtr h, SoundTouchSettings settingId, int value); + + /// + /// Reads a setting controlling the processing system behaviour. See the + /// 'SETTING_...' defines for available setting ID's. + /// + /// Handle + /// Setting ID number, see SETTING_... defines. + /// The setting value + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int soundtouch_getSetting(IntPtr h, SoundTouchSettings settingId); + + /// + /// Returns number of samples currently unprocessed. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int soundtouch_numUnprocessedSamples(IntPtr h); + + /// + /// Adjusts book-keeping so that given number of samples are removed from beginning of the + /// sample buffer without copying them anywhere. + /// + /// Used to reduce the number of samples in the buffer when accessing the sample buffer directly + /// with 'ptrBegin' function. + /// + /// Handle + /// Buffer where to copy output samples. + /// How many samples to receive at max. + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint soundtouch_receiveSamples(IntPtr h, [MarshalAs(UnmanagedType.LPArray)] float[] outBuffer, uint maxSamples); + + /// + /// Returns number of samples currently available. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern uint soundtouch_numSamples(IntPtr h); + + /// + /// Returns nonzero if there aren't any samples available for outputting. + /// + [DllImport(SoundTouchDllName, CallingConvention = CallingConvention.Cdecl)] + public static extern int soundtouch_isEmpty(IntPtr h); + + } +} \ No newline at end of file diff --git a/MusicPlayer/SoundTouch/SoundTouchInterop64.cs b/MusicPlayer/SoundTouch/SoundTouchInterop64.cs new file mode 100644 index 0000000..83a5c56 --- /dev/null +++ b/MusicPlayer/SoundTouch/SoundTouchInterop64.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Collection_Editor.code.Modules.MusicPlayer.SoundTouch +{ + class SoundTouchInterop64 + { + private const string SoundTouchDllName = "SoundTouch_x64.dll"; + + /// + /// Create a new instance of SoundTouch processor. + /// + [DllImport(SoundTouchDllName)] + public static extern IntPtr soundtouch_createInstance(); + + /// + /// Destroys a SoundTouch processor instance. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_destroyInstance(IntPtr h); + + /// + /// Get SoundTouch library version string - alternative function for + /// environments that can't properly handle character string as return value + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_getVersionString2(StringBuilder versionString, int bufferSize); + + /// + /// Get SoundTouch library version Id + /// + [DllImport(SoundTouchDllName)] + public static extern uint soundtouch_getVersionId(); + + /// + /// Sets new rate control value. Normal rate = 1.0, smaller values + /// represent slower rate, larger faster rates. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setRate(IntPtr h, float newRate); + + /// + /// Sets new tempo control value. Normal tempo = 1.0, smaller values + /// represent slower tempo, larger faster tempo. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setTempo(IntPtr h, float newTempo); + + /// + /// Sets new rate control value as a difference in percents compared + /// to the original rate (-50 .. +100 %); + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setRateChange(IntPtr h, float newRate); + + /// + /// Sets new tempo control value as a difference in percents compared + /// to the original tempo (-50 .. +100 %); + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setTempoChange(IntPtr h, float newTempo); + + /// + /// Sets new pitch control value. Original pitch = 1.0, smaller values + /// represent lower pitches, larger values higher pitch. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setPitch(IntPtr h, float newPitch); + + /// + /// Sets pitch change in octaves compared to the original pitch + /// (-1.00 .. +1.00); + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setPitchOctaves(IntPtr h, float newPitch); + + /// + /// Sets pitch change in semi-tones compared to the original pitch + /// (-12 .. +12); + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setPitchSemiTones(IntPtr h, float newPitch); + + /// + /// Sets the number of channels, 1 = mono, 2 = stereo + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setChannels(IntPtr h, uint numChannels); + + /// + /// Sets sample rate. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_setSampleRate(IntPtr h, uint srate); + + /// + /// Flushes the last samples from the processing pipeline to the output. + /// Clears also the internal processing buffers. + /// + /// Note: This function is meant for extracting the last samples of a sound + /// stream. This function may introduce additional blank samples in the end + /// of the sound stream, and thus it's not recommended to call this function + /// in the middle of a sound stream. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_flush(IntPtr h); + + /// + /// Adds 'numSamples' pcs of samples from the 'samples' memory position into + /// the input of the object. Notice that sample rate _has_to_ be set before + /// calling this function, otherwise throws a runtime_error exception. + /// + /// Handle + /// Pointer to sample buffer. + /// Number of samples in buffer. Notice that in case of stereo-sound a single sample contains data for both channels. + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_putSamples(IntPtr h, [MarshalAs(UnmanagedType.LPArray)] float[] samples, int numSamples); + + /// + /// Clears all the samples in the object's output and internal processing + /// buffers. + /// + [DllImport(SoundTouchDllName)] + public static extern void soundtouch_clear(IntPtr h); + + /// + /// Changes a setting controlling the processing system behaviour. See the + /// 'SETTING_...' defines for available setting ID's. + /// + /// Handle + /// Setting ID number, see SETTING_... defines. + /// New setting value. + /// 'TRUE' if the setting was succesfully changed + [DllImport(SoundTouchDllName)] + public static extern bool soundtouch_setSetting(IntPtr h, SoundTouchSettings settingId, int value); + + /// + /// Reads a setting controlling the processing system behaviour. See the + /// 'SETTING_...' defines for available setting ID's. + /// + /// Handle + /// Setting ID number, see SETTING_... defines. + /// The setting value + [DllImport(SoundTouchDllName)] + public static extern int soundtouch_getSetting(IntPtr h, SoundTouchSettings settingId); + + /// + /// Returns number of samples currently unprocessed. + /// + [DllImport(SoundTouchDllName)] + public static extern int soundtouch_numUnprocessedSamples(IntPtr h); + + /// + /// Adjusts book-keeping so that given number of samples are removed from beginning of the + /// sample buffer without copying them anywhere. + /// + /// Used to reduce the number of samples in the buffer when accessing the sample buffer directly + /// with 'ptrBegin' function. + /// + /// Handle + /// Buffer where to copy output samples. + /// How many samples to receive at max. + [DllImport(SoundTouchDllName)] + public static extern uint soundtouch_receiveSamples(IntPtr h, [MarshalAs(UnmanagedType.LPArray)] float[] outBuffer, uint maxSamples); + + /// + /// Returns number of samples currently available. + /// + [DllImport(SoundTouchDllName)] + public static extern uint soundtouch_numSamples(IntPtr h); + + /// + /// Returns nonzero if there aren't any samples available for outputting. + /// + [DllImport(SoundTouchDllName)] + public static extern int soundtouch_isEmpty(IntPtr h); + + } +} \ No newline at end of file diff --git a/MusicPlayer/SoundTouch/SoundTouchProfile.cs b/MusicPlayer/SoundTouch/SoundTouchProfile.cs new file mode 100644 index 0000000..a8df5af --- /dev/null +++ b/MusicPlayer/SoundTouch/SoundTouchProfile.cs @@ -0,0 +1,15 @@ +namespace Collection_Editor.code.Modules.MusicPlayer.SoundTouch +{ + public class SoundTouchProfile + { + public bool UseTempo { get; set; } + public bool UseAntiAliasing { get; set; } + public bool UseQuickSeek { get; set; } + + public SoundTouchProfile(bool useTempo, bool useAntiAliasing) + { + UseTempo = useTempo; + UseAntiAliasing = useAntiAliasing; + } + } +} \ No newline at end of file diff --git a/MusicPlayer/SoundTouch/SoundTouchSettings.cs b/MusicPlayer/SoundTouch/SoundTouchSettings.cs new file mode 100644 index 0000000..a1905b3 --- /dev/null +++ b/MusicPlayer/SoundTouch/SoundTouchSettings.cs @@ -0,0 +1,46 @@ +namespace Collection_Editor.code.Modules.MusicPlayer.SoundTouch +{ + enum SoundTouchSettings + { + /// + /// Available setting IDs for the 'setSetting' and 'get_setting' functions. + /// Enable/disable anti-alias filter in pitch transposer (0 = disable) + /// + UseAaFilter = 0, + + /// + /// Pitch transposer anti-alias filter length (8 .. 128 taps, default = 32) + /// + AaFilterLength = 1, + + /// + /// Enable/disable quick seeking algorithm in tempo changer routine + /// (enabling quick seeking lowers CPU utilization but causes a minor sound + /// quality compromising) + /// + UseQuickSeek = 2, + + /// + /// Time-stretch algorithm single processing sequence length in milliseconds. This determines + /// to how long sequences the original sound is chopped in the time-stretch algorithm. + /// See "STTypes.h" or README for more information. + /// + SequenceMs = 3, + + /// + /// Time-stretch algorithm seeking window length in milliseconds for algorithm that finds the + /// best possible overlapping location. This determines from how wide window the algorithm + /// may look for an optimal joining location when mixing the sound sequences back together. + /// See "STTypes.h" or README for more information. + /// + SeekWindowMs = 4, + + /// + /// Time-stretch algorithm overlap length in milliseconds. When the chopped sound sequences + /// are mixed back together, to form a continuous sound stream, this parameter defines over + /// how long period the two consecutive sequences are let to overlap each other. + /// See "STTypes.h" or README for more information. + /// + OverlapMs = 5 + }; +} \ No newline at end of file diff --git a/MusicPlayer/SoundTouch/VarispeedSampleProvider.cs b/MusicPlayer/SoundTouch/VarispeedSampleProvider.cs new file mode 100644 index 0000000..ee98b0f --- /dev/null +++ b/MusicPlayer/SoundTouch/VarispeedSampleProvider.cs @@ -0,0 +1,148 @@ +using System; +using NAudio.Wave; + +namespace Collection_Editor.code.Modules.MusicPlayer.SoundTouch +{ + public class VarispeedSampleProvider : ISampleProvider, IDisposable + { + private readonly ISampleProvider sourceProvider; + private readonly VarispeedDemo.SoundTouch.SoundTouch soundTouch; + private readonly float[] sourceReadBuffer; + private readonly float[] soundTouchReadBuffer; + private readonly int channelCount; + private float playbackRate = 1.0f; + private SoundTouchProfile currentSoundTouchProfile; + private bool repositionRequested; + + public VarispeedSampleProvider(ISampleProvider sourceProvider, int readDurationMilliseconds, SoundTouchProfile soundTouchProfile) + { + soundTouch = new VarispeedDemo.SoundTouch.SoundTouch(); + // explore what the default values are before we change them: + //Debug.WriteLine(String.Format("SoundTouch Version {0}", soundTouch.VersionString)); + //Debug.WriteLine("Use QuickSeek: {0}", soundTouch.GetUseQuickSeek()); + //Debug.WriteLine("Use AntiAliasing: {0}", soundTouch.GetUseAntiAliasing()); + + SetSoundTouchProfile(soundTouchProfile); + this.sourceProvider = sourceProvider; + soundTouch.SetSampleRate(WaveFormat.SampleRate); + channelCount = WaveFormat.Channels; + soundTouch.SetChannels(channelCount); + sourceReadBuffer = new float[(WaveFormat.SampleRate * channelCount * (long)readDurationMilliseconds) / 1000]; + soundTouchReadBuffer = new float[sourceReadBuffer.Length * 10]; // support down to 0.1 speed + } + + public int Read(float[] buffer, int offset, int count) + { + if (playbackRate == 0) // play silence + { + for (int n = 0; n < count; n++) + { + buffer[offset++] = 0; + } + return count; + } + + if (repositionRequested) + { + soundTouch.Clear(); + repositionRequested = false; + } + + int samplesRead = 0; + bool reachedEndOfSource = false; + while (samplesRead < count) + { + if (soundTouch.NumberOfSamplesAvailable == 0) + { + var readFromSource = sourceProvider.Read(sourceReadBuffer, 0, sourceReadBuffer.Length); + if (readFromSource > 0) + { + soundTouch.PutSamples(sourceReadBuffer, readFromSource/channelCount); + } + else + { + reachedEndOfSource = true; + // we've reached the end, tell SoundTouch we're done + soundTouch.Flush(); + } + } + var desiredSampleFrames = (count - samplesRead)/channelCount; + + var received = soundTouch.ReceiveSamples(soundTouchReadBuffer, desiredSampleFrames)*channelCount; + // use loop instead of Array.Copy due to WaveBuffer + for (int n = 0; n < received; n++) + { + buffer[offset+samplesRead++] = soundTouchReadBuffer[n]; + } + if (received == 0 && reachedEndOfSource) break; + } + return samplesRead; + } + + public WaveFormat WaveFormat => sourceProvider.WaveFormat; + + public float PlaybackRate + { + get + { + return playbackRate; + } + set + { + if (playbackRate != value) + { + UpdatePlaybackRate(value); + playbackRate = value; + } + } + } + + private void UpdatePlaybackRate(float value) + { + if (value != 0) + { + if (currentSoundTouchProfile.UseTempo) + { + soundTouch.SetTempo(value); + } + else + { + soundTouch.SetRate(value); + } + } + } + + public void Dispose() + { + soundTouch.Dispose(); + } + + public void SetSoundTouchProfile(SoundTouchProfile soundTouchProfile) + { + if (currentSoundTouchProfile != null && + playbackRate != 1.0f && + soundTouchProfile.UseTempo != currentSoundTouchProfile.UseTempo) + { + if (soundTouchProfile.UseTempo) + { + soundTouch.SetRate(1.0f); + soundTouch.SetPitchOctaves(0f); + soundTouch.SetTempo(playbackRate); + } + else + { + soundTouch.SetTempo(1.0f); + soundTouch.SetRate(playbackRate); + } + } + this.currentSoundTouchProfile = soundTouchProfile; + soundTouch.SetUseAntiAliasing(soundTouchProfile.UseAntiAliasing); + soundTouch.SetUseQuickSeek(soundTouchProfile.UseQuickSeek); + } + + public void Reposition() + { + repositionRequested = true; + } + } +} \ No newline at end of file diff --git a/MusicPlayer/SoundTouch/lib/COPYING.TXT b/MusicPlayer/SoundTouch/lib/COPYING.TXT new file mode 100644 index 0000000..5b2161b --- /dev/null +++ b/MusicPlayer/SoundTouch/lib/COPYING.TXT @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authoried party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/MusicPlayer/SoundTouch/lib/SoundTouch.dll b/MusicPlayer/SoundTouch/lib/SoundTouch.dll new file mode 100644 index 0000000..91e2f3b Binary files /dev/null and b/MusicPlayer/SoundTouch/lib/SoundTouch.dll differ diff --git a/MusicPlayer/SoundTouch/lib/SoundTouch_x64.dll b/MusicPlayer/SoundTouch/lib/SoundTouch_x64.dll new file mode 100644 index 0000000..f0b4fc4 Binary files /dev/null and b/MusicPlayer/SoundTouch/lib/SoundTouch_x64.dll differ diff --git a/MusicPlayer/SoundTouchPlayer.cs b/MusicPlayer/SoundTouchPlayer.cs new file mode 100644 index 0000000..4cb33a4 --- /dev/null +++ b/MusicPlayer/SoundTouchPlayer.cs @@ -0,0 +1,72 @@ +using System.Timers; +using Collection_Editor.code.Modules.MusicPlayer.SoundTouch; +using NAudio.Wave; + +namespace MusicPlayer +{ + internal class SoundTouchPlayer : AudioPlayer + { + public override bool IsSpeedControlAvaliable => true; + private VarispeedSampleProvider _speedControl; + private readonly SoundTouchProfile _soundTouchProfile = new SoundTouchProfile(true, false); + private float playbackSpeed = 1f; + private readonly Timer _timer; + + internal SoundTouchPlayer() + { + _timer = new Timer(500); + _timer.Elapsed += Timer_Elapsed; + } + public override void SetSpeed(float speed) + { + playbackSpeed = speed; + lock (_lockingObject) + { + if (_speedControl != null) + _speedControl.PlaybackRate = speed; + } + } + + public override void Play(string audioFile, int startTime) + { + var audioReader = CreateAudioReader(audioFile); + SetAudioReader(audioReader); + SetSpeedReader(audioReader); + + Play(startTime); + } + + protected override void Play(int startTime) + { + if (_audioFileReader == null) + return; + StopPlayback(); + _speedControl.PlaybackRate = playbackSpeed; + _waveOutDevice.Init(_speedControl); + _audioFileReader.Volume = SoundVolume; + _audioFileReader.Skip(startTime); + _waveOutDevice.Play(); + + _playbackAborted = false; + _timer.Start(); + } + + private void SetSpeedReader(AudioFileReader audio) + { + _speedControl = new VarispeedSampleProvider(audio, 100, _soundTouchProfile); + } + private double LastTime = 0.0; + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + //For some reason VarispeedSampleProvider doesn't ever "finish" playing, + //so we have to workaround it. (dirty and I don't like it, but it works) + var delta = TotalTime - CurrentTime; + if (CurrentTime >= TotalTime || (LastTime.Equals(CurrentTime) && delta < 0.16)) + { + _timer?.Stop(); + _waveOutDevice?.Stop(); + } + LastTime = CurrentTime; + } + } +} \ No newline at end of file diff --git a/MusicPlayer/WaveStreamExtensions.cs b/MusicPlayer/WaveStreamExtensions.cs new file mode 100644 index 0000000..c1c3c2b --- /dev/null +++ b/MusicPlayer/WaveStreamExtensions.cs @@ -0,0 +1,37 @@ +using System; +using NAudio.Wave; + +namespace MusicPlayer +{ + public static class WaveStreamExtensions + { + // Set position of WaveStream to nearest block to supplied position + public static void SetPosition(this WaveStream strm, long position) + { + // distance from block boundary (may be 0) + long adj = position % strm.WaveFormat.BlockAlign; + // adjust position to boundary and clamp to valid range + long newPos = Math.Max(0, Math.Min(strm.Length, position - adj)); + // set playback position + strm.Position = newPos; + } + + // Set playback position of WaveStream by seconds + public static void SetPosition(this WaveStream strm, double seconds) + { + strm.SetPosition((long)(seconds * strm.WaveFormat.AverageBytesPerSecond)); + } + + // Set playback position of WaveStream by time (as a TimeSpan) + public static void SetPosition(this WaveStream strm, TimeSpan time) + { + strm.SetPosition(time.TotalSeconds); + } + + // Set playback position of WaveStream relative to current position + public static void Seek(this WaveStream strm, double offset) + { + strm.SetPosition(strm.Position + (long)(offset * strm.WaveFormat.AverageBytesPerSecond)); + } + } +} \ No newline at end of file diff --git a/ObjectListView/CellEditing/CellEditKeyEngine.cs b/ObjectListView/CellEditing/CellEditKeyEngine.cs new file mode 100644 index 0000000..a806d20 --- /dev/null +++ b/ObjectListView/CellEditing/CellEditKeyEngine.cs @@ -0,0 +1,520 @@ +/* + * CellEditKeyEngine - A engine that allows the behaviour of arbitrary keys to be configured + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * v2.8 + * 2014-05-30 JPP - When a row is disabled, skip over it when looking for another cell to edit + * v2.5 + * 2012-04-14 JPP - Fixed bug where, on a OLV with only a single editable column, tabbing + * to change rows would edit the cell above rather than the cell below + * the cell being edited. + * 2.5 + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using BrightIdeasSoftware; + +namespace BrightIdeasSoftware { + /// + /// Indicates the behavior of a key when a cell "on the edge" is being edited. + /// and the normal behavior of that key would exceed the edge. For example, + /// for a key that normally moves one column to the left, the "edge" would be + /// the left most column, since the normal action of the key cannot be taken + /// (since there are no more columns to the left). + /// + public enum CellEditAtEdgeBehaviour { + /// + /// The key press will be ignored + /// + Ignore, + + /// + /// The key press will result in the cell editing wrapping to the + /// cell on the opposite edge. + /// + Wrap, + + /// + /// The key press will wrap, but the column will be changed to the + /// appropiate adjacent column. This only makes sense for keys where + /// the normal action is ChangeRow. + /// + ChangeColumn, + + /// + /// The key press will wrap, but the row will be changed to the + /// appropiate adjacent row. This only makes sense for keys where + /// the normal action is ChangeColumn. + /// + ChangeRow, + + /// + /// The key will result in the current edit operation being ended. + /// + EndEdit + }; + + /// + /// Indicates the normal behaviour of a key when used during a cell edit + /// operation. + /// + public enum CellEditCharacterBehaviour { + /// + /// The key press will be ignored + /// + Ignore, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the next editable cell to the left. + /// + ChangeColumnLeft, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the next editable cell to the right. + /// + ChangeColumnRight, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the row above. + /// + ChangeRowUp, + + /// + /// The key press will end the current edit and begin an edit + /// operation on the row below + /// + ChangeRowDown, + + /// + /// The key press will cancel the current edit + /// + CancelEdit, + + /// + /// The key press will finish the current edit operation + /// + EndEdit, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb1, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb2, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb3, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb4, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb5, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb6, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb7, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb8, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb9, + + /// + /// Custom verb that can be used for specialized actions. + /// + CustomVerb10, + }; + + /// + /// Instances of this class handle key presses during a cell edit operation. + /// + public class CellEditKeyEngine { + + #region Public interface + + /// + /// Sets the behaviour of a given key + /// + /// + /// + /// + public virtual void SetKeyBehaviour(Keys key, CellEditCharacterBehaviour normalBehaviour, CellEditAtEdgeBehaviour atEdgeBehaviour) { + this.CellEditKeyMap[key] = normalBehaviour; + this.CellEditKeyAtEdgeBehaviourMap[key] = atEdgeBehaviour; + } + + /// + /// Handle a key press + /// + /// + /// + /// True if the key was completely handled. + public virtual bool HandleKey(ObjectListView olv, Keys keyData) { + if (olv == null) throw new ArgumentNullException("olv"); + + CellEditCharacterBehaviour behaviour; + if (!CellEditKeyMap.TryGetValue(keyData, out behaviour)) + return false; + + this.ListView = olv; + + switch (behaviour) { + case CellEditCharacterBehaviour.Ignore: + break; + case CellEditCharacterBehaviour.CancelEdit: + this.HandleCancelEdit(); + break; + case CellEditCharacterBehaviour.EndEdit: + this.HandleEndEdit(); + break; + case CellEditCharacterBehaviour.ChangeColumnLeft: + case CellEditCharacterBehaviour.ChangeColumnRight: + this.HandleColumnChange(keyData, behaviour); + break; + case CellEditCharacterBehaviour.ChangeRowDown: + case CellEditCharacterBehaviour.ChangeRowUp: + this.HandleRowChange(keyData, behaviour); + break; + default: + return this.HandleCustomVerb(keyData, behaviour); + }; + + return true; + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the ObjectListView on which the current key is being handled. + /// This cannot be null. + /// + protected ObjectListView ListView { + get { return listView; } + set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the row of the cell that is currently being edited + /// + protected OLVListItem ItemBeingEdited { + get { + return this.ListView.CellEditEventArgs.ListViewItem; + } + } + + /// + /// Gets the index of the column of the cell that is being edited + /// + protected int SubItemIndexBeingEdited { + get { + return this.ListView.CellEditEventArgs.SubItemIndex; + } + } + + /// + /// Gets or sets the map that remembers the normal behaviour of keys + /// + protected IDictionary CellEditKeyMap { + get { + if (cellEditKeyMap == null) + this.InitializeCellEditKeyMaps(); + return cellEditKeyMap; + } + set { + cellEditKeyMap = value; + } + } + private IDictionary cellEditKeyMap; + + /// + /// Gets or sets the map that remembers the desired behaviour of keys + /// on edge cases. + /// + protected IDictionary CellEditKeyAtEdgeBehaviourMap { + get { + if (cellEditKeyAtEdgeBehaviourMap == null) + this.InitializeCellEditKeyMaps(); + return cellEditKeyAtEdgeBehaviourMap; + } + set { + cellEditKeyAtEdgeBehaviourMap = value; + } + } + private IDictionary cellEditKeyAtEdgeBehaviourMap; + + #endregion + + #region Initialization + + /// + /// Setup the default key mapping + /// + protected virtual void InitializeCellEditKeyMaps() { + this.cellEditKeyMap = new Dictionary(); + this.cellEditKeyMap[Keys.Escape] = CellEditCharacterBehaviour.CancelEdit; + this.cellEditKeyMap[Keys.Return] = CellEditCharacterBehaviour.EndEdit; + this.cellEditKeyMap[Keys.Enter] = CellEditCharacterBehaviour.EndEdit; + this.cellEditKeyMap[Keys.Tab] = CellEditCharacterBehaviour.ChangeColumnRight; + this.cellEditKeyMap[Keys.Tab | Keys.Shift] = CellEditCharacterBehaviour.ChangeColumnLeft; + this.cellEditKeyMap[Keys.Left | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnLeft; + this.cellEditKeyMap[Keys.Right | Keys.Alt] = CellEditCharacterBehaviour.ChangeColumnRight; + this.cellEditKeyMap[Keys.Up | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowUp; + this.cellEditKeyMap[Keys.Down | Keys.Alt] = CellEditCharacterBehaviour.ChangeRowDown; + + this.cellEditKeyAtEdgeBehaviourMap = new Dictionary(); + this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Tab | Keys.Shift] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Left | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Right | Keys.Alt] = CellEditAtEdgeBehaviour.Wrap; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Up | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn; + this.cellEditKeyAtEdgeBehaviourMap[Keys.Down | Keys.Alt] = CellEditAtEdgeBehaviour.ChangeColumn; + } + + #endregion + + #region Command handling + + /// + /// Handle the end edit command + /// + protected virtual void HandleEndEdit() { + this.ListView.PossibleFinishCellEditing(); + } + + /// + /// Handle the cancel edit command + /// + protected virtual void HandleCancelEdit() { + this.ListView.CancelCellEdit(); + } + + /// + /// Placeholder that subclasses can override to handle any custom verbs + /// + /// + /// + /// + protected virtual bool HandleCustomVerb(Keys keyData, CellEditCharacterBehaviour behaviour) { + return false; + } + + /// + /// Handle a change row command + /// + /// + /// + protected virtual void HandleRowChange(Keys keyData, CellEditCharacterBehaviour behaviour) { + // If we couldn't finish editing the current cell, don't try to move it + if (!this.ListView.PossibleFinishCellEditing()) + return; + + OLVListItem olvi = this.ItemBeingEdited; + int subItemIndex = this.SubItemIndexBeingEdited; + bool isGoingUp = behaviour == CellEditCharacterBehaviour.ChangeRowUp; + + // Try to find a row above (or below) the currently edited cell + // If we find one, start editing it and we're done. + OLVListItem adjacentOlvi = this.GetAdjacentItemOrNull(olvi, isGoingUp); + if (adjacentOlvi != null) { + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + return; + } + + // There is no adjacent row in the direction we want, so we must be on an edge. + CellEditAtEdgeBehaviour atEdgeBehaviour; + if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour)) + atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap; + switch (atEdgeBehaviour) { + case CellEditAtEdgeBehaviour.Ignore: + break; + case CellEditAtEdgeBehaviour.EndEdit: + this.ListView.PossibleFinishCellEditing(); + break; + case CellEditAtEdgeBehaviour.Wrap: + adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp); + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + break; + case CellEditAtEdgeBehaviour.ChangeColumn: + // Figure out the next editable column + List editableColumnsInDisplayOrder = this.EditableColumnsInDisplayOrder; + int displayIndex = Math.Max(0, editableColumnsInDisplayOrder.IndexOf(this.ListView.GetColumn(subItemIndex))); + if (isGoingUp) + displayIndex = (editableColumnsInDisplayOrder.Count + displayIndex - 1) % editableColumnsInDisplayOrder.Count; + else + displayIndex = (displayIndex + 1) % editableColumnsInDisplayOrder.Count; + subItemIndex = editableColumnsInDisplayOrder[displayIndex].Index; + + // Wrap to the next row and start the cell edit + adjacentOlvi = this.GetAdjacentItemOrNull(null, isGoingUp); + this.StartCellEditIfDifferent(adjacentOlvi, subItemIndex); + break; + } + } + + /// + /// Handle a change column command + /// + /// + /// + protected virtual void HandleColumnChange(Keys keyData, CellEditCharacterBehaviour behaviour) + { + // If we couldn't finish editing the current cell, don't try to move it + if (!this.ListView.PossibleFinishCellEditing()) + return; + + // Changing columns only works in details mode + if (this.ListView.View != View.Details) + return; + + List editableColumns = this.EditableColumnsInDisplayOrder; + OLVListItem olvi = this.ItemBeingEdited; + int displayIndex = Math.Max(0, + editableColumns.IndexOf(this.ListView.GetColumn(this.SubItemIndexBeingEdited))); + bool isGoingLeft = behaviour == CellEditCharacterBehaviour.ChangeColumnLeft; + + // Are we trying to continue past one of the edges? + if ((isGoingLeft && displayIndex == 0) || + (!isGoingLeft && displayIndex == editableColumns.Count - 1)) + { + // Yes, so figure out our at edge behaviour + CellEditAtEdgeBehaviour atEdgeBehaviour; + if (!this.CellEditKeyAtEdgeBehaviourMap.TryGetValue(keyData, out atEdgeBehaviour)) + atEdgeBehaviour = CellEditAtEdgeBehaviour.Wrap; + switch (atEdgeBehaviour) + { + case CellEditAtEdgeBehaviour.Ignore: + return; + case CellEditAtEdgeBehaviour.EndEdit: + this.HandleEndEdit(); + return; + case CellEditAtEdgeBehaviour.ChangeRow: + case CellEditAtEdgeBehaviour.Wrap: + if (atEdgeBehaviour == CellEditAtEdgeBehaviour.ChangeRow) + olvi = GetAdjacentItem(olvi, isGoingLeft && displayIndex == 0); + if (isGoingLeft) + displayIndex = editableColumns.Count - 1; + else + displayIndex = 0; + break; + } + } + else + { + if (isGoingLeft) + displayIndex -= 1; + else + displayIndex += 1; + } + + int subItemIndex = editableColumns[displayIndex].Index; + this.StartCellEditIfDifferent(olvi, subItemIndex); + } + + #endregion + + #region Utilities + + /// + /// Start editing the indicated cell if that cell is not already being edited + /// + /// The row to edit + /// The cell within that row to edit + protected void StartCellEditIfDifferent(OLVListItem olvi, int subItemIndex) { + if (this.ItemBeingEdited == olvi && this.SubItemIndexBeingEdited == subItemIndex) + return; + + this.ListView.EnsureVisible(olvi.Index); + this.ListView.StartCellEdit(olvi, subItemIndex); + } + + /// + /// Gets the adjacent item to the given item in the given direction. + /// If that item is disabled, continue in that direction until an enabled item is found. + /// + /// The row whose neighbour is sought + /// The direction of the adjacentness + /// An OLVListView adjacent to the given item, or null if there are no more enabled items in that direction. + protected OLVListItem GetAdjacentItemOrNull(OLVListItem olvi, bool up) { + OLVListItem item = up ? this.ListView.GetPreviousItem(olvi) : this.ListView.GetNextItem(olvi); + while (item != null && !item.Enabled) + item = up ? this.ListView.GetPreviousItem(item) : this.ListView.GetNextItem(item); + return item; + } + + /// + /// Gets the adjacent item to the given item in the given direction, wrapping if needed. + /// + /// The row whose neighbour is sought + /// The direction of the adjacentness + /// An OLVListView adjacent to the given item, or null if there are no more items in that direction. + protected OLVListItem GetAdjacentItem(OLVListItem olvi, bool up) { + return this.GetAdjacentItemOrNull(olvi, up) ?? this.GetAdjacentItemOrNull(null, up); + } + + /// + /// Gets a collection of columns that are editable in the order they are shown to the user + /// + protected List EditableColumnsInDisplayOrder { + get { + List editableColumnsInDisplayOrder = new List(); + foreach (OLVColumn x in this.ListView.ColumnsInDisplayOrder) + if (x.IsEditable) + editableColumnsInDisplayOrder.Add(x); + return editableColumnsInDisplayOrder; + } + } + + #endregion + } +} diff --git a/ObjectListView/CellEditing/CellEditors.cs b/ObjectListView/CellEditing/CellEditors.cs new file mode 100644 index 0000000..7ecd9fb --- /dev/null +++ b/ObjectListView/CellEditing/CellEditors.cs @@ -0,0 +1,288 @@ +/* + * CellEditors - Several slightly modified controls that are used as celleditors within ObjectListView. + * + * Author: Phillip Piper + * Date: 20/10/2008 5:15 PM + * + * Change log: + * v2.6 + * 2012-08-02 JPP - Make most editors public so they can be reused/subclassed + * v2.3 + * 2009-08-13 JPP - Standardized code formatting + * v2.2.1 + * 2008-01-18 JPP - Added special handling for enums + * 2008-01-16 JPP - Added EditorRegistry + * v2.0.1 + * 2008-10-20 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// These items allow combo boxes to remember a value and its description. + /// + public class ComboBoxItem + { + /// + /// + /// + /// + /// + public ComboBoxItem(Object key, String description) { + this.key = key; + this.description = description; + } + private readonly String description; + + /// + /// + /// + public Object Key { + get { return key; } + } + private readonly Object key; + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() { + return this.description; + } + } + + //----------------------------------------------------------------------- + // Cell editors + // These classes are simple cell editors that make it easier to get and set + // the value that the control is showing. + // In many cases, you can intercept the CellEditStarting event to + // change the characteristics of the editor. For example, changing + // the acceptable range for a numeric editor or changing the strings + // that respresent true and false values for a boolean editor. + + /// + /// This editor shows and auto completes values from the given listview column. + /// + [ToolboxItem(false)] + public class AutoCompleteCellEditor : ComboBox + { + /// + /// Create an AutoCompleteCellEditor + /// + /// + /// + public AutoCompleteCellEditor(ObjectListView lv, OLVColumn column) { + this.DropDownStyle = ComboBoxStyle.DropDown; + + Dictionary alreadySeen = new Dictionary(); + for (int i = 0; i < Math.Min(lv.GetItemCount(), 1000); i++) { + String str = column.GetStringValue(lv.GetModelObject(i)); + if (!alreadySeen.ContainsKey(str)) { + this.Items.Add(str); + alreadySeen[str] = true; + } + } + + this.Sorted = true; + this.AutoCompleteSource = AutoCompleteSource.ListItems; + this.AutoCompleteMode = AutoCompleteMode.Append; + } + } + + /// + /// This combo box is specialised to allow editing of an enum. + /// + [ToolboxItem(false)] + public class EnumCellEditor : ComboBox + { + /// + /// + /// + /// + public EnumCellEditor(Type type) { + this.DropDownStyle = ComboBoxStyle.DropDownList; + this.ValueMember = "Key"; + + ArrayList values = new ArrayList(); + foreach (object value in Enum.GetValues(type)) + values.Add(new ComboBoxItem(value, Enum.GetName(type, value))); + + this.DataSource = values; + } + } + + /// + /// This editor simply shows and edits integer values. + /// + [ToolboxItem(false)] + public class IntUpDown : NumericUpDown + { + /// + /// + /// + public IntUpDown() { + this.DecimalPlaces = 0; + this.Minimum = -9999999; + this.Maximum = 9999999; + } + + /// + /// Gets or sets the value shown by this editor + /// + new public int Value { + get { return Decimal.ToInt32(base.Value); } + set { base.Value = new Decimal(value); } + } + } + + /// + /// This editor simply shows and edits unsigned integer values. + /// + /// This class can't be made public because unsigned int is not a + /// CLS-compliant type. If you want to use, just copy the code to this class + /// into your project and use it from there. + [ToolboxItem(false)] + internal class UintUpDown : NumericUpDown + { + public UintUpDown() { + this.DecimalPlaces = 0; + this.Minimum = 0; + this.Maximum = 9999999; + } + + new public uint Value { + get { return Decimal.ToUInt32(base.Value); } + set { base.Value = new Decimal(value); } + } + } + + /// + /// This editor simply shows and edits boolean values. + /// + [ToolboxItem(false)] + public class BooleanCellEditor : ComboBox + { + /// + /// + /// + public BooleanCellEditor() { + this.DropDownStyle = ComboBoxStyle.DropDownList; + this.ValueMember = "Key"; + + ArrayList values = new ArrayList(); + values.Add(new ComboBoxItem(false, "False")); + values.Add(new ComboBoxItem(true, "True")); + + this.DataSource = values; + } + } + + /// + /// This editor simply shows and edits boolean values using a checkbox + /// + [ToolboxItem(false)] + public class BooleanCellEditor2 : CheckBox + { + /// + /// Gets or sets the value shown by this editor + /// + public bool? Value { + get { + switch (this.CheckState) { + case CheckState.Checked: return true; + case CheckState.Indeterminate: return null; + case CheckState.Unchecked: + default: return false; + } + } + set { + if (value.HasValue) + this.CheckState = value.Value ? CheckState.Checked : CheckState.Unchecked; + else + this.CheckState = CheckState.Indeterminate; + } + } + + /// + /// Gets or sets how the checkbox will be aligned + /// + public new HorizontalAlignment TextAlign { + get { + switch (this.CheckAlign) { + case ContentAlignment.MiddleRight: return HorizontalAlignment.Right; + case ContentAlignment.MiddleCenter: return HorizontalAlignment.Center; + case ContentAlignment.MiddleLeft: + default: return HorizontalAlignment.Left; + } + } + set { + switch (value) { + case HorizontalAlignment.Left: + this.CheckAlign = ContentAlignment.MiddleLeft; + break; + case HorizontalAlignment.Center: + this.CheckAlign = ContentAlignment.MiddleCenter; + break; + case HorizontalAlignment.Right: + this.CheckAlign = ContentAlignment.MiddleRight; + break; + } + } + } + } + + /// + /// This editor simply shows and edits floating point values. + /// + /// You can intercept the CellEditStarting event if you want + /// to change the characteristics of the editor. For example, by increasing + /// the number of decimal places. + [ToolboxItem(false)] + public class FloatCellEditor : NumericUpDown + { + /// + /// + /// + public FloatCellEditor() { + this.DecimalPlaces = 2; + this.Minimum = -9999999; + this.Maximum = 9999999; + } + + /// + /// Gets or sets the value shown by this editor + /// + new public double Value { + get { return Convert.ToDouble(base.Value); } + set { base.Value = Convert.ToDecimal(value); } + } + } +} diff --git a/ObjectListView/CellEditing/EditorRegistry.cs b/ObjectListView/CellEditing/EditorRegistry.cs new file mode 100644 index 0000000..c75a4ba --- /dev/null +++ b/ObjectListView/CellEditing/EditorRegistry.cs @@ -0,0 +1,203 @@ +/* + * EditorRegistry - A registry mapping types to cell editors. + * + * Author: Phillip Piper + * Date: 6-March-2011 7:53 am + * + * Change log: + * 2011-03-31 JPP - Use OLVColumn.DataType if the value to be edited is null + * 2011-03-06 JPP - Separated from CellEditors.cs + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Reflection; + +namespace BrightIdeasSoftware { + + /// + /// A delegate that creates an editor for the given value + /// + /// The model from which that value came + /// The column for which the editor is being created + /// A representative value of the type to be edited. This value may not be the exact + /// value for the column/model combination. It could be simply representative of + /// the appropriate type of value. + /// A control which can edit the given value + public delegate Control EditorCreatorDelegate(Object model, OLVColumn column, Object value); + + /// + /// An editor registry gives a way to decide what cell editor should be used to edit + /// the value of a cell. Programmers can register non-standard types and the control that + /// should be used to edit instances of that type. + /// + /// + /// All ObjectListViews share the same editor registry. + /// + public class EditorRegistry { + #region Initializing + + /// + /// Create an EditorRegistry + /// + public EditorRegistry() { + this.InitializeStandardTypes(); + } + + private void InitializeStandardTypes() { + this.Register(typeof(Boolean), typeof(BooleanCellEditor)); + this.Register(typeof(Int16), typeof(IntUpDown)); + this.Register(typeof(Int32), typeof(IntUpDown)); + this.Register(typeof(Int64), typeof(IntUpDown)); + this.Register(typeof(UInt16), typeof(UintUpDown)); + this.Register(typeof(UInt32), typeof(UintUpDown)); + this.Register(typeof(UInt64), typeof(UintUpDown)); + this.Register(typeof(Single), typeof(FloatCellEditor)); + this.Register(typeof(Double), typeof(FloatCellEditor)); + this.Register(typeof(DateTime), delegate(Object model, OLVColumn column, Object value) { + DateTimePicker c = new DateTimePicker(); + c.Format = DateTimePickerFormat.Short; + return c; + }); + this.Register(typeof(Boolean), delegate(Object model, OLVColumn column, Object value) { + CheckBox c = new BooleanCellEditor2(); + c.ThreeState = column.TriStateCheckBoxes; + return c; + }); + } + + #endregion + + #region Registering + + /// + /// Register that values of 'type' should be edited by instances of 'controlType'. + /// + /// The type of value to be edited + /// The type of the Control that will edit values of 'type' + /// + /// ObjectListView.EditorRegistry.Register(typeof(Color), typeof(MySpecialColorEditor)); + /// + public void Register(Type type, Type controlType) { + this.Register(type, delegate(Object model, OLVColumn column, Object value) { + return controlType.InvokeMember("", BindingFlags.CreateInstance, null, null, null) as Control; + }); + } + + /// + /// Register the given delegate so that it is called to create editors + /// for values of the given type + /// + /// The type of value to be edited + /// The delegate that will create a control that can edit values of 'type' + /// + /// ObjectListView.EditorRegistry.Register(typeof(Color), CreateColorEditor); + /// ... + /// public Control CreateColorEditor(Object model, OLVColumn column, Object value) + /// { + /// return new MySpecialColorEditor(); + /// } + /// + public void Register(Type type, EditorCreatorDelegate creator) { + this.creatorMap[type] = creator; + } + + /// + /// Register a delegate that will be called to create an editor for values + /// that have not been handled. + /// + /// The delegate that will create a editor for all other types + public void RegisterDefault(EditorCreatorDelegate creator) { + this.defaultCreator = creator; + } + + /// + /// Register a delegate that will be given a chance to create a control + /// before any other option is considered. + /// + /// The delegate that will create a control + public void RegisterFirstChance(EditorCreatorDelegate creator) { + this.firstChanceCreator = creator; + } + + #endregion + + #region Accessing + + /// + /// Create and return an editor that is appropriate for the given value. + /// Return null if no appropriate editor can be found. + /// + /// The model involved + /// The column to be edited + /// The value to be edited. This value may not be the exact + /// value for the column/model combination. It could be simply representative of + /// the appropriate type of value. + /// A Control that can edit the given type of values + public Control GetEditor(Object model, OLVColumn column, Object value) { + Control editor; + + // Give the first chance delegate a chance to decide + if (this.firstChanceCreator != null) { + editor = this.firstChanceCreator(model, column, value); + if (editor != null) + return editor; + } + + // Try to find a creator based on the type of the value (or the column) + Type type = value == null ? column.DataType : value.GetType(); + if (type != null && this.creatorMap.ContainsKey(type)) { + editor = this.creatorMap[type](model, column, value); + if (editor != null) + return editor; + } + + // Enums without other processing get a special editor + if (value != null && value.GetType().IsEnum) + return this.CreateEnumEditor(value.GetType()); + + // Give any default creator a final chance + if (this.defaultCreator != null) + return this.defaultCreator(model, column, value); + + return null; + } + + /// + /// Create and return an editor that will edit values of the given type + /// + /// A enum type + protected Control CreateEnumEditor(Type type) { + return new EnumCellEditor(type); + } + + #endregion + + #region Private variables + + private EditorCreatorDelegate firstChanceCreator; + private EditorCreatorDelegate defaultCreator; + private Dictionary creatorMap = new Dictionary(); + + #endregion + } +} diff --git a/ObjectListView/CustomDictionary.xml b/ObjectListView/CustomDictionary.xml new file mode 100644 index 0000000..f2cf5b9 --- /dev/null +++ b/ObjectListView/CustomDictionary.xml @@ -0,0 +1,46 @@ + + + + + br + Canceled + Center + Color + Colors + f + fmt + g + gdi + hti + i + lightbox + lv + lvi + lvsi + m + multi + Munger + n + olv + olvi + p + parms + r + Renderer + s + SubItem + Unapply + Unpause + x + y + + + ComPlus + + + + + OLV + + + diff --git a/ObjectListView/DataListView.cs b/ObjectListView/DataListView.cs new file mode 100644 index 0000000..16f31ce --- /dev/null +++ b/ObjectListView/DataListView.cs @@ -0,0 +1,199 @@ +/* + * DataListView - A data-bindable listview + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2011-02-27 JPP - Moved most of the logic to DataSourceAdapter (where it + * can be used by FastDataListView too) + * v2.3 + * 2009-01-18 JPP - Boolean columns are now handled as checkboxes + * - Auto-generated columns would fail if the data source was + * reseated, even to the same data source + * v2.0.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-10-03 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing.Design; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + + /// + /// A DataListView is a ListView that can be bound to a datasource (which would normally be a DataTable or DataView). + /// + /// + /// This listview keeps itself in sync with its source datatable by listening for change events. + /// The DataListView will automatically create columns to show all of the data source's columns/properties, if there is not already + /// a column showing that property. This allows you to define one or two columns in the designer and then have the others generated automatically. + /// If you don't want any column to be auto generated, set to false. + /// These generated columns will be only the simplest view of the world, and would look more interesting with a few delegates installed. + /// This listview will also automatically generate missing aspect getters to fetch the values from the data view. + /// Changing data sources is possible, but error prone. Before changing data sources, the programmer is responsible for modifying/resetting + /// the column collection to be valid for the new data source. + /// Internally, a CurrencyManager controls keeping the data source in-sync with other users of the data source (as per normal .NET + /// behavior). This means that the model objects in the DataListView are DataRowView objects. If you write your own AspectGetters/Setters, + /// they will be given DataRowView objects. + /// + public class DataListView : ObjectListView + { + /// + /// Make a DataListView + /// + public DataListView() + { + this.Adapter = new DataSourceAdapter(this); + } + + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + [Category("Data"), + Description("Should the control automatically generate columns from the DataSource"), + DefaultValue(true)] + public bool AutoGenerateColumns { + get { return this.Adapter.AutoGenerateColumns; } + set { this.Adapter.AutoGenerateColumns = value; } + } + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + /// The DataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// DataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// When a DataSource is set, the control will create OLVColumns to show any + /// data source columns that are not already shown. + /// If the DataSource is changed, you will have to remove any previously + /// created columns, since they will be configured for the previous DataSource. + /// . + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource + { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember + { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + protected DataSourceAdapter Adapter { + get { + Debug.Assert(adapter != null, "Data adapter should not be null"); + return adapter; + } + set { adapter = value; } + } + private DataSourceAdapter adapter; + + #endregion + + #region Object manipulations + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// This is a no-op for data lists, since the data + /// is controlled by the VirtualListDataSource. Manipulate the data source + /// rather than this view of the data source. + public override void AddObjects(ICollection modelObjects) + { + } + + /// + /// Remove the given collection of model objects from this control. + /// + /// This is a no-op for data lists, since the data + /// is controlled by the VirtualListDataSource. Manipulate the data source + /// rather than this view of the data source. + public override void RemoveObjects(ICollection modelObjects) + { + } + + #endregion + + #region Event Handlers + + /// + /// Handles parent binding context changes + /// + /// Unused EventArgs. + protected override void OnParentBindingContextChanged(EventArgs e) + { + base.OnParentBindingContextChanged(e); + + // BindingContext is an ambient property - by default it simply picks + // up the parent control's context (unless something has explicitly + // given us our own). So we must respond to changes in our parent's + // binding context in the same way we would changes to our own + // binding context. + + // THINK: Do we need to forward this to the adapter? + } + + #endregion + } +} diff --git a/ObjectListView/DataTreeListView.cs b/ObjectListView/DataTreeListView.cs new file mode 100644 index 0000000..b8e61d7 --- /dev/null +++ b/ObjectListView/DataTreeListView.cs @@ -0,0 +1,226 @@ +/* + * DataTreeListView - A data bindable TreeListView + * + * Author: Phillip Piper + * Date: 05/05/2012 3:26 PM + * + * Change log: + + * 2012-05-05 JPP Initial version + * + * TO DO: + + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing.Design; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A DataTreeListView is a TreeListView that calculates its hierarchy based on + /// information in the data source. + /// + /// + /// Like a , a DataTreeListView sources all its information + /// from a combination of and . + /// can be a DataTable, DataSet, + /// or anything that implements . + /// + /// + /// To function properly, the DataTreeListView requires: + /// + /// the table to have a column which holds a unique for the row. The name of this column must be set in . + /// the table to have a column which holds id of the hierarchical parent of the row. The name of this column must be set in . + /// a value which identifies which rows are the roots of the tree (). + /// + /// The hierarchy structure is determined finding all the rows where the parent key is equal to . These rows + /// become the root objects of the hierarchy. + /// + /// Like a TreeListView, the hierarchy must not contain cycles. Bad things will happen if the data is cyclic. + /// + public partial class DataTreeListView : TreeListView + { + #region Public Properties + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + /// The DataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// DataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + /// + /// Gets or sets the name of the property/column that uniquely identifies each row. + /// + /// + /// + /// The value contained by this column must be unique across all rows + /// in the data source. Odd and unpredictable things will happen if two + /// rows have the same id. + /// + /// Null cannot be a valid key value. + /// + [Category("Data"), + Description("The name of the property/column that holds the key of a row"), + DefaultValue(null)] + public virtual string KeyAspectName { + get { return this.Adapter.KeyAspectName; } + set { this.Adapter.KeyAspectName = value; } + } + + /// + /// Gets or sets the name of the property/column that contains the key of + /// the parent of a row. + /// + /// + /// + /// The test condition for deciding if one row is the parent of another is functionally + /// equivilent to this: + /// + /// Object.Equals(candidateParentRow[this.KeyAspectName], row[this.ParentKeyAspectName]) + /// + /// + /// Unlike key value, parent keys can be null but a null parent key can only be used + /// to identify root objects. + /// + [Category("Data"), + Description("The name of the property/column that holds the key of the parent of a row"), + DefaultValue(null)] + public virtual string ParentKeyAspectName { + get { return this.Adapter.ParentKeyAspectName; } + set { this.Adapter.ParentKeyAspectName = value; } + } + + /// + /// Gets or sets the value that identifies a row as a root object. + /// When the ParentKey of a row equals the RootKeyValue, that row will + /// be treated as root of the TreeListView. + /// + /// + /// + /// The test condition for deciding a root object is functionally + /// equivilent to this: + /// + /// Object.Equals(candidateRow[this.ParentKeyAspectName], this.RootKeyValue) + /// + /// + /// The RootKeyValue can be null. Actually, it can be any value that can + /// be compared for equality against a basic type. + /// If this is set to the wrong value (i.e. to a value that no row + /// has in the parent id column), the list will be empty. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual object RootKeyValue { + get { return this.Adapter.RootKeyValue; } + set { this.Adapter.RootKeyValue = value; } + } + + /// + /// Gets or sets the value that identifies a row as a root object. + /// . The RootKeyValue can be of any type, + /// but the IDE cannot sensibly represent a value of any type, + /// so this is a typed wrapper around that property. + /// + /// + /// If you want the root value to be something other than a string, + /// you will have set it yourself. + /// + [Category("Data"), + Description("The parent id value that identifies a row as a root object"), + DefaultValue(null)] + public virtual string RootKeyValueString { + get { return Convert.ToString(this.Adapter.RootKeyValue); } + set { this.Adapter.RootKeyValue = value; } + } + + /// + /// Gets or sets whether or not the key columns (id and parent id) should + /// be shown to the user. + /// + /// This must be set before the DataSource is set. It has no effect + /// afterwards. + [Category("Data"), + Description("Should the keys columns (id and parent id) be shown to the user?"), + DefaultValue(true)] + public virtual bool ShowKeyColumns { + get { return this.Adapter.ShowKeyColumns; } + set { this.Adapter.ShowKeyColumns = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + protected TreeDataSourceAdapter Adapter { + get { + if (this.adapter == null) + this.adapter = new TreeDataSourceAdapter(this); + return adapter; + } + set { adapter = value; } + } + private TreeDataSourceAdapter adapter; + + #endregion + } +} diff --git a/ObjectListView/DragDrop/DragSource.cs b/ObjectListView/DragDrop/DragSource.cs new file mode 100644 index 0000000..c1a1844 --- /dev/null +++ b/ObjectListView/DragDrop/DragSource.cs @@ -0,0 +1,219 @@ +/* + * DragSource.cs - Add drag source functionality to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2011-03-29 JPP - Separate OLVDataObject.cs + * v2.3 + * 2009-07-06 JPP - Make sure Link is acceptable as an drop effect by default + * (since MS didn't make it part of the 'All' value) + * v2.2 + * 2009-04-15 JPP - Separated DragSource.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware +{ + /// + /// An IDragSource controls how drag out from the ObjectListView will behave + /// + public interface IDragSource + { + /// + /// A drag operation is beginning. Return the data object that will be used + /// for data transfer. Return null to prevent the drag from starting. The data + /// object will normally include all the selected objects. + /// + /// + /// The returned object is later passed to the GetAllowedEffect() and EndDrag() + /// methods. + /// + /// What ObjectListView is being dragged from. + /// Which mouse button is down? + /// What item was directly dragged by the user? There may be more than just this + /// item selected. + /// The data object that will be used for data transfer. This will often be a subclass + /// of DataObject, but does not need to be. + Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item); + + /// + /// What operations are possible for this drag? This controls the icon shown during the drag + /// + /// The data object returned by StartDrag() + /// A combination of DragDropEffects flags + DragDropEffects GetAllowedEffects(Object dragObject); + + /// + /// The drag operation is complete. Do whatever is necessary to complete the action. + /// + /// The data object returned by StartDrag() + /// The value returned from GetAllowedEffects() + void EndDrag(Object dragObject, DragDropEffects effect); + } + + /// + /// A do-nothing implementation of IDragSource that can be safely subclassed. + /// + public class AbstractDragSource : IDragSource + { + #region IDragSource Members + + /// + /// See IDragSource documentation + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + return null; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.None; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + } + + #endregion + } + + /// + /// A reasonable implementation of IDragSource that provides normal + /// drag source functionality. It creates a data object that supports + /// inter-application dragging of text and HTML representation of + /// the dragged rows. It can optionally force a refresh of all dragged + /// rows when the drag is complete. + /// + /// Subclasses can override GetDataObject() to add new + /// data formats to the data transfer object. + public class SimpleDragSource : IDragSource + { + #region Constructors + + /// + /// Construct a SimpleDragSource + /// + public SimpleDragSource() { + } + + /// + /// Construct a SimpleDragSource that refreshes the dragged rows when + /// the drag is complete + /// + /// + public SimpleDragSource(bool refreshAfterDrop) { + this.RefreshAfterDrop = refreshAfterDrop; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets whether the dragged rows should be refreshed when the + /// drag operation is complete. + /// + public bool RefreshAfterDrop { + get { return refreshAfterDrop; } + set { refreshAfterDrop = value; } + } + private bool refreshAfterDrop; + + #endregion + + #region IDragSource Members + + /// + /// Create a DataObject when the user does a left mouse drag operation. + /// See IDragSource for further information. + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + // We only drag on left mouse + if (button != MouseButtons.Left) + return null; + + return this.CreateDataObject(olv); + } + + /// + /// Which operations are allowed in the operation? By default, all operations are supported. + /// + /// + /// All opertions are supported + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.All | DragDropEffects.Link; // why didn't MS include 'Link' in 'All'?? + } + + /// + /// The drag operation is finished. Refreshe the dragged rows if so configured. + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + OLVDataObject data = dragObject as OLVDataObject; + if (data == null) + return; + + if (this.RefreshAfterDrop) + data.ListView.RefreshObjects(data.ModelObjects); + } + + /// + /// Create a data object that will be used to as the data object + /// for the drag operation. + /// + /// + /// Subclasses can override this method add new formats to the data object. + /// + /// The ObjectListView that is the source of the drag + /// A data object for the drag + protected virtual object CreateDataObject(ObjectListView olv) { + return new OLVDataObject(olv); + } + + #endregion + } +} diff --git a/ObjectListView/DragDrop/DropSink.cs b/ObjectListView/DragDrop/DropSink.cs new file mode 100644 index 0000000..e634ecc --- /dev/null +++ b/ObjectListView/DragDrop/DropSink.cs @@ -0,0 +1,1447 @@ +/* + * DropSink.cs - Add drop sink ability to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2011-04-20 JPP - Rewrote how ModelDropEventArgs.RefreshObjects() works on TreeListViews + * v2.4.1 + * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource. + * v2.3 + * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for + * objects that were children but are now roots. + * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after + * a drag-drop operation + * 2009-08-19 JPP - Changed to use OlvHitTest() + * v2.2.1 + * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs + * v2.2 + * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs + * - Tweaked the appearance of the drop-on-background feedback + * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Objects that implement this interface can acts as the receiver for drop + /// operation for an ObjectListView. + /// + public interface IDropSink + { + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + ObjectListView ListView { get; set; } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + void DrawFeedback(Graphics g, Rectangle bounds); + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + void Drop(DragEventArgs args); + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + void Enter(DragEventArgs args); + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + void GiveFeedback(GiveFeedbackEventArgs args); + + /// + /// The drag has left the bounds of this control + /// + void Leave(); + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + void Over(DragEventArgs args); + + /// + /// Should the drag be allowed to continue? + /// + /// + void QueryContinue(QueryContinueDragEventArgs args); + } + + /// + /// This is a do-nothing implementation of IDropSink that is a useful + /// base class for more sophisticated implementations. + /// + public class AbstractDropSink : IDropSink + { + #region IDropSink Members + + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + public virtual ObjectListView ListView { + get { return listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public virtual void DrawFeedback(Graphics g, Rectangle bounds) { + } + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + public virtual void Drop(DragEventArgs args) { + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public virtual void Enter(DragEventArgs args) { + } + + /// + /// The drag has left the bounds of this control + /// + public virtual void Leave() { + this.Cleanup(); + } + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + public virtual void Over(DragEventArgs args) { + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// You only need to override this if you want non-standard cursors. + /// The standard cursors are supplied automatically. + /// + public virtual void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = true; + } + + /// + /// Should the drag be allowed to continue? + /// + /// + /// You only need to override this if you want the user to be able + /// to end the drop in some non-standard way, e.g. dragging to a + /// certain point even without releasing the mouse, or going outside + /// the bounds of the application. + /// + /// + public virtual void QueryContinue(QueryContinueDragEventArgs args) { + } + + + #endregion + + #region Commands + + /// + /// This is called when the mouse leaves the drop region and after the + /// drop has completed. + /// + protected virtual void Cleanup() { + } + + #endregion + } + + /// + /// The enum indicates which target has been found for a drop operation + /// + [Flags] + public enum DropTargetLocation + { + /// + /// No applicable target has been found + /// + None = 0, + + /// + /// The list itself is the target of the drop + /// + Background = 0x01, + + /// + /// An item is the target + /// + Item = 0x02, + + /// + /// Between two items (or above the top item or below the bottom item) + /// can be the target. This is not actually ever a target, only a value indicate + /// that it is valid to drop between items + /// + BetweenItems = 0x04, + + /// + /// Above an item is the target + /// + AboveItem = 0x08, + + /// + /// Below an item is the target + /// + BelowItem = 0x10, + + /// + /// A subitem is the target of the drop + /// + SubItem = 0x20, + + /// + /// On the right of an item is the target (not currently used) + /// + RightOfItem = 0x40, + + /// + /// On the left of an item is the target (not currently used) + /// + LeftOfItem = 0x80 + } + + /// + /// This class represents a simple implementation of a drop sink. + /// + /// + /// Actually, it's far from simple and can do quite a lot in its own right. + /// + public class SimpleDropSink : AbstractDropSink + { + #region Life and death + + /// + /// Make a new drop sink + /// + public SimpleDropSink() { + this.timer = new Timer(); + this.timer.Interval = 250; + this.timer.Tick += new EventHandler(this.timer_Tick); + + this.CanDropOnItem = true; + //this.CanDropOnSubItem = true; + //this.CanDropOnBackground = true; + //this.CanDropBetween = true; + + this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue); + this.billboard = new BillboardOverlay(); + } + + #endregion + + #region Public properties + + /// + /// Get or set the locations where a drop is allowed to occur (OR-ed together) + /// + public DropTargetLocation AcceptableLocations { + get { return this.acceptableLocations; } + set { this.acceptableLocations = value; } + } + private DropTargetLocation acceptableLocations; + + /// + /// Gets or sets whether this sink allows model objects to be dragged from other lists + /// + public bool AcceptExternal { + get { return this.acceptExternal; } + set { this.acceptExternal = value; } + } + private bool acceptExternal = true; + + /// + /// Gets or sets whether the ObjectListView should scroll when the user drags + /// something near to the top or bottom rows. + /// + public bool AutoScroll { + get { return this.autoScroll; } + set { this.autoScroll = value; } + } + private bool autoScroll = true; + + /// + /// Gets the billboard overlay that will be used to display feedback + /// messages during a drag operation. + /// + /// Set this to null to stop the feedback. + public BillboardOverlay Billboard { + get { return this.billboard; } + set { this.billboard = value; } + } + private BillboardOverlay billboard; + + /// + /// Get or set whether a drop can occur between items of the list + /// + public bool CanDropBetween { + get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.BetweenItems; + else + this.AcceptableLocations &= ~DropTargetLocation.BetweenItems; + } + } + + /// + /// Get or set whether a drop can occur on the listview itself + /// + public bool CanDropOnBackground { + get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Background; + else + this.AcceptableLocations &= ~DropTargetLocation.Background; + } + } + + /// + /// Get or set whether a drop can occur on items in the list + /// + public bool CanDropOnItem { + get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Item; + else + this.AcceptableLocations &= ~DropTargetLocation.Item; + } + } + + /// + /// Get or set whether a drop can occur on a subitem in the list + /// + public bool CanDropOnSubItem { + get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.SubItem; + else + this.AcceptableLocations &= ~DropTargetLocation.SubItem; + } + } + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { + if (this.dropTargetIndex != value) { + this.dropTargetIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + } + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { + if (this.dropTargetLocation != value) { + this.dropTargetLocation = value; + this.ListView.Invalidate(); + } + } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { + if (this.dropTargetSubItemIndex != value) { + this.dropTargetSubItemIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get or set the color that will be used to provide drop feedback + /// + public Color FeedbackColor { + get { return this.feedbackColor; } + set { this.feedbackColor = value; } + } + private Color feedbackColor; + + /// + /// Get whether the alt key was down during this drop event + /// + public bool IsAltDown { + get { return (this.KeyState & 32) == 32; } + } + + /// + /// Get whether any modifier key was down during this drop event + /// + public bool IsAnyModifierDown { + get { return (this.KeyState & (4 + 8 + 32)) != 0; } + } + + /// + /// Get whether the control key was down during this drop event + /// + public bool IsControlDown { + get { return (this.KeyState & 8) == 8; } + } + + /// + /// Get whether the left mouse button was down during this drop event + /// + public bool IsLeftMouseButtonDown { + get { return (this.KeyState & 1) == 1; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsMiddleMouseButtonDown { + get { return (this.KeyState & 16) == 16; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsRightMouseButtonDown { + get { return (this.KeyState & 2) == 2; } + } + + /// + /// Get whether the shift key was down during this drop event + /// + public bool IsShiftDown { + get { return (this.KeyState & 4) == 4; } + } + + /// + /// Get or set the state of the keys during this drop event + /// + public int KeyState { + get { return this.keyState; } + set { this.keyState = value; } + } + private int keyState; + + /// + /// Gets or sets whether the drop sink will automatically use cursors + /// based on the drop effect. By default, this is true. If this is + /// set to false, you must set the Cursor yourself. + /// + public bool UseDefaultCursors { + get { return useDefaultCursors; } + set { useDefaultCursors = value; } + } + private bool useDefaultCursors = true; + + #endregion + + #region Events + + /// + /// Triggered when the sink needs to know if a drop can occur. + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler CanDrop; + + /// + /// Triggered when the drop is made. + /// + public event EventHandler Dropped; + + /// + /// Triggered when the sink needs to know if a drop can occur + /// AND the source is an ObjectListView + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler ModelCanDrop; + + /// + /// Triggered when the drop is made. + /// AND the source is an ObjectListView + /// + public event EventHandler ModelDropped; + + #endregion + + #region DropSink Interface + + /// + /// Cleanup the drop sink when the mouse has left the control or + /// the drag has finished. + /// + protected override void Cleanup() { + this.DropTargetLocation = DropTargetLocation.None; + this.ListView.FullRowSelect = this.originalFullRowSelect; + this.Billboard.Text = null; + } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public override void DrawFeedback(Graphics g, Rectangle bounds) { + g.SmoothingMode = ObjectListView.SmoothingMode; + + switch (this.DropTargetLocation) { + case DropTargetLocation.Background: + this.DrawFeedbackBackgroundTarget(g, bounds); + break; + case DropTargetLocation.Item: + this.DrawFeedbackItemTarget(g, bounds); + break; + case DropTargetLocation.AboveItem: + this.DrawFeedbackAboveItemTarget(g, bounds); + break; + case DropTargetLocation.BelowItem: + this.DrawFeedbackBelowItemTarget(g, bounds); + break; + } + + if (this.Billboard != null) { + this.Billboard.Draw(this.ListView, g, bounds); + } + } + + /// + /// The user has released the drop over this control + /// + /// + public override void Drop(DragEventArgs args) { + this.dropEventArgs.DragEventArgs = args; + this.TriggerDroppedEvent(args); + this.timer.Stop(); + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public override void Enter(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Enter"); + + /* + * When FullRowSelect is true, we have two problems: + * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes + * up our calculation of the drop rectangle. + * 2) during the drag, the Timer events will not fire! This is the major problem, since without + * those events we can't autoscroll. + * + * The first problem we can solve through coding, but the second is more difficult. + * We avoid both problems by turning off FullRowSelect during the drop operation. + */ + this.originalFullRowSelect = this.ListView.FullRowSelect; + this.ListView.FullRowSelect = false; + + // Setup our drop event args block + this.dropEventArgs = new ModelDropEventArgs(); + this.dropEventArgs.DropSink = this; + this.dropEventArgs.ListView = this.ListView; + this.dropEventArgs.DragEventArgs = args; + this.dropEventArgs.DataObject = args.Data; + OLVDataObject olvData = args.Data as OLVDataObject; + if (olvData != null) { + this.dropEventArgs.SourceListView = olvData.ListView; + this.dropEventArgs.SourceModels = olvData.ModelObjects; + } + + this.Over(args); + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + public override void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = this.UseDefaultCursors; + } + + /// + /// The drag is moving over this control. + /// + /// + public override void Over(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Over"); + this.dropEventArgs.DragEventArgs = args; + this.KeyState = args.KeyState; + Point pt = this.ListView.PointToClient(new Point(args.X, args.Y)); + args.Effect = this.CalculateDropAction(args, pt); + this.CheckScrolling(pt); + } + + #endregion + + #region Events + + /// + /// Trigger the Dropped events + /// + /// + protected virtual void TriggerDroppedEvent(DragEventArgs args) { + this.dropEventArgs.Handled = false; + + // If the source is an ObjectListView, trigger the ModelDropped event + if (this.dropEventArgs.SourceListView != null) + this.OnModelDropped(this.dropEventArgs); + + if (!this.dropEventArgs.Handled) + this.OnDropped(this.dropEventArgs); + } + + /// + /// Trigger CanDrop + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// Trigger Dropped + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// Trigger ModelCanDrop + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + return; + } + + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// Trigger ModelDropped + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + #endregion + + #region Implementation + + private void timer_Tick(object sender, EventArgs e) { + this.HandleTimerTick(); + } + + /// + /// Handle the timer tick event, which is sent when the listview should + /// scroll + /// + protected virtual void HandleTimerTick() { + + // If the mouse has been released, stop scrolling. + // This is only necessary if the mouse is released outside of the control. + // If the mouse is released inside the control, we would receive a Drop event. + if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) || + (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) || + (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) { + this.timer.Stop(); + this.Cleanup(); + return; + } + + // Auto scrolling will continune while the mouse is close to the ListView + const int GRACE_PERIMETER = 30; + + Point pt = this.ListView.PointToClient(Cursor.Position); + Rectangle r2 = this.ListView.ClientRectangle; + r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER); + if (r2.Contains(pt)) { + this.ListView.LowLevelScroll(0, this.scrollAmount); + } + } + + /// + /// When the mouse is at the given point, what should the target of the drop be? + /// + /// This method should update the DropTarget* members of the given arg block + /// + /// The mouse point, in client co-ordinates + protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) { + const int SMALL_VALUE = 3; + DropTargetLocation location = DropTargetLocation.None; + int targetIndex = -1; + int targetSubIndex = 0; + + if (this.CanDropOnBackground) + location = DropTargetLocation.Background; + + // Which item is the mouse over? + // If it is not over any item, it's over the background. + //ListViewHitTestInfo info = this.ListView.HitTest(pt.X, pt.Y); + OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y); + if (info.Item != null && this.CanDropOnItem) { + location = DropTargetLocation.Item; + targetIndex = info.Item.Index; + if (info.SubItem != null && this.CanDropOnSubItem) + targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem); + } + + // Check to see if the mouse is "between" rows. + // ("between" is somewhat loosely defined) + if (this.CanDropBetween && this.ListView.GetItemCount() > 0) { + + // If the mouse is over an item, check to see if it is near the top or bottom + if (location == DropTargetLocation.Item) { + if (pt.Y - SMALL_VALUE <= info.Item.Bounds.Top) + location = DropTargetLocation.AboveItem; + if (pt.Y + SMALL_VALUE >= info.Item.Bounds.Bottom) + location = DropTargetLocation.BelowItem; + } else { + // Is there an item a little below the mouse? + // If so, we say the drop point is above that row + info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.AboveItem; + } else { + // Is there an item a little above the mouse? + info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.BelowItem; + } + } + } + } + + args.DropTargetLocation = location; + args.DropTargetIndex = targetIndex; + args.DropTargetSubItemIndex = targetSubIndex; + } + + /// + /// What sort of action is possible when the mouse is at the given point? + /// + /// + /// + /// + /// + /// + public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) { + + this.CalculateDropTarget(this.dropEventArgs, pt); + + this.dropEventArgs.MouseLocation = pt; + this.dropEventArgs.InfoMessage = null; + this.dropEventArgs.Handled = false; + + if (this.dropEventArgs.SourceListView != null) { + this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex); + this.OnModelCanDrop(this.dropEventArgs); + } + + if (!this.dropEventArgs.Handled) + this.OnCanDrop(this.dropEventArgs); + + this.UpdateAfterCanDropEvent(this.dropEventArgs); + + return this.dropEventArgs.Effect; + } + + /// + /// Based solely on the state of the modifier keys, what drop operation should + /// be used? + /// + /// The drop operation that matches the state of the keys + public DragDropEffects CalculateStandardDropActionFromKeys() { + if (this.IsControlDown) { + if (this.IsShiftDown) + return DragDropEffects.Link; + else + return DragDropEffects.Copy; + } else { + return DragDropEffects.Move; + } + } + + /// + /// Should the listview be made to scroll when the mouse is at the given point? + /// + /// + protected virtual void CheckScrolling(Point pt) { + if (!this.AutoScroll) + return; + + Rectangle r = this.ListView.ContentRectangle; + int rowHeight = this.ListView.RowHeightEffective; + int close = rowHeight; + + // In Tile view, using the whole row height is too much + if (this.ListView.View == View.Tile) + close /= 2; + + if (pt.Y <= (r.Top + close)) { + // Scroll faster if the mouse is closer to the top + this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = -rowHeight; + } else { + if (pt.Y >= (r.Bottom - close)) { + this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = rowHeight; + } else { + this.timer.Stop(); + } + } + } + + /// + /// Update the state of our sink to reflect the information that + /// may have been written into the drop event args. + /// + /// + protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) { + this.DropTargetIndex = args.DropTargetIndex; + this.DropTargetLocation = args.DropTargetLocation; + this.DropTargetSubItemIndex = args.DropTargetSubItemIndex; + + if (this.Billboard != null) { + Point pt = args.MouseLocation; + pt.Offset(5, 5); + if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) { + this.Billboard.Text = this.dropEventArgs.InfoMessage; + this.Billboard.Location = pt; + this.ListView.Invalidate(); + } + } + } + + #endregion + + #region Rendering + + /// + /// Draw the feedback that shows that the background is the target + /// + /// + /// + protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) { + float penWidth = 12.0f; + Rectangle r = bounds; + r.Inflate((int)-penWidth / 2, (int)-penWidth / 2); + using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) { + using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows that an item (or a subitem) is the target + /// + /// + /// + /// + /// DropTargetItem and DropTargetSubItemIndex tells what is the target + /// + protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + r.Inflate(1, 1); + float diameter = r.Height / 3; + using (GraphicsPath path = this.GetRoundedRect(r, diameter)) { + using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) { + g.FillPath(b, path); + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows the drop will occur before target + /// + /// + /// + protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top); + } + + /// + /// Draw the feedback that shows the drop will occur after target + /// + /// + /// + protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom); + } + + /// + /// Return a GraphicPath that is round corner rectangle. + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + + return path; + } + + /// + /// Calculate the target rectangle when the given item (and possible subitem) + /// is the target of the drop. + /// + /// + /// + /// + protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) { + if (subItem > 0) + return item.SubItems[subItem].Bounds; + + Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem); + + // Allow for indent + if (item.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width; + r.X += (indentWidth * item.IndentCount); + r.Width -= (indentWidth * item.IndentCount); + } + + return r; + } + + /// + /// Draw a "between items" line at the given co-ordinates + /// + /// + /// + /// + /// + /// + protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) { + using (Brush b = new SolidBrush(this.FeedbackColor)) { + int x = x1; + int y = y1; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 5, + x, y - 5); + gp.AddBezier( + x, y - 6, + x + 3, y - 2, + x + 6, y - 1, + x + 11, y); + gp.AddBezier( + x + 11, y, + x + 6, y + 1, + x + 3, y + 2, + x, y + 6); + gp.CloseFigure(); + g.FillPath(b, gp); + } + x = x2; + y = y2; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 6, + x, y - 6); + gp.AddBezier( + x, y - 7, + x - 3, y - 2, + x - 6, y - 1, + x - 11, y); + gp.AddBezier( + x - 11, y, + x - 6, y + 1, + x - 3, y + 2, + x, y + 7); + gp.CloseFigure(); + g.FillPath(b, gp); + } + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawLine(p, x1, y1, x2, y2); + } + } + + #endregion + + private Timer timer; + private int scrollAmount; + private bool originalFullRowSelect; + private ModelDropEventArgs dropEventArgs; + } + + /// + /// This drop sink allows items within the same list to be rearranged, + /// as well as allowing items to be dropped from other lists. + /// + /// + /// + /// This class can only be used on plain ObjectListViews and FastObjectListViews. + /// The other flavours have no way to implement the insert operation that is required. + /// + /// + /// This class does not work with grouping. + /// + /// + /// This class works when the OLV is sorted, but it is up to the programmer + /// to decide what rearranging such lists "means". Example: if the control is sorting + /// students by academic grade, and the user drags a "Fail" grade student up amonst the "A+" + /// students, it is the responsibility of the programmer to makes the appropriate changes + /// to the model and redraw/rebuild the control so that the users action makes sense. + /// + /// + /// Users of this class should listen for the CanDrop event to decide + /// if models from another OLV can be moved to OLV under this sink. + /// + /// + public class RearrangingDropSink : SimpleDropSink + { + /// + /// Create a RearrangingDropSink + /// + public RearrangingDropSink() { + this.CanDropBetween = true; + this.CanDropOnBackground = true; + this.CanDropOnItem = false; + } + + /// + /// Create a RearrangingDropSink + /// + /// + public RearrangingDropSink(bool acceptDropsFromOtherLists) + : this() { + this.AcceptExternal = acceptDropsFromOtherLists; + } + + /// + /// Trigger OnModelCanDrop + /// + /// + protected override void OnModelCanDrop(ModelDropEventArgs args) { + base.OnModelCanDrop(args); + + if (args.Handled) + return; + + args.Effect = DragDropEffects.Move; + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + } + + // If we are rearranging a list, don't allow drops on the background + if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + } + } + + /// + /// Trigger OnModelDropped + /// + /// + protected override void OnModelDropped(ModelDropEventArgs args) { + base.OnModelDropped(args); + + if (!args.Handled) + this.RearrangeModels(args); + } + + /// + /// Do the work of processing the dropped items + /// + /// + public virtual void RearrangeModels(ModelDropEventArgs args) { + switch (args.DropTargetLocation) { + case DropTargetLocation.AboveItem: + this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels); + break; + case DropTargetLocation.BelowItem: + this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels); + break; + case DropTargetLocation.Background: + this.ListView.AddObjects(args.SourceModels); + break; + default: + return; + } + + if (args.SourceListView != this.ListView && !args.PreserveModels) { + args.SourceListView.RemoveObjects(args.SourceModels); + } + } + } + + /// + /// When a drop sink needs to know if something can be dropped, or + /// to notify that a drop has occured, it uses an instance of this class. + /// + public class OlvDropEventArgs : EventArgs + { + /// + /// Create a OlvDropEventArgs + /// + public OlvDropEventArgs() { + } + + #region Data Properties + + /// + /// Get the original drag-drop event args + /// + public DragEventArgs DragEventArgs + { + get { return this.dragEventArgs; } + internal set { this.dragEventArgs = value; } + } + private DragEventArgs dragEventArgs; + + /// + /// Get the data object that is being dragged + /// + public object DataObject + { + get { return this.dataObject; } + internal set { this.dataObject = value; } + } + private object dataObject; + + /// + /// Get the drop sink that originated this event + /// + public SimpleDropSink DropSink { + get { return this.dropSink; } + internal set { this.dropSink = value; } + } + private SimpleDropSink dropSink; + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { this.dropTargetIndex = value; } + } + private int dropTargetIndex = -1; + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { this.dropTargetLocation = value; } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { this.dropTargetSubItemIndex = value; } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + set { + if (value == null) + this.DropTargetIndex = -1; + else + this.DropTargetIndex = value.Index; + } + } + + /// + /// Get or set the drag effect that should be used for this operation + /// + public DragDropEffects Effect { + get { return this.effect; } + set { this.effect = value; } + } + private DragDropEffects effect; + + /// + /// Get or set if this event was handled. No further processing will be done for a handled event. + /// + public bool Handled { + get { return this.handled; } + set { this.handled = value; } + } + private bool handled; + + /// + /// Get or set if models from this event should be removed from source list. Ignored if Handled is true. + /// + public bool PreserveModels { + get { return this.preserveModels; } + set { this.preserveModels = value; } + } + private bool preserveModels; + + + + + /// + /// Get or set the feedback message for this operation + /// + /// + /// If this is not null, it will be displayed as a feedback message + /// during the drag. + /// + public string InfoMessage { + get { return this.infoMessage; } + set { this.infoMessage = value; } + } + private string infoMessage; + + /// + /// Get the ObjectListView that is being dropped on + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Get the location of the mouse (in target ListView co-ords) + /// + public Point MouseLocation { + get { return this.mouseLocation; } + internal set { this.mouseLocation = value; } + } + private Point mouseLocation; + + /// + /// Get the drop action indicated solely by the state of the modifier keys + /// + public DragDropEffects StandardDropActionFromKeys { + get { + return this.DropSink.CalculateStandardDropActionFromKeys(); + } + } + + #endregion + } + + /// + /// These events are triggered when the drag source is an ObjectListView. + /// + public class ModelDropEventArgs : OlvDropEventArgs + { + /// + /// Create a ModelDropEventArgs + /// + public ModelDropEventArgs() + { + } + + /// + /// Gets the model objects that are being dragged. + /// + public IList SourceModels { + get { return this.dragModels; } + internal set { + this.dragModels = value; + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + } + } + private IList dragModels; + private ArrayList toBeRefreshed = new ArrayList(); + + /// + /// Gets the ObjectListView that is the source of the dragged objects. + /// + public ObjectListView SourceListView { + get { return this.sourceListView; } + internal set { this.sourceListView = value; } + } + private ObjectListView sourceListView; + + /// + /// Get the model object that is being dropped upon. + /// + /// This is only value for TargetLocation == Item + public object TargetModel { + get { return this.targetModel; } + internal set { this.targetModel = value; } + } + private object targetModel; + + /// + /// Refresh all the objects involved in the operation + /// + public void RefreshObjects() { + + toBeRefreshed.AddRange(this.SourceModels); + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv == null) + this.SourceListView.RefreshObjects(toBeRefreshed); + else + tlv.RebuildAll(true); + + TreeListView tlv2 = this.ListView as TreeListView; + if (tlv2 == null) + this.ListView.RefreshObject(this.TargetModel); + else + tlv2.RebuildAll(true); + } + } +} diff --git a/ObjectListView/DragDrop/OLVDataObject.cs b/ObjectListView/DragDrop/OLVDataObject.cs new file mode 100644 index 0000000..8047554 --- /dev/null +++ b/ObjectListView/DragDrop/OLVDataObject.cs @@ -0,0 +1,185 @@ +/* + * OLVDataObject.cs - An OLE DataObject that knows how to convert rows of an OLV to text and HTML + * + * Author: Phillip Piper + * Date: 2011-03-29 3:34PM + * + * Change log: + * v2.8 + * 2014-05-02 JPP - When the listview is completely empty, don't try to set CSV text in the clipboard. + * v2.6 + * 2012-08-08 JPP - Changed to use OLVExporter. + * - Added CSV to formats exported to Clipboard + * v2.4 + * 2011-03-29 JPP - Initial version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// A data transfer object that knows how to transform a list of model + /// objects into a text and HTML representation. + /// + public class OLVDataObject : DataObject { + #region Life and death + + /// + /// Create a data object from the selected objects in the given ObjectListView + /// + /// The source of the data object + public OLVDataObject(ObjectListView olv) + : this(olv, olv.SelectedObjects) { + } + + /// + /// Create a data object which operates on the given model objects + /// in the given ObjectListView + /// + /// The source of the data object + /// The model objects to be put into the data object + public OLVDataObject(ObjectListView olv, IList modelObjects) { + this.objectListView = olv; + this.modelObjects = modelObjects; + this.includeHiddenColumns = olv.IncludeHiddenColumnsInDataTransfer; + this.includeColumnHeaders = olv.IncludeColumnHeadersInCopy; + this.CreateTextFormats(); + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the text + /// and HTML representation. If this is false, only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + } + private readonly bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. + /// + public bool IncludeColumnHeaders { + get { return includeColumnHeaders; } + } + private readonly bool includeColumnHeaders; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// + public ObjectListView ListView { + get { return objectListView; } + } + private readonly ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + } + private readonly IList modelObjects; + + #endregion + + /// + /// Put a text and HTML representation of our model objects + /// into the data object. + /// + public void CreateTextFormats() { + + OLVExporter exporter = this.CreateExporter(); + + // Put both the text and html versions onto the clipboard. + // For some reason, SetText() with UnicodeText doesn't set the basic CF_TEXT format, + // but using SetData() does. + //this.SetText(sbText.ToString(), TextDataFormat.UnicodeText); + this.SetData(exporter.ExportTo(OLVExporter.ExportFormat.TabSeparated)); + string exportTo = exporter.ExportTo(OLVExporter.ExportFormat.CSV); + if (!String.IsNullOrEmpty(exportTo)) + this.SetText(exportTo, TextDataFormat.CommaSeparatedValue); + this.SetText(ConvertToHtmlFragment(exporter.ExportTo(OLVExporter.ExportFormat.HTML)), TextDataFormat.Html); + } + + /// + /// Create an exporter for the data contained in this object + /// + /// + protected OLVExporter CreateExporter() { + OLVExporter exporter = new OLVExporter(this.ListView); + exporter.IncludeColumnHeaders = this.IncludeColumnHeaders; + exporter.IncludeHiddenColumns = this.IncludeHiddenColumns; + exporter.ModelObjects = this.ModelObjects; + return exporter; + } + + /// + /// Make a HTML representation of our model objects + /// + [Obsolete("Use OLVExporter directly instead", false)] + public string CreateHtml() { + OLVExporter exporter = this.CreateExporter(); + return exporter.ExportTo(OLVExporter.ExportFormat.HTML); + } + + /// + /// Convert the fragment of HTML into the Clipboards HTML format. + /// + /// The HTML format is found here http://msdn2.microsoft.com/en-us/library/aa767917.aspx + /// + /// The HTML to put onto the clipboard. It must be valid HTML! + /// A string that can be put onto the clipboard and will be recognized as HTML + private string ConvertToHtmlFragment(string fragment) { + // Minimal implementation of HTML clipboard format + const string SOURCE = "http://www.codeproject.com/Articles/16009/A-Much-Easier-to-Use-ListView"; + + const String MARKER_BLOCK = + "Version:1.0\r\n" + + "StartHTML:{0,8}\r\n" + + "EndHTML:{1,8}\r\n" + + "StartFragment:{2,8}\r\n" + + "EndFragment:{3,8}\r\n" + + "StartSelection:{2,8}\r\n" + + "EndSelection:{3,8}\r\n" + + "SourceURL:{4}\r\n" + + "{5}"; + + int prefixLength = String.Format(MARKER_BLOCK, 0, 0, 0, 0, SOURCE, "").Length; + + const String DEFAULT_HTML_BODY = + "" + + "{0}"; + + string html = String.Format(DEFAULT_HTML_BODY, fragment); + int startFragment = prefixLength + html.IndexOf(fragment, StringComparison.Ordinal); + int endFragment = startFragment + fragment.Length; + + return String.Format(MARKER_BLOCK, prefixLength, prefixLength + html.Length, startFragment, endFragment, SOURCE, html); + } + } +} diff --git a/ObjectListView/FastDataListView.cs b/ObjectListView/FastDataListView.cs new file mode 100644 index 0000000..785dcfb --- /dev/null +++ b/ObjectListView/FastDataListView.cs @@ -0,0 +1,140 @@ +/* + * FastDataListView - A data bindable listview that has the speed of a virtual list + * + * Author: Phillip Piper + * Date: 22/09/2010 8:11 AM + * + * Change log: + * 2010-09-22 JPP - Initial version + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.ComponentModel; +using System.Windows.Forms; +using System.Drawing.Design; + +namespace BrightIdeasSoftware +{ + /// + /// A FastDataListView virtualizes the display of data from a DataSource. It operates on + /// DataSets and DataTables in the same way as a DataListView, but does so much more efficiently. + /// + /// + /// + /// A FastDataListView still has to load all its data from the DataSource. If you have SQL statement + /// that returns 1 million rows, all 1 million rows will still need to read from the database. + /// However, once the rows are loaded, the FastDataListView will only build rows as they are displayed. + /// + /// + public class FastDataListView : FastObjectListView + { + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + [Category("Data"), + Description("Should the control automatically generate columns from the DataSource"), + DefaultValue(true)] + public bool AutoGenerateColumns + { + get { return this.Adapter.AutoGenerateColumns; } + set { this.Adapter.AutoGenerateColumns = value; } + } + + /// + /// Get or set the VirtualListDataSource that will be displayed in this list view. + /// + /// The VirtualListDataSource should implement either , , + /// or . Some common examples are the following types of objects: + /// + /// + /// + /// + /// + /// + /// + /// When binding to a list container (i.e. one that implements the + /// interface, such as ) + /// you must also set the property in order + /// to identify which particular list you would like to display. You + /// may also set the property even when + /// VirtualListDataSource refers to a list, since can + /// also be used to navigate relations between lists. + /// + [Category("Data"), + TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")] + public virtual Object DataSource { + get { return this.Adapter.DataSource; } + set { this.Adapter.DataSource = value; } + } + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + [Category("Data"), + Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design", typeof(UITypeEditor)), + DefaultValue("")] + public virtual string DataMember { + get { return this.Adapter.DataMember; } + set { this.Adapter.DataMember = value; } + } + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the DataSourceAdaptor that does the bulk of the work needed + /// for data binding. + /// + protected DataSourceAdapter Adapter { + get { + if (adapter == null) + adapter = this.CreateDataSourceAdapter(); + return adapter; + } + set { adapter = value; } + } + private DataSourceAdapter adapter; + + #endregion + + #region Implementation + + /// + /// Create the DataSourceAdapter that this control will use. + /// + /// A DataSourceAdapter configured for this list + /// Subclasses should overrride this to create their + /// own specialized adapters + protected virtual DataSourceAdapter CreateDataSourceAdapter() { + return new DataSourceAdapter(this); + } + + #endregion + } +} diff --git a/ObjectListView/FastObjectListView.cs b/ObjectListView/FastObjectListView.cs new file mode 100644 index 0000000..eba2102 --- /dev/null +++ b/ObjectListView/FastObjectListView.cs @@ -0,0 +1,360 @@ +/* + * FastObjectListView - A listview that behaves like an ObjectListView but has the speed of a virtual list + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * 2012-06-11 JPP - Added more efficient version of FilteredObjects + * v2.5.1 + * 2011-04-25 JPP - Fixed problem with removing objects from filtered or sorted list + * v2.4 + * 2010-04-05 JPP - Added filtering + * v2.3 + * 2009-08-27 JPP - Added GroupingStrategy + * - Added optimized Objects property + * v2.2.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A FastObjectListView trades function for speed. + /// + /// + /// On my mid-range laptop, this view builds a list of 10,000 objects in 0.1 seconds, + /// as opposed to a normal ObjectListView which takes 10-15 seconds. Lists of up to 50,000 items should be + /// able to be handled with sub-second response times even on low end machines. + /// + /// A FastObjectListView is implemented as a virtual list with many of the virtual modes limits (e.g. no sorting) + /// fixed through coding. There are some functions that simply cannot be provided. Specifically, a FastObjectListView cannot: + /// + /// use Tile view + /// show groups on XP + /// + /// + /// + public class FastObjectListView : VirtualObjectListView + { + /// + /// Make a FastObjectListView + /// + public FastObjectListView() { + this.VirtualListDataSource = new FastObjectListDataSource(this); + this.GroupingStrategy = new FastListGroupingStrategy(); + } + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable FilteredObjects { + get { + // This is much faster than the base method + return ((FastObjectListDataSource)this.VirtualListDataSource).FilteredObjectList; + } + } + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// This method preserves selection, if possible. Use SetObjects() if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code and performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { + // This is much faster than the base method + return ((FastObjectListDataSource)this.VirtualListDataSource).ObjectList; + } + set { base.Objects = value; } + } + + /// + /// Remove any sorting and revert to the given order of the model objects + /// + /// To be really honest, Unsort() doesn't work on FastObjectListViews since + /// the original ordering of model objects is lost when Sort() is called. So this method + /// effectively just turns off sorting. + public override void Unsort() { + this.ShowGroups = false; + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + this.SetObjects(this.Objects); + } + } + + /// + /// Provide a data source for a FastObjectListView + /// + /// + /// This class isn't intended to be used directly, but it is left as a public + /// class just in case someone wants to subclass it. + /// + public class FastObjectListDataSource : AbstractVirtualListDataSource + { + /// + /// Create a FastObjectListDataSource + /// + /// + public FastObjectListDataSource(FastObjectListView listView) + : base(listView) { + } + + #region IVirtualListDataSource Members + + /// + /// Get n'th object + /// + /// + /// + public override object GetNthObject(int n) { + if (n >= 0 && n < this.filteredObjectList.Count) + return this.filteredObjectList[n]; + + return null; + } + + /// + /// How many items are in the data source + /// + /// + public override int GetObjectCount() { + return this.filteredObjectList.Count; + } + + /// + /// Get the index of the given model + /// + /// + /// + public override int GetObjectIndex(object model) { + int index; + + if (model != null && this.objectsToIndexMap.TryGetValue(model, out index)) + return index; + + return -1; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int SearchText(string text, int first, int last, OLVColumn column) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(this.listView.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + /// + /// + /// + /// + /// + public override void Sort(OLVColumn column, SortOrder sortOrder) { + if (sortOrder != SortOrder.None) { + ModelObjectComparer comparer = new ModelObjectComparer(column, sortOrder, this.listView.SecondarySortColumn, this.listView.SecondarySortOrder); + this.fullObjectList.Sort(comparer); + this.filteredObjectList.Sort(comparer); + } + this.RebuildIndexMap(); + } + + /// + /// + /// + /// + public override void AddObjects(ICollection modelObjects) { + foreach (object modelObject in modelObjects) { + if (modelObject != null) + this.fullObjectList.Add(modelObject); + } + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// Remove the given collection of models from this source. + /// + /// + public override void RemoveObjects(ICollection modelObjects) { + List indicesToRemove = new List(); + foreach (object modelObject in modelObjects) { + int i = this.GetObjectIndex(modelObject); + if (i >= 0) + indicesToRemove.Add(i); + + // Remove the objects from the unfiltered list + this.fullObjectList.Remove(modelObject); + } + + // Sort the indices from highest to lowest so that we + // remove latter ones before earlier ones. In this way, the + // indices of the rows doesn't change after the deletes. + indicesToRemove.Sort(); + indicesToRemove.Reverse(); + + foreach (int i in indicesToRemove) + this.listView.SelectedIndices.Remove(i); + + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// + /// + /// + public override void SetObjects(IEnumerable collection) { + ArrayList newObjects = ObjectListView.EnumerableToArray(collection, true); + + this.fullObjectList = newObjects; + this.FilterObjects(); + this.RebuildIndexMap(); + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public override void UpdateObject(int index, object modelObject) { + if (index < 0 || index >= this.filteredObjectList.Count) + return; + + int i = this.fullObjectList.IndexOf(this.filteredObjectList[index]); + if (i < 0) + return; + + this.fullObjectList[i] = modelObject; + this.filteredObjectList[index] = modelObject; + this.objectsToIndexMap[modelObject] = index; + } + + private ArrayList fullObjectList = new ArrayList(); + private ArrayList filteredObjectList = new ArrayList(); + private IModelFilter modelFilter; + private IListFilter listFilter; + + #endregion + + #region IFilterableDataSource Members + + /// + /// Apply the given filters to this data source. One or both may be null. + /// + /// + /// + public override void ApplyFilters(IModelFilter iModelFilter, IListFilter iListFilter) { + this.modelFilter = iModelFilter; + this.listFilter = iListFilter; + this.SetObjects(this.fullObjectList); + } + + #endregion + + #region Implementation + + /// + /// Gets the full list of objects being used for this fast list. + /// This list is unfiltered. + /// + public ArrayList ObjectList { + get { return fullObjectList; } + } + + /// + /// Gets the list of objects from ObjectList which survive any installed filters. + /// + public ArrayList FilteredObjectList { + get { return filteredObjectList; } + } + + /// + /// Rebuild the map that remembers which model object is displayed at which line + /// + protected void RebuildIndexMap() { + this.objectsToIndexMap.Clear(); + for (int i = 0; i < this.filteredObjectList.Count; i++) + this.objectsToIndexMap[this.filteredObjectList[i]] = i; + } + readonly Dictionary objectsToIndexMap = new Dictionary(); + + /// + /// Build our filtered list from our full list. + /// + protected void FilterObjects() { + if (!this.listView.UseFiltering || (this.modelFilter == null && this.listFilter == null)) { + this.filteredObjectList = new ArrayList(this.fullObjectList); + return; + } + + IEnumerable objects = (this.listFilter == null) ? + this.fullObjectList : this.listFilter.Filter(this.fullObjectList); + + // Apply the object filter if there is one + if (this.modelFilter == null) { + this.filteredObjectList = ObjectListView.EnumerableToArray(objects, false); + } else { + this.filteredObjectList = new ArrayList(); + foreach (object model in objects) { + if (this.modelFilter.Filter(model)) + this.filteredObjectList.Add(model); + } + } + } + + #endregion + } + +} diff --git a/ObjectListView/Filtering/Cluster.cs b/ObjectListView/Filtering/Cluster.cs new file mode 100644 index 0000000..32fd5a1 --- /dev/null +++ b/ObjectListView/Filtering/Cluster.cs @@ -0,0 +1,125 @@ +/* + * Cluster - Implements a simple cluster + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// Concrete implementation of the ICluster interface. + /// + public class Cluster : ICluster { + + #region Life and death + + /// + /// Create a cluster + /// + /// The key for the cluster + public Cluster(object key) { + this.Count = 1; + this.ClusterKey = key; + } + + #endregion + + #region Public overrides + + /// + /// Return a string representation of this cluster + /// + /// + public override string ToString() { + return this.DisplayLabel ?? "[empty]"; + } + + #endregion + + #region Implementation of ICluster + + /// + /// Gets or sets how many items belong to this cluster + /// + public int Count { + get { return count; } + set { count = value; } + } + private int count; + + /// + /// Gets or sets the label that will be shown to the user to represent + /// this cluster + /// + public string DisplayLabel { + get { return displayLabel; } + set { displayLabel = value; } + } + private string displayLabel; + + /// + /// Gets or sets the actual data object that all members of this cluster + /// have commonly returned. + /// + public object ClusterKey { + get { return clusterKey; } + set { clusterKey = value; } + } + private object clusterKey; + + #endregion + + #region Implementation of IComparable + + /// + /// Return an indication of the ordering between this object and the given one + /// + /// + /// + public int CompareTo(object other) { + if (other == null || other == System.DBNull.Value) + return 1; + + ICluster otherCluster = other as ICluster; + if (otherCluster == null) + return 1; + + string keyAsString = this.ClusterKey as string; + if (keyAsString != null) + return String.Compare(keyAsString, otherCluster.ClusterKey as string, StringComparison.CurrentCultureIgnoreCase); + + IComparable keyAsComparable = this.ClusterKey as IComparable; + if (keyAsComparable != null) + return keyAsComparable.CompareTo(otherCluster.ClusterKey); + + return -1; + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/ClusteringStrategy.cs b/ObjectListView/Filtering/ClusteringStrategy.cs new file mode 100644 index 0000000..891c77e --- /dev/null +++ b/ObjectListView/Filtering/ClusteringStrategy.cs @@ -0,0 +1,189 @@ +/* + * ClusteringStrategy - Implements a simple clustering strategy + * + * Author: Phillip Piper + * Date: 3-March-2011 10:53 pm + * + * Change log: + * 2011-03-03 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// This class provides a useful base implemention of a clustering + /// strategy where the clusters are grouped around the value of a given column. + /// + public class ClusteringStrategy : IClusteringStrategy { + + #region Static properties + + /// + /// This field is the text that will be shown to the user when a cluster + /// key is null. It is exposed so it can be localized. + /// + static public string NULL_LABEL = "[null]"; + + /// + /// This field is the text that will be shown to the user when a cluster + /// key is empty (i.e. a string of zero length). It is exposed so it can be localized. + /// + static public string EMPTY_LABEL = "[empty]"; + + /// + /// Gets or sets the format that will be used by default for clusters that only + /// contain 1 item. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster (always 1 in this case) + /// + static public string DefaultDisplayLabelFormatSingular { + get { return defaultDisplayLabelFormatSingular; } + set { defaultDisplayLabelFormatSingular = value; } + } + static private string defaultDisplayLabelFormatSingular = "{0} ({1} item)"; + + /// + /// Gets or sets the format that will be used by default for clusters that + /// contain 0 or two or more items. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster + /// + static public string DefaultDisplayLabelFormatPlural { + get { return defaultDisplayLabelFormatPural; } + set { defaultDisplayLabelFormatPural = value; } + } + static private string defaultDisplayLabelFormatPural = "{0} ({1} items)"; + + #endregion + + #region Life and death + + /// + /// Create a clustering strategy + /// + public ClusteringStrategy() { + this.DisplayLabelFormatSingular = DefaultDisplayLabelFormatSingular; + this.DisplayLabelFormatPlural = DefaultDisplayLabelFormatPlural; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets the column upon which this strategy is operating + /// + public OLVColumn Column { + get { return column; } + set { column = value; } + } + private OLVColumn column; + + /// + /// Gets or sets the format that will be used when the cluster + /// contains only 1 item. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster (always 1 in this case) + /// + /// If this is not set, the value from + /// ClusteringStrategy.DefaultDisplayLabelFormatSingular will be used + public string DisplayLabelFormatSingular { + get { return displayLabelFormatSingular; } + set { displayLabelFormatSingular = value; } + } + private string displayLabelFormatSingular; + + /// + /// Gets or sets the format that will be used when the cluster + /// contains 0 or two or more items. The format string must accept two placeholders: + /// - {0} is the cluster key converted to a string + /// - {1} is the number of items in the cluster + /// + /// If this is not set, the value from + /// ClusteringStrategy.DefaultDisplayLabelFormatPlural will be used + public string DisplayLabelFormatPlural { + get { return displayLabelFormatPural; } + set { displayLabelFormatPural = value; } + } + private string displayLabelFormatPural; + + #endregion + + #region ICluster implementation + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + virtual public object GetClusterKey(object model) { + return this.Column.GetValue(model); + } + + /// + /// Create a cluster to hold the given cluster key + /// + /// + /// + virtual public ICluster CreateCluster(object clusterKey) { + return new Cluster(clusterKey); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + virtual public string GetClusterDisplayLabel(ICluster cluster) { + string s = this.Column.ValueToString(cluster.ClusterKey) ?? NULL_LABEL; + if (String.IsNullOrEmpty(s)) + s = EMPTY_LABEL; + return this.ApplyDisplayFormat(cluster, s); + } + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + virtual public IModelFilter CreateFilter(IList valuesChosenForFiltering) { + return new OneOfFilter(this.GetClusterKey, valuesChosenForFiltering); + } + + /// + /// Create a label that combines the string representation of the cluster + /// key with a format string that holds an "X [N items in cluster]" type layout. + /// + /// + /// + /// + virtual protected string ApplyDisplayFormat(ICluster cluster, string s) { + string format = (cluster.Count == 1) ? this.DisplayLabelFormatSingular : this.DisplayLabelFormatPlural; + return String.IsNullOrEmpty(format) ? s : String.Format(format, s, cluster.Count); + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs b/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs new file mode 100644 index 0000000..7d3ea66 --- /dev/null +++ b/ObjectListView/Filtering/ClustersFromGroupsStrategy.cs @@ -0,0 +1,70 @@ +/* + * ClusteringStrategy - Implements a simple clustering strategy + * + * Author: Phillip Piper + * Date: 1-April-2011 8:12am + * + * Change log: + * 2011-04-01 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// This class calculates clusters from the groups that the column uses. + /// + /// + /// + /// This is the default strategy for all non-date, filterable columns. + /// + /// + /// This class does not strictly mimic the groups created by the given column. + /// In particular, if the programmer changes the default grouping technique + /// by listening for grouping events, this class will not mimic that behaviour. + /// + /// + public class ClustersFromGroupsStrategy : ClusteringStrategy { + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + return this.Column.GetGroupKey(model); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + string s = this.Column.ConvertGroupKeyToTitle(cluster.ClusterKey); + if (String.IsNullOrEmpty(s)) + s = EMPTY_LABEL; + return this.ApplyDisplayFormat(cluster, s); + } + } +} diff --git a/ObjectListView/Filtering/DateTimeClusteringStrategy.cs b/ObjectListView/Filtering/DateTimeClusteringStrategy.cs new file mode 100644 index 0000000..e0e420b --- /dev/null +++ b/ObjectListView/Filtering/DateTimeClusteringStrategy.cs @@ -0,0 +1,187 @@ +/* + * DateTimeClusteringStrategy - A strategy to cluster objects by a date time + * + * Author: Phillip Piper + * Date: 30-March-2011 9:40am + * + * Change log: + * 2011-03-30 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Globalization; + +namespace BrightIdeasSoftware { + + /// + /// This enum is used to indicate various portions of a datetime + /// + [Flags] + public enum DateTimePortion { + /// + /// Year + /// + Year = 0x01, + + /// + /// Month + /// + Month = 0x02, + + /// + /// Day of the month + /// + Day = 0x04, + + /// + /// Hour + /// + Hour = 0x08, + + /// + /// Minute + /// + Minute = 0x10, + + /// + /// Second + /// + Second = 0x20 + } + + /// + /// This class implements a strategy where the model objects are clustered + /// according to some portion of the datetime value in the configured column. + /// + /// To create a strategy that grouped people who were born in + /// the same month, you would create a strategy that extracted just + /// the month, and formatted it to show just the month's name. Like this: + /// + /// + /// someColumn.ClusteringStrategy = new DateTimeClusteringStrategy(DateTimePortion.Month, "MMMM"); + /// + public class DateTimeClusteringStrategy : ClusteringStrategy { + #region Life and death + + /// + /// Create a strategy that clusters by month/year + /// + public DateTimeClusteringStrategy() + : this(DateTimePortion.Year | DateTimePortion.Month, "MMMM yyyy") { + } + + /// + /// Create a strategy that clusters around the given parts + /// + /// + /// + public DateTimeClusteringStrategy(DateTimePortion portions, string format) { + this.Portions = portions; + this.Format = format; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the format string will will be used to create a user-presentable + /// version of the cluster key. + /// + /// The format should use the date/time format strings, as documented + /// in the Windows SDK. Both standard formats and custom format will work. + /// "D" - long date pattern + /// "MMMM, yyyy" - "January, 1999" + public string Format { + get { return format; } + set { format = value; } + } + private string format; + + /// + /// Gets or sets the parts of the DateTime that will be extracted when + /// determining the clustering key for an object. + /// + public DateTimePortion Portions { + get { return portions; } + set { portions = value; } + } + private DateTimePortion portions = DateTimePortion.Year | DateTimePortion.Month; + + #endregion + + #region IClusterStrategy implementation + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + // Get the data attribute we want from the given model + // Make sure the returned value is a DateTime + DateTime? dateTime = this.Column.GetValue(model) as DateTime?; + if (!dateTime.HasValue) + return null; + + // Extract the parts of the datetime that we are intereted in. + // Even if we aren't interested in a particular portion, we still have to give it a reasonable default + // otherwise we won't be able to build a DateTime object for it + int year = ((this.Portions & DateTimePortion.Year) == DateTimePortion.Year) ? dateTime.Value.Year : 1; + int month = ((this.Portions & DateTimePortion.Month) == DateTimePortion.Month) ? dateTime.Value.Month : 1; + int day = ((this.Portions & DateTimePortion.Day) == DateTimePortion.Day) ? dateTime.Value.Day : 1; + int hour = ((this.Portions & DateTimePortion.Hour) == DateTimePortion.Hour) ? dateTime.Value.Hour : 0; + int minute = ((this.Portions & DateTimePortion.Minute) == DateTimePortion.Minute) ? dateTime.Value.Minute : 0; + int second = ((this.Portions & DateTimePortion.Second) == DateTimePortion.Second) ? dateTime.Value.Second : 0; + + return new DateTime(year, month, day, hour, minute, second); + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + DateTime? dateTime = cluster.ClusterKey as DateTime?; + + return this.ApplyDisplayFormat(cluster, dateTime.HasValue ? this.DateToString(dateTime.Value) : NULL_LABEL); + } + + /// + /// Convert the given date into a user presentable string + /// + /// + /// + protected virtual string DateToString(DateTime dateTime) { + if (String.IsNullOrEmpty(this.Format)) + return dateTime.ToString(CultureInfo.CurrentUICulture); + + try { + return dateTime.ToString(this.Format); + } + catch (FormatException) { + return String.Format("Bad format string '{0}' for value '{1}'", this.Format, dateTime); + } + } + + #endregion + } +} diff --git a/ObjectListView/Filtering/FilterMenuBuilder.cs b/ObjectListView/Filtering/FilterMenuBuilder.cs new file mode 100644 index 0000000..ba94b36 --- /dev/null +++ b/ObjectListView/Filtering/FilterMenuBuilder.cs @@ -0,0 +1,369 @@ +/* + * FilterMenuBuilder - Responsible for creating a Filter menu + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2012-05-20 JPP - Allow the same model object to be in multiple clusters + * Useful for xor'ed flag fields, and multi-value strings + * (e.g. hobbies that are stored as comma separated values). + * v2.5.1 + * 2012-04-14 JPP - Fixed rare bug with clustering an empty list (SF #3445118) + * v2.5 + * 2011-04-12 JPP - Added some images to menu + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Collections; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class know how to build a Filter menu. + /// It is responsible for clustering the values in the target column, + /// build a menu that shows those clusters, and then constructing + /// a filter that will enact the users choices. + /// + /// + /// Almost all of the methods in this class are declared as "virtual protected" + /// so that subclasses can provide alternative behaviours. + /// + public class FilterMenuBuilder { + + #region Static properties + + /// + /// Gets or sets the string that labels the Apply button. + /// Exposed so it can be localized. + /// + static public string APPLY_LABEL = "Apply"; + + /// + /// Gets or sets the string that labels the Clear All menu item. + /// Exposed so it can be localized. + /// + static public string CLEAR_ALL_FILTERS_LABEL = "Clear All Filters"; + + /// + /// Gets or sets the string that labels the Filtering menu as a whole.. + /// Exposed so it can be localized. + /// + static public string FILTERING_LABEL = "Filtering"; + + /// + /// Gets or sets the string that represents Select All values. + /// If this is set to null or empty, no Select All option will be included. + /// Exposed so it can be localized. + /// + static public string SELECT_ALL_LABEL = "Select All"; + + /// + /// Gets or sets the image that will be placed next to the Clear Filtering menu item + /// + static public Bitmap ClearFilteringImage = BrightIdeasSoftware.Properties.Resources.ClearFiltering; + + /// + /// Gets or sets the image that will be placed next to all "Apply" menu items on the filtering menu + /// + static public Bitmap FilteringImage = BrightIdeasSoftware.Properties.Resources.Filtering; + + #endregion + + #region Public properties + + /// + /// Gets or sets whether null should be considered as a valid data value. + /// If this is true (the default), then a cluster will null as a key will be allow. + /// If this is false, object that return a cluster key of null will ignored. + /// + public bool TreatNullAsDataValue { + get { return treatNullAsDataValue; } + set { treatNullAsDataValue = value; } + } + private bool treatNullAsDataValue = true; + + /// + /// Gets or sets the maximum number of objects that the clustering strategy + /// will consider. This should be large enough to collect all unique clusters, + /// but small enough to finish in a reasonable time. + /// + /// The default value is 10,000. This should be perfectly + /// acceptable for almost all lists. + public int MaxObjectsToConsider { + get { return maxObjectsToConsider; } + set { maxObjectsToConsider = value; } + } + private int maxObjectsToConsider = 10000; + + #endregion + + /// + /// Create a Filter menu on the given tool tip for the given column in the given ObjectListView. + /// + /// This is the main entry point into this class. + /// + /// + /// + /// The strip that should be shown to the user + virtual public ToolStripDropDown MakeFilterMenu(ToolStripDropDown strip, ObjectListView listView, OLVColumn column) { + if (strip == null) throw new ArgumentNullException("strip"); + if (listView == null) throw new ArgumentNullException("listView"); + if (column == null) throw new ArgumentNullException("column"); + + if (!column.UseFiltering || column.ClusteringStrategy == null || listView.Objects == null) + return strip; + + List clusters = this.Cluster(column.ClusteringStrategy, listView, column); + if (clusters.Count > 0) { + this.SortClusters(column.ClusteringStrategy, clusters); + strip.Items.Add(this.CreateFilteringMenuItem(column, clusters)); + } + + return strip; + } + + /// + /// Create a collection of clusters that should be presented to the user + /// + /// + /// + /// + /// + virtual protected List Cluster(IClusteringStrategy strategy, ObjectListView listView, OLVColumn column) { + // Build a map that correlates cluster key to clusters + NullableDictionary map = new NullableDictionary(); + int count = 0; + foreach (object model in listView.ObjectsForClustering) { + this.ClusterOneModel(strategy, map, model); + + if (count++ > this.MaxObjectsToConsider) + break; + } + + // Now that we know exactly how many items are in each cluster, create a label for it + foreach (ICluster cluster in map.Values) + cluster.DisplayLabel = strategy.GetClusterDisplayLabel(cluster); + + return new List(map.Values); + } + + private void ClusterOneModel(IClusteringStrategy strategy, NullableDictionary map, object model) { + object clusterKey = strategy.GetClusterKey(model); + + // If the returned value is an IEnumerable, that means the given model can belong to more than one cluster + IEnumerable keyEnumerable = clusterKey as IEnumerable; + if (clusterKey is string || keyEnumerable == null) + keyEnumerable = new object[] {clusterKey}; + + // Deal with nulls and DBNulls + ArrayList nullCorrected = new ArrayList(); + foreach (object key in keyEnumerable) { + if (key == null || key == System.DBNull.Value) { + if (this.TreatNullAsDataValue) + nullCorrected.Add(null); + } else nullCorrected.Add(key); + } + + // Group by key + foreach (object key in nullCorrected) { + if (map.ContainsKey(key)) + map[key].Count += 1; + else + map[key] = strategy.CreateCluster(key); + } + } + + /// + /// Order the given list of clusters in the manner in which they should be presented to the user. + /// + /// + /// + virtual protected void SortClusters(IClusteringStrategy strategy, List clusters) { + clusters.Sort(); + } + + /// + /// Do the work of making a menu that shows the clusters to the users + /// + /// + /// + /// + virtual protected ToolStripMenuItem CreateFilteringMenuItem(OLVColumn column, List clusters) { + ToolStripCheckedListBox checkedList = new ToolStripCheckedListBox(); + checkedList.Tag = column; + foreach (ICluster cluster in clusters) + checkedList.AddItem(cluster, column.ValuesChosenForFiltering.Contains(cluster.ClusterKey)); + if (!String.IsNullOrEmpty(SELECT_ALL_LABEL)) { + int checkedCount = checkedList.CheckedItems.Count; + if (checkedCount == 0) + checkedList.AddItem(SELECT_ALL_LABEL, CheckState.Unchecked); + else + checkedList.AddItem(SELECT_ALL_LABEL, checkedCount == clusters.Count ? CheckState.Checked : CheckState.Indeterminate); + } + checkedList.ItemCheck += new ItemCheckEventHandler(HandleItemCheckedWrapped); + + ToolStripMenuItem clearAll = new ToolStripMenuItem(CLEAR_ALL_FILTERS_LABEL, ClearFilteringImage, delegate(object sender, EventArgs args) { + this.ClearAllFilters(column); + }); + ToolStripMenuItem apply = new ToolStripMenuItem(APPLY_LABEL, FilteringImage, delegate(object sender, EventArgs args) { + this.EnactFilter(checkedList, column); + }); + ToolStripMenuItem subMenu = new ToolStripMenuItem(FILTERING_LABEL, null, new ToolStripItem[] { + clearAll, new ToolStripSeparator(), checkedList, apply }); + return subMenu; + } + + /// + /// Wrap a protected section around the real HandleItemChecked method, so that if + /// that method tries to change a "checkedness" of an item, we don't get a recursive + /// stack error. Effectively, this ensure that HandleItemChecked is only called + /// in response to a user action. + /// + /// + /// + private void HandleItemCheckedWrapped(object sender, ItemCheckEventArgs e) { + if (alreadyInHandleItemChecked) + return; + + try { + alreadyInHandleItemChecked = true; + this.HandleItemChecked(sender, e); + } + finally { + alreadyInHandleItemChecked = false; + } + } + bool alreadyInHandleItemChecked = false; + + /// + /// Handle a user-generated ItemCheck event + /// + /// + /// + virtual protected void HandleItemChecked(object sender, ItemCheckEventArgs e) { + + ToolStripCheckedListBox checkedList = sender as ToolStripCheckedListBox; + if (checkedList == null) return; + OLVColumn column = checkedList.Tag as OLVColumn; + if (column == null) return; + ObjectListView listView = column.ListView as ObjectListView; + if (listView == null) return; + + // Deal with the "Select All" item if there is one + int selectAllIndex = checkedList.Items.IndexOf(SELECT_ALL_LABEL); + if (selectAllIndex >= 0) + HandleSelectAllItem(e, checkedList, selectAllIndex); + } + + /// + /// Handle any checking/unchecking of the Select All option, and keep + /// its checkedness in sync with everything else that is checked. + /// + /// + /// + /// + virtual protected void HandleSelectAllItem(ItemCheckEventArgs e, ToolStripCheckedListBox checkedList, int selectAllIndex) { + // Did they check/uncheck the "Select All"? + if (e.Index == selectAllIndex) { + if (e.NewValue == CheckState.Checked) + checkedList.CheckAll(); + if (e.NewValue == CheckState.Unchecked) + checkedList.UncheckAll(); + return; + } + + // OK. The user didn't check/uncheck SelectAll. Now we have to update it's + // checkedness to reflect the state of everything else + // If all clusters are checked, we check the Select All. + // If no clusters are checked, the uncheck the Select All. + // For everything else, Select All is set to indeterminate. + + // How many items are currenty checked? + int count = checkedList.CheckedItems.Count; + + // First complication. + // The value of the Select All itself doesn't count + if (checkedList.GetItemCheckState(selectAllIndex) != CheckState.Unchecked) + count -= 1; + + // Another complication. + // CheckedItems does not yet know about the item the user has just + // clicked, so we have to adjust the count of checked items to what + // it is going to be + if (e.NewValue != e.CurrentValue) { + if (e.NewValue == CheckState.Checked) + count += 1; + else + count -= 1; + } + + // Update the state of the Select All item + if (count == 0) + checkedList.SetItemState(selectAllIndex, CheckState.Unchecked); + else if (count == checkedList.Items.Count - 1) + checkedList.SetItemState(selectAllIndex, CheckState.Checked); + else + checkedList.SetItemState(selectAllIndex, CheckState.Indeterminate); + } + + /// + /// Clear all the filters that are applied to the given column + /// + /// The column from which filters are to be removed + virtual protected void ClearAllFilters(OLVColumn column) { + + ObjectListView olv = column.ListView as ObjectListView; + if (olv == null || olv.IsDisposed) + return; + + olv.ResetColumnFiltering(); + } + + /// + /// Apply the selected values from the given list as a filter on the given column + /// + /// A list in which the checked items should be used as filters + /// The column for which a filter should be generated + virtual protected void EnactFilter(ToolStripCheckedListBox checkedList, OLVColumn column) { + + ObjectListView olv = column.ListView as ObjectListView; + if (olv == null || olv.IsDisposed) + return; + + // Collect all the checked values + ArrayList chosenValues = new ArrayList(); + foreach (object x in checkedList.CheckedItems) { + ICluster cluster = x as ICluster; + if (cluster != null) { + chosenValues.Add(cluster.ClusterKey); + } + } + column.ValuesChosenForFiltering = chosenValues; + + olv.UpdateColumnFiltering(); + } + } +} diff --git a/ObjectListView/Filtering/Filters.cs b/ObjectListView/Filtering/Filters.cs new file mode 100644 index 0000000..926bcf1 --- /dev/null +++ b/ObjectListView/Filtering/Filters.cs @@ -0,0 +1,475 @@ +/* + * Filters - Filtering on ObjectListViews + * + * Author: Phillip Piper + * Date: 03/03/2010 17:00 + * + * Change log: + * 2011-03-01 JPP Added CompositeAllFilter, CompositeAnyFilter and OneOfFilter + * v2.4.1 + * 2010-06-23 JPP Extended TextMatchFilter to handle regular expressions and string prefix matching. + * v2.4 + * 2010-03-03 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2010-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Reflection; +using System.Drawing; + +namespace BrightIdeasSoftware +{ + /// + /// Interface for model-by-model filtering + /// + public interface IModelFilter + { + /// + /// Should the given model be included when this filter is installed + /// + /// The model object to consider + /// Returns true if the model will be included by the filter + bool Filter(object modelObject); + } + /// + /// Interface for whole list filtering + /// + public interface IListFilter + { + /// + /// Return a subset of the given list of model objects as the new + /// contents of the ObjectListView + /// + /// The collection of model objects that the list will possibly display + /// The filtered collection that holds the model objects that will be displayed. + IEnumerable Filter(IEnumerable modelObjects); + } + + /// + /// Base class for model-by-model filters + /// + public class AbstractModelFilter : IModelFilter + { + /// + /// Should the given model be included when this filter is installed + /// + /// The model object to consider + /// Returns true if the model will be included by the filter + virtual public bool Filter(object modelObject) { + return true; + } + } + + /// + /// This filter calls a given Predicate to decide if a model object should be included + /// + public class ModelFilter : IModelFilter + { + /// + /// Create a filter based on the given predicate + /// + /// The function that will filter objects + public ModelFilter(Predicate predicate) { + this.Predicate = predicate; + } + + /// + /// Gets or sets the predicate used to filter model objects + /// + protected Predicate Predicate { + get { return predicate; } + set { predicate = value; } + } + private Predicate predicate; + + /// + /// Should the given model object be included? + /// + /// + /// + virtual public bool Filter(object modelObject) { + return this.Predicate == null ? true : this.Predicate(modelObject); + } + } + + /// + /// A CompositeFilter joins several other filters together. + /// If there are no filters, all model objects are included + /// + abstract public class CompositeFilter : IModelFilter { + + /// + /// Create an empty filter + /// + public CompositeFilter() { + } + + /// + /// Create a composite filter from the given list of filters + /// + /// A list of filters + public CompositeFilter(IEnumerable filters) { + foreach (IModelFilter filter in filters) { + if (filter != null) + Filters.Add(filter); + } + } + + /// + /// Gets or sets the filters used by this composite + /// + public IList Filters { + get { return filters; } + set { filters = value; } + } + private IList filters = new List(); + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// + /// True if the object is included by the filter + virtual public bool Filter(object modelObject) { + if (this.Filters == null || this.Filters.Count == 0) + return true; + + return this.FilterObject(modelObject); + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + abstract public bool FilterObject(object modelObject); + } + + /// + /// A CompositeAllFilter joins several other filters together. + /// A model object must satisfy all filters to be included. + /// If there are no filters, all model objects are included + /// + public class CompositeAllFilter : CompositeFilter { + + /// + /// Create a filter + /// + /// + public CompositeAllFilter(List filters) + : base(filters) { + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + override public bool FilterObject(object modelObject) { + foreach (IModelFilter filter in this.Filters) + if (!filter.Filter(modelObject)) + return false; + + return true; + } + } + + /// + /// A CompositeAllFilter joins several other filters together. + /// A model object must only satisfy one of the filters to be included. + /// If there are no filters, all model objects are included + /// + public class CompositeAnyFilter : CompositeFilter { + + /// + /// Create a filter from the given filters + /// + /// + public CompositeAnyFilter(List filters) + : base(filters) { + } + + /// + /// Decide whether or not the given model should be included by the filter + /// + /// Filters is guaranteed to be non-empty when this method is called + /// The model object under consideration + /// True if the object is included by the filter + override public bool FilterObject(object modelObject) { + foreach (IModelFilter filter in this.Filters) + if (filter.Filter(modelObject)) + return true; + + return false; + } + } + + /// + /// Instances of this class extract a value from the model object + /// and compare that value to a list of fixed values. The model + /// object is included if the extracted value is in the list + /// + /// If there is no delegate installed or there are + /// no values to match, no model objects will be matched + public class OneOfFilter : IModelFilter { + + /// + /// Create a filter that will use the given delegate to extract values + /// + /// + public OneOfFilter(AspectGetterDelegate valueGetter) : + this(valueGetter, new ArrayList()) { + } + + /// + /// Create a filter that will extract values using the given delegate + /// and compare them to the values in the given list. + /// + /// + /// + public OneOfFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) { + this.ValueGetter = valueGetter; + this.PossibleValues = new ArrayList(possibleValues); + } + + /// + /// Gets or sets the delegate that will be used to extract values + /// from model objects + /// + virtual public AspectGetterDelegate ValueGetter { + get { return valueGetter; } + set { valueGetter = value; } + } + private AspectGetterDelegate valueGetter; + + /// + /// Gets or sets the list of values that the value extracted from + /// the model object must match in order to be included. + /// + virtual public IList PossibleValues { + get { return possibleValues; } + set { possibleValues = value; } + } + private IList possibleValues; + + /// + /// Should the given model object be included? + /// + /// + /// + public virtual bool Filter(object modelObject) { + if (this.ValueGetter == null || this.PossibleValues == null || this.PossibleValues.Count == 0) + return false; + + object result = this.ValueGetter(modelObject); + IEnumerable enumerable = result as IEnumerable; + if (result is string || enumerable == null) + return this.DoesValueMatch(result); + + foreach (object x in enumerable) { + if (this.DoesValueMatch(x)) + return true; + } + return false; + } + + /// + /// Decides if the given property is a match for the values in the PossibleValues collection + /// + /// + /// + protected virtual bool DoesValueMatch(object result) { + return this.PossibleValues.Contains(result); + } + } + + /// + /// Instances of this class match a property of a model objects against + /// a list of bit flags. The property should be an xor-ed collection + /// of bits flags. + /// + /// Both the property compared and the list of possible values + /// must be convertible to ulongs. + public class FlagBitSetFilter : OneOfFilter { + + /// + /// Create an instance + /// + /// + /// + public FlagBitSetFilter(AspectGetterDelegate valueGetter, ICollection possibleValues) : base(valueGetter, possibleValues) { + this.ConvertPossibleValues(); + } + + /// + /// Gets or sets the collection of values that will be matched. + /// These must be ulongs (or convertible to ulongs). + /// + public override IList PossibleValues { + get { return base.PossibleValues; } + set { + base.PossibleValues = value; + this.ConvertPossibleValues(); + } + } + + private void ConvertPossibleValues() { + this.possibleValuesAsUlongs = new List(); + foreach (object x in this.PossibleValues) + this.possibleValuesAsUlongs.Add(Convert.ToUInt64(x)); + } + + /// + /// Decides if the given property is a match for the values in the PossibleValues collection + /// + /// + /// + protected override bool DoesValueMatch(object result) { + try { + UInt64 value = Convert.ToUInt64(result); + foreach (ulong flag in this.possibleValuesAsUlongs) { + if ((value & flag) == flag) + return true; + } + return false; + } + catch (InvalidCastException) { + return false; + } + catch (FormatException) { + return false; + } + } + + private List possibleValuesAsUlongs = new List(); + } + + /// + /// Base class for whole list filters + /// + public class AbstractListFilter : IListFilter + { + /// + /// Return a subset of the given list of model objects as the new + /// contents of the ObjectListView + /// + /// The collection of model objects that the list will possibly display + /// The filtered collection that holds the model objects that will be displayed. + virtual public IEnumerable Filter(IEnumerable modelObjects) { + return modelObjects; + } + } + + /// + /// Instance of this class implement delegate based whole list filtering + /// + public class ListFilter : AbstractListFilter + { + /// + /// A delegate that filters on a whole list + /// + /// + /// + public delegate IEnumerable ListFilterDelegate(IEnumerable rowObjects); + + /// + /// Create a ListFilter + /// + /// + public ListFilter(ListFilterDelegate function) { + this.Function = function; + } + + /// + /// Gets or sets the delegate that will filter the list + /// + public ListFilterDelegate Function { + get { return function; } + set { function = value; } + } + private ListFilterDelegate function; + + /// + /// Do the actual work of filtering + /// + /// + /// + public override IEnumerable Filter(IEnumerable modelObjects) { + if (this.Function == null) + return modelObjects; + + return this.Function(modelObjects); + } + } + + /// + /// Filter the list so only the last N entries are displayed + /// + public class TailFilter : AbstractListFilter + { + /// + /// Create a no-op tail filter + /// + public TailFilter() { + } + + /// + /// Create a filter that includes on the last N model objects + /// + /// + public TailFilter(int numberOfObjects) { + this.Count = numberOfObjects; + } + + /// + /// Gets or sets the number of model objects that will be + /// returned from the tail of the list + /// + public int Count { + get { return count; } + set { count = value; } + } + private int count; + + /// + /// Return the last N subset of the model objects + /// + /// + /// + public override IEnumerable Filter(IEnumerable modelObjects) { + if (this.Count <= 0) + return modelObjects; + + ArrayList list = ObjectListView.EnumerableToArray(modelObjects, false); + + if (this.Count > list.Count) + return list; + + object[] tail = new object[this.Count]; + list.CopyTo(list.Count - this.Count, tail, 0, this.Count); + return new ArrayList(tail); + } + } +} \ No newline at end of file diff --git a/ObjectListView/Filtering/FlagClusteringStrategy.cs b/ObjectListView/Filtering/FlagClusteringStrategy.cs new file mode 100644 index 0000000..df0ebbc --- /dev/null +++ b/ObjectListView/Filtering/FlagClusteringStrategy.cs @@ -0,0 +1,160 @@ +/* + * FlagClusteringStrategy - Implements a clustering strategy for a field which is a single integer + * containing an XOR'ed collection of bit flags + * + * Author: Phillip Piper + * Date: 23-March-2012 8:33 am + * + * Change log: + * 2012-03-23 JPP - First version + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class cluster model objects on the basis of a + /// property that holds an xor-ed collection of bit flags. + /// + public class FlagClusteringStrategy : ClusteringStrategy + { + #region Life and death + + /// + /// Create a clustering strategy that operates on the flags of the given enum + /// + /// + public FlagClusteringStrategy(Type enumType) { + if (enumType == null) throw new ArgumentNullException("enumType"); + if (!enumType.IsEnum) throw new ArgumentException("Type must be enum", "enumType"); + if (enumType.GetCustomAttributes(typeof(FlagsAttribute), false) == null) throw new ArgumentException("Type must have [Flags] attribute", "enumType"); + + List flags = new List(); + foreach (object x in Enum.GetValues(enumType)) + flags.Add(Convert.ToInt64(x)); + + List flagLabels = new List(); + foreach (string x in Enum.GetNames(enumType)) + flagLabels.Add(x); + + this.SetValues(flags.ToArray(), flagLabels.ToArray()); + } + + /// + /// Create a clustering strategy around the given collections of flags and their display labels. + /// There must be the same number of elements in both collections. + /// + /// The list of flags. + /// + public FlagClusteringStrategy(long[] values, string[] labels) { + this.SetValues(values, labels); + } + + #endregion + + #region Implementation + + /// + /// Gets the value that will be xor-ed to test for the presence of a particular value. + /// + public long[] Values { + get { return this.values; } + private set { this.values = value; } + } + private long[] values; + + /// + /// Gets the labels that will be used when the corresponding Value is XOR present in the data. + /// + public string[] Labels { + get { return this.labels; } + private set { this.labels = value; } + } + private string[] labels; + + private void SetValues(long[] flags, string[] flagLabels) { + if (flags == null || flags.Length == 0) throw new ArgumentNullException("flags"); + if (flagLabels == null || flagLabels.Length == 0) throw new ArgumentNullException("flagLabels"); + if (flags.Length != flagLabels.Length) throw new ArgumentException("values and labels must have the same number of entries", "flags"); + + this.Values = flags; + this.Labels = flagLabels; + } + + #endregion + + #region Implementation of IClusteringStrategy + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// + /// + public override object GetClusterKey(object model) { + List flags = new List(); + try { + long modelValue = Convert.ToInt64(this.Column.GetValue(model)); + foreach (long x in this.Values) { + if ((x & modelValue) == x) + flags.Add(x); + } + return flags; + } + catch (InvalidCastException ex) { + System.Diagnostics.Debug.Write(ex); + return flags; + } + catch (FormatException ex) { + System.Diagnostics.Debug.Write(ex); + return flags; + } + } + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + public override string GetClusterDisplayLabel(ICluster cluster) { + long clusterKeyAsUlong = Convert.ToInt64(cluster.ClusterKey); + for (int i = 0; i < this.Values.Length; i++ ) { + if (clusterKeyAsUlong == this.Values[i]) + return this.ApplyDisplayFormat(cluster, this.Labels[i]); + } + return this.ApplyDisplayFormat(cluster, clusterKeyAsUlong.ToString(CultureInfo.CurrentUICulture)); + } + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + public override IModelFilter CreateFilter(IList valuesChosenForFiltering) { + return new FlagBitSetFilter(this.GetClusterKey, valuesChosenForFiltering); + } + + #endregion + } +} \ No newline at end of file diff --git a/ObjectListView/Filtering/ICluster.cs b/ObjectListView/Filtering/ICluster.cs new file mode 100644 index 0000000..37f45a6 --- /dev/null +++ b/ObjectListView/Filtering/ICluster.cs @@ -0,0 +1,56 @@ +/* + * ICluster - A cluster is a group of objects that can be included or excluded as a whole + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + /// + /// A cluster is a like collection of objects that can be usefully filtered + /// as whole using the filtering UI provided by the ObjectListView. + /// + public interface ICluster : IComparable { + /// + /// Gets or sets how many items belong to this cluster + /// + int Count { get; set; } + + /// + /// Gets or sets the label that will be shown to the user to represent + /// this cluster + /// + string DisplayLabel { get; set; } + + /// + /// Gets or sets the actual data object that all members of this cluster + /// have commonly returned. + /// + object ClusterKey { get; set; } + } +} diff --git a/ObjectListView/Filtering/IClusteringStrategy.cs b/ObjectListView/Filtering/IClusteringStrategy.cs new file mode 100644 index 0000000..e18ddc0 --- /dev/null +++ b/ObjectListView/Filtering/IClusteringStrategy.cs @@ -0,0 +1,80 @@ +/* + * IClusterStrategy - Encapsulates the ability to create a list of clusters from an ObjectListView + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2012-05-23 JPP - Added CreateFilter() method to interface to allow the strategy + * to control the actual model filter that is created. + * v2.5 + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware{ + + /// + /// Implementation of this interface control the selecting of cluster keys + /// and how those clusters will be presented to the user + /// + public interface IClusteringStrategy { + + /// + /// Gets or sets the column upon which this strategy will operate + /// + OLVColumn Column { get; set; } + + /// + /// Get the cluster key by which the given model will be partitioned by this strategy + /// + /// If the returned value is an IEnumerable, the given model is considered + /// to belong to multiple clusters + /// + /// + object GetClusterKey(object model); + + /// + /// Create a cluster to hold the given cluster key + /// + /// + /// + ICluster CreateCluster(object clusterKey); + + /// + /// Gets the display label that the given cluster should use + /// + /// + /// + string GetClusterDisplayLabel(ICluster cluster); + + /// + /// Create a filter that will include only model objects that + /// match one or more of the given values. + /// + /// + /// + IModelFilter CreateFilter(IList valuesChosenForFiltering); + } +} diff --git a/ObjectListView/Filtering/TextMatchFilter.cs b/ObjectListView/Filtering/TextMatchFilter.cs new file mode 100644 index 0000000..ed26b1b --- /dev/null +++ b/ObjectListView/Filtering/TextMatchFilter.cs @@ -0,0 +1,623 @@ +/* + * TextMatchFilter - Text based filtering on ObjectListViews + * + * Author: Phillip Piper + * Date: 31/05/2011 7:45am + * + * Change log: + * v2.6 + * 2012-10-13 JPP Allow filtering to consider additional columns + * v2.5.1 + * 2011-06-22 JPP Handle searching for empty strings + * v2.5.0 + * 2011-05-31 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class include only those rows of the listview + /// that match one or more given strings. + /// + /// This class can match strings by prefix, regex, or simple containment. + /// There are factory methods for each of these matching strategies. + public class TextMatchFilter : AbstractModelFilter { + + #region Life and death + + /// + /// Create a text filter that will include rows where any cell matches + /// any of the given regex expressions. + /// + /// + /// + /// + /// Any string that is not a valid regex expression will be ignored. + public static TextMatchFilter Regex(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.RegexStrings = texts; + return filter; + } + + /// + /// Create a text filter that includes rows where any cell begins with one of the given strings + /// + /// + /// + /// + public static TextMatchFilter Prefix(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.PrefixStrings = texts; + return filter; + } + + /// + /// Create a text filter that includes rows where any cell contains any of the given strings. + /// + /// + /// + /// + public static TextMatchFilter Contains(ObjectListView olv, params string[] texts) { + TextMatchFilter filter = new TextMatchFilter(olv); + filter.ContainsStrings = texts; + return filter; + } + + /// + /// Create a TextFilter + /// + /// + public TextMatchFilter(ObjectListView olv) { + this.ListView = olv; + } + + /// + /// Create a TextFilter that finds the given string + /// + /// + /// + public TextMatchFilter(ObjectListView olv, string text) { + this.ListView = olv; + this.ContainsStrings = new string[] { text }; + } + + /// + /// Create a TextFilter that finds the given string using the given comparison + /// + /// + /// + /// + public TextMatchFilter(ObjectListView olv, string text, StringComparison comparison) { + this.ListView = olv; + this.ContainsStrings = new string[] { text }; + this.StringComparison = comparison; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets which columns will be used for the comparisons? If this is null, all columns will be used + /// + public OLVColumn[] Columns { + get { return columns; } + set { columns = value; } + } + private OLVColumn[] columns; + + /// + /// Gets or sets additional columns which will be used in the comparison. These will be used + /// in addition to either the Columns property or to all columns taken from the control. + /// + public OLVColumn[] AdditionalColumns { + get { return additionalColumns; } + set { additionalColumns = value; } + } + private OLVColumn[] additionalColumns; + + /// + /// Gets or sets the collection of strings that will be used for + /// contains matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable ContainsStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextContainsMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets whether or not this filter has any search criteria + /// + public bool HasComponents { + get { + return this.MatchingStrategies.Count > 0; + } + } + + /// + /// Gets or set the ObjectListView upon which this filter will work + /// + /// + /// You cannot really rebase a filter after it is created, so do not change this value. + /// It is included so that it can be set in an object initializer. + /// + public ObjectListView ListView { + get { return listView; } + set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the collection of strings that will be used for + /// prefix matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable PrefixStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextBeginsMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets or sets the options that will be used when compiling the regular expression. + /// + /// + /// This is only used when doing Regex matching (obviously). + /// If this is not set specifically, the appropriate options are chosen to match the + /// StringComparison setting (culture invariant, case sensitive). + /// + public RegexOptions RegexOptions { + get { + if (!regexOptions.HasValue) { + switch (this.StringComparison) { + case StringComparison.CurrentCulture: + regexOptions = RegexOptions.None; + break; + case StringComparison.CurrentCultureIgnoreCase: + regexOptions = RegexOptions.IgnoreCase; + break; + case StringComparison.Ordinal: + case StringComparison.InvariantCulture: + regexOptions = RegexOptions.CultureInvariant; + break; + case StringComparison.OrdinalIgnoreCase: + case StringComparison.InvariantCultureIgnoreCase: + regexOptions = RegexOptions.CultureInvariant | RegexOptions.IgnoreCase; + break; + default: + regexOptions = RegexOptions.None; + break; + } + } + return regexOptions.Value; + } + set { + regexOptions = value; + } + } + private RegexOptions? regexOptions; + + /// + /// Gets or sets the collection of strings that will be used for + /// regex pattern matching. Setting this replaces all previous texts + /// of any kind. + /// + public IEnumerable RegexStrings { + get { + foreach (TextMatchingStrategy component in this.MatchingStrategies) + yield return component.Text; + } + set { + this.MatchingStrategies = new List(); + if (value != null) { + foreach (string text in value) + this.MatchingStrategies.Add(new TextRegexMatchingStrategy(this, text)); + } + } + } + + /// + /// Gets or sets how the filter will match text + /// + public StringComparison StringComparison { + get { return this.stringComparison; } + set { this.stringComparison = value; } + } + private StringComparison stringComparison = StringComparison.InvariantCultureIgnoreCase; + + #endregion + + #region Implementation + + /// + /// Loop over the columns that are being considering by the filter + /// + /// + protected virtual IEnumerable IterateColumns() { + if (this.Columns == null) { + foreach (OLVColumn column in this.ListView.Columns) + yield return column; + } else { + foreach (OLVColumn column in this.Columns) + yield return column; + } + if (this.AdditionalColumns != null) { + foreach (OLVColumn column in this.AdditionalColumns) + yield return column; + } + } + + #endregion + + #region Public interface + + /// + /// Do the actual work of filtering + /// + /// + /// + public override bool Filter(object modelObject) { + if (this.ListView == null || !this.HasComponents) + return true; + + foreach (OLVColumn column in this.IterateColumns()) { + if (column.IsVisible && column.Searchable) { + string cellText = column.GetStringValue(modelObject); + foreach (TextMatchingStrategy filter in this.MatchingStrategies) { + if (String.IsNullOrEmpty(filter.Text) || filter.MatchesText(cellText)) + return true; + } + } + } + + return false; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted + /// + /// A list of character ranges indicating the matched substrings + public IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + foreach (TextMatchingStrategy filter in this.MatchingStrategies) { + if (!String.IsNullOrEmpty(filter.Text)) + ranges.AddRange(filter.FindAllMatchedRanges(cellText)); + } + + return ranges; + } + + /// + /// Is the given column one of the columns being used by this filter? + /// + /// + /// + public bool IsIncluded(OLVColumn column) { + if (this.Columns == null) { + return column.ListView == this.ListView; + } + + foreach (OLVColumn x in this.Columns) { + if (x == column) + return true; + } + + return false; + } + + #endregion + + #region Implementation members + + private List MatchingStrategies = new List(); + + #endregion + + #region Components + + /// + /// Base class for the various types of string matching that TextMatchFilter provides + /// + abstract protected class TextMatchingStrategy { + + /// + /// Gets how the filter will match text + /// + public StringComparison StringComparison { + get { return this.TextFilter.StringComparison; } + } + + /// + /// Gets the text filter to which this component belongs + /// + public TextMatchFilter TextFilter { + get { return textFilter; } + set { textFilter = value; } + } + private TextMatchFilter textFilter; + + /// + /// Gets or sets the text that will be matched + /// + public string Text { + get { return this.text; } + set { this.text = value; } + } + private string text; + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + abstract public IEnumerable FindAllMatchedRanges(string cellText); + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + abstract public bool MatchesText(string cellText); + } + + /// + /// This component provides text contains matching strategy. + /// + protected class TextContainsMatchingStrategy : TextMatchingStrategy { + + /// + /// Create a text contains strategy + /// + /// + /// + public TextContainsMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + override public bool MatchesText(string cellText) { + return cellText.IndexOf(this.Text, this.StringComparison) != -1; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + override public IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + int matchIndex = cellText.IndexOf(this.Text, this.StringComparison); + while (matchIndex != -1) { + ranges.Add(new CharacterRange(matchIndex, this.Text.Length)); + matchIndex = cellText.IndexOf(this.Text, matchIndex + this.Text.Length, this.StringComparison); + } + + return ranges; + } + } + + /// + /// This component provides text begins with matching strategy. + /// + protected class TextBeginsMatchingStrategy : TextMatchingStrategy { + + /// + /// Create a text begins strategy + /// + /// + /// + public TextBeginsMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + override public bool MatchesText(string cellText) { + return cellText.StartsWith(this.Text, this.StringComparison); + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + override public IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + if (cellText.StartsWith(this.Text, this.StringComparison)) + ranges.Add(new CharacterRange(0, this.Text.Length)); + + return ranges; + } + + } + + /// + /// This component provides regex matching strategy. + /// + protected class TextRegexMatchingStrategy : TextMatchingStrategy { + + /// + /// Creates a regex strategy + /// + /// + /// + public TextRegexMatchingStrategy(TextMatchFilter filter, string text) { + this.TextFilter = filter; + this.Text = text; + } + + /// + /// Gets or sets the options that will be used when compiling the regular expression. + /// + public RegexOptions RegexOptions { + get { + return this.TextFilter.RegexOptions; + } + } + + /// + /// Gets or sets a compilex regular expression, based on our current Text and RegexOptions. + /// + /// + /// If Text fails to compile as a regular expression, this will return a Regex object + /// that will match all strings. + /// + protected Regex Regex { + get { + if (this.regex == null) { + try { + this.regex = new Regex(this.Text, this.RegexOptions); + } + catch (ArgumentException) { + this.regex = TextRegexMatchingStrategy.InvalidRegexMarker; + } + } + return this.regex; + } + set { + this.regex = value; + } + } + private Regex regex; + + /// + /// Gets whether or not our current regular expression is a valid regex + /// + protected bool IsRegexInvalid { + get { + return this.Regex == TextRegexMatchingStrategy.InvalidRegexMarker; + } + } + static private Regex InvalidRegexMarker = new Regex(".*"); + + /// + /// Does the given text match the filter + /// + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// Return true if the given cellText matches our strategy + public override bool MatchesText(string cellText) { + if (this.IsRegexInvalid) + return true; + return this.Regex.Match(cellText).Success; + } + + /// + /// Find all the ways in which this filter matches the given string. + /// + /// + /// + /// This is used by the renderer to decide which bits of + /// the string should be highlighted. + /// + /// this.Text will not be null or empty when this is called. + /// + /// The text of the cell we want to search + /// A list of character ranges indicating the matched substrings + override public IEnumerable FindAllMatchedRanges(string cellText) { + List ranges = new List(); + + if (!this.IsRegexInvalid) { + foreach (Match match in this.Regex.Matches(cellText)) { + if (match.Length > 0) + ranges.Add(new CharacterRange(match.Index, match.Length)); + } + } + + return ranges; + } + } + + #endregion + } +} diff --git a/ObjectListView/FullClassDiagram.cd b/ObjectListView/FullClassDiagram.cd new file mode 100644 index 0000000..3126de2 --- /dev/null +++ b/ObjectListView/FullClassDiagram.cd @@ -0,0 +1,1261 @@ + + + + + + AQCABIAAAIAhAACgAAAAAIAMAAAECAAggAIAIIAAAEA= + CellEditing\CellEditKeyEngine.cs + + + + + + AAAAAAAAAAAAAAQEIAAEAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + CellEditing\CellEditors.cs + + + + + + AAAAAAQIAAABAQAAQAAgAAAAAAABAIAAAAQAAAAAAAA= + CellEditing\EditorRegistry.cs + + + + + + ABgAAAAAAAAAAgIhAAACAAAEAAAAAAAAAAAAAAAAAAA= + DataListView.cs + + + + + + BAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + + BAAAAAAAAAAAAAAAAQAAAAAQAAAQEAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + + AAAAAAAAAAAQQAAAAAIAEAAAAAEFAAAAgAAAAMAAAAA= + DragDrop\DropSink.cs + + + + + + + ZS0gAiQEBAoQDA8YQBMAMCAFgwQHBALcAEBEiEAAAQA= + DragDrop\DropSink.cs + + + + + + BYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + DragDrop\DropSink.cs + + + + + + IACgGiAAIBoAAAAAAAAAAAAAAALAAAAKgAQECIAEgAA= + DragDrop\DropSink.cs + + + + + + BAEAAAAAAAAAAAAAAAEQAAAAAAAAAAIAAAAAgAQAAEA= + DragDrop\DropSink.cs + + + + + + AQAAEAEABAAAAAAAEAAAAAAAAAIAAgACgAAAAAAAQBA= + DragDrop\OLVDataObject.cs + + + + + + AAAAAAAAAAAAAgIAAAACAAAEQAAAAAAAAAAAAAAAAAA= + FastDataListView.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAA= + FastObjectListView.cs + + + + + + ABAAAAgAQAQAABAgAAASQ4AQAAIEAAAAAAAAAEIAgAA= + FastObjectListView.cs + + + + + + AAAAAAAQAAAAEAQEBAAAAAQAgAAAAIAAAAAAAAAAAAA= + Filtering\Cluster.cs + + + + + + + AAAAAAAEAAAAAAAAAhBABAgAQAAIKAQBAQAAAAAAoIA= + Filtering\ClusteringStrategy.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBAAAAAAAAAAA= + Filtering\ClustersFromGroupsStrategy.cs + + + + + + AAAAAAIAAAACAAAAAAAACAAAAQgAAAQBAAAAAAAAAAA= + Filtering\DateTimeClusteringStrategy.cs + + + + + + SAEAADAQgEEAAQAIAAgQIAAAAFQAAAAAAAAAoAAAAAE= + Filtering\FilterMenuBuilder.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAEAAAABAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAgAAAAAAIAAAACAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + + AAAAAAAAAAAAAAAAAgAAAAIAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAABAAAAAQAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAgAAACAAAABAAABAAgAQACABABAAAAyEIAAIgSgAA= + Filtering\TextMatchFilter.cs + + + + + + EgAAQTAAZAEgWGAASFwIIEYECGBGAQKAQEEGAQJAAEE= + Implementation\Attributes.cs + + + + + + AAIAAAAAAAQAAAAAAAAAAAAAQAAAAAAAAABQAAAAAAA= + Implementation\Comparers.cs + + + + + + + AAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA= + Implementation\Comparers.cs + + + + + + + AAIAAAAAAAQAAAAAAAAAAAAAQAAAAAAAAABQAAAAAAA= + Implementation\Comparers.cs + + + + + + + AZggAAAwAGAAAgACQAYCBCAEICACACUEhAEAQKBAgQg= + Implementation\DataSourceAdapter.cs + + + + + + + 5/7+//3///fX/+//+///f/3//f/37N////+//7+///8= + Implementation\Enums.cs + + + + + + + ASgQyXBAABICBAAAAIAAACCEMAKBQAOAABDAgAUpAQA= + Implementation\Events.cs + + + + + + ABEAEAAFQAAAABAABABQEAQAQAAAECAAAAAgAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAABAAAIAAAAAAAAAEAAAQAAAAABAAAAAAAABAAIAA= + Implementation\Events.cs + + + + + + AAQABAAAAAQQAAAAAAEAAAQAAAAABAAAAAAgABQAIAE= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAEAAAAAAAAAAAAEAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAEAAAAAAAAAAAAAAAEAAAQAAAAAAAAAAAAAAAQAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAgAAAAIAAAAAAAAAAAAgAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAQAAAIAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAEA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAQAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAMABAAAAQgAZAFAAGUARAAAAAgAgAAIAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + CAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAQAAAAAAAEGAAAAAAAAAAAgAACAIAAAACAAHAAA= + Implementation\Events.cs + + + + + + AAAgAAAAMAAAAAAAgARABAAEUAQIAAAAiAgAAIAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAABAAAAAQAAAAAAIiAgACIAIAAA= + Implementation\Events.cs + + + + + + AEAAAAAAAAAAAAAAgAQAAAAEAAAAAQAAgAkEAIAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAEAAAAAAAAABABAAAUAQAAAAAAAgAAAAAAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAA= + Implementation\Events.cs + + + + + + AAIgAAAGJACRCAAEEABEAAAAJACBAAAAAAgIAAAAAAA= + Implementation\Events.cs + + + + + + QAAAEAAAAAAAABAABABQEAQAQAAAEAAAAAAAAEAAAAA= + Implementation\Events.cs + + + + + + QAAAAEAAAAAAAAAAgAAAAIAAAAAAAAAAAIAAAACAAAA= + Implementation\Events.cs + + + + + + QAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Events.cs + + + + + + QAAAgEAACggAgAAIAAAAAEAAAIAAAAAAAAAAAAAAAII= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAgAAAAIAAA= + Implementation\Events.cs + + + + + + AAAAAAAAIMAAAACBEAEAoQAAAKAACAAUhAAgRIQAIAE= + Implementation\GroupingParameters.cs + + + + + + bEYChOwmAAQgiCQEQBgEAMwAEAAMAEQMgEFAFODAGhM= + Implementation\Groups.cs + + + + + + AAAAgAAAJAAAAAQEABCAAAAAhAAAAACAAAAAAAIAAAE= + Implementation\Munger.cs + + + + + + AAAIACAAJAAAgAAEACAAAAAABAAAIAAAAAEAAAAAEAA= + Implementation\Munger.cs + + + + + + AAEAAAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAABAAA= + Implementation\Munger.cs + + + + + + cChxFKmMjlNGI/LfZWKToPLMK45gioYxDANnzL7yfN4= + Implementation\NativeMethods.cs + + + + + + AAEAAAAAAAAEAAAACAAAAAAAQAAAAAAAAAAAAAAAAAA= + Implementation\NullableDictionary.cs + + + + + + ABAAAAAAAABEAACAAAAAAAAQABAAEgAQAAIAASAAAgE= + Implementation\OLVListItem.cs + + + + + + AAAAAAAAAAAEAAAAAAAAAAAAABAIAAAQCAgACSAAAAk= + Implementation\OLVListSubItem.cs + + + + + + AAAAgEAAEAgAAABEAAZABAAGAAQAEAAAgAAAAAAIAAA= + Implementation\OlvListViewHitTestInfo.cs + + + + + + AAAAAAAAAAAAAACAAAAEAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + + AAAAABAAAAAAAACAAAAAAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + AIAAAAAAAAAAAAAAAAAAAgAIAAAQAAAAAAAEAEACAAA= + Implementation\VirtualGroups.cs + + + + + + + ABAAAAAAgAAAAAAgAAASQgAQAAAEAAAAAAAAAIAAgAA= + Implementation\VirtualListDataSource.cs + + + + + + + AABAAAAAAAAAAAAAAABCAAAAAAAEAAAAAAAAAAAAAAA= + Implementation\VirtualListDataSource.cs + + + + + + MgARTxAAJEcEWCbkoNwJbE6WTnSOEnOKQhSWGDJgsFk= + OLVColumn.cs + + + + + + AACgAIAABAAAAIABACCiAIAgAACAAgAAABAIAAAAgAE= + Rendering\Adornments.cs + + + + + + ABAAAAAAAIAAAAAAAEAAAAAAEAAAABAAAEABAAAAAAA= + Rendering\Adornments.cs + + + + + + QAIggAAEIAAgBIAIAAIASEACIABAACIIAAMACCADAsA= + Rendering\Adornments.cs + + + + + + AAEAAAAAAAABAgAAAQAABAAAAAQBAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AgAAAAAAEAAAAgAAAAAAAAAAAAAAAAAAAAAQAAIAAAA= + Rendering\Decorations.cs + + + + + + YAgAgCABAAAgA4AAAAIAgAAAAAAAAAAAAAIAACBIgAA= + Rendering\Decorations.cs + + + + + + AAAAgAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAABAAIA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAA= + Rendering\Decorations.cs + + + + + + QAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAEAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAABAgAAAQAABAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AAAAAAAAAAABAgAAAQAABAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + + AAAAAAAAAAAAAgAAAAAAAIAAAACAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAAAAAgAAAAAAABAAAAAQAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAIAAAgAAAAAAABAAAAAQAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + + AAAAAAAAAAAAAgAAAAIAAAACAAAAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAQAAAABAAAAAABAAAAAAAAAAABAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + + AAABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AxLAQSEQZQhhAmA5IRBAASSQUhQgkFAAgJDGAAMDE8I= + Rendering\Renderers.cs + + + + + + QAggCAEAAEAAAIAwAAKAEAIQABAAEAAAAAAAAAAKAAA= + Rendering\Renderers.cs + + + + + + AAIAEBAAAQAAAAgAABAAEAAAAAAAAAAAABAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AAAAAAQBIAAAAAAAAAAAAAAAAAAAAAAACBAAAAIAAAA= + Rendering\Renderers.cs + + + + + + AAAgAAAAAAAAAAAAAIAAAAAgIAAAAABBABAAAAAEIAA= + Rendering\Renderers.cs + + + + + + AAIAAQAAAAEAAgAAEAIAABAAAAAAAAAAIAEAACADAAA= + Rendering\Styles.cs + + + + + + + AAIAAQAAAAEAAgAAAAIAAAAAAAAAAAAAAAEAAAADAAA= + Rendering\Styles.cs + + + + + + + AAAAAAAAQAiAAAAAgAIAAAAAAAAIBAAgAAAAAAAAAAA= + Rendering\Styles.cs + + + + + + AAIAAQAAAAEAAAAAAAAAAAACACAAAgAgAAEAAAADAAA= + Rendering\Styles.cs + + + + + + AAAAAAAQAAgAAAAAAAAAAICAAIAIAAAABAAAAQAEAAA= + Rendering\Styles.cs + + + + + + BgCkgAAARAoAAEAyEAAAAAIAAAAIAhCAAQIERAgAASA= + SubControls\GlassPanelForm.cs + + + + + + MAAIOQAUABAAAACQiBQCKRgAACECAAoAwAAQxJAACaE= + SubControls\HeaderControl.cs + + + + + + AkAACAgAACCACAAAAAAAAIAAwAAIAAQCAAAAAAAAAAA= + SubControls\ToolStripCheckedListBox.cs + + + + + + CkoAByAwQQAggEvSAAIQiIWIBELAYOIpiAKQUIQDqEA= + SubControls\ToolTipControl.cs + + + + + + AAAAAEAAFDAAQTAUAACIBAoWEAAAAAAoAAAAAIEAAgA= + Utilities\ColumnSelectionForm.cs + + + + + + AAABAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAACAAAAAA= + Utilities\Generator.cs + + + + + + AAAAwABEAAAAAACIAIAQEAABEAAAAAEEggAAAAACQAA= + Utilities\TypedObjectListView.cs + + + + + + AAAACAAAAEAEAABAAEAQCAAAQAAEAEAAAAgAAAAAAAA= + Utilities\TypedObjectListView.cs + + + + + + AVkQQAAAAGIEAQAkKkHAEAgRH4gBgAGOCCEigAAgIAQ= + VirtualObjectListView.cs + + + + + + AAEAAAABACAEAQAEAAAAAAAgAQCAAAAAAAMIQAABEAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAABAAAABAAAAAAAAQAAAAAAAAAAAAAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAIAAAAAAAA= + ObjectListView.DesignTime.cs + + + + + + AAAAAAAAAAAAAAIAAAABEAAAgQAAAAAAAAAAQAIAAIg= + + + + + + AAAAAAAAAAAAAgIAAAACAAEGAAAAAEAAAAAAABAAQAA= + DataTreeListView.cs + + + + + + BAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAA= + DragDrop\DragSource.cs + + + + + + AAAAAAAAAAAQQAAAAAIAEAAAAAEFAAAAgAAAAAAAAAA= + DragDrop\DropSink.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAA= + Filtering\Filters.cs + + + + + + AAAAAAAQAAAAAAAAAAAAAAQAgAAAAAAAAAAAAAAAAAA= + Filtering\ICluster.cs + + + + + + AAAAAAAAAAAAAAAAABBAAAAAAAAAAAQBAQAAAAAAAAA= + Filtering\IClusteringStrategy.cs + + + + + + AAAAAAAAAAAAAACAAAAEAAAAAAAAAAAEAAAACAAAAAA= + Implementation\VirtualGroups.cs + + + + + + AIAAAAAAAAAAAAAAAAAAAgAIAAAQAAAAAAAEAEAAAAA= + Implementation\VirtualGroups.cs + + + + + + ABAAAAAAAAAAAAAgAAASAgAQAAAEAAAAAAAAAAAAgAA= + Implementation\VirtualListDataSource.cs + + + + + + AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA= + Implementation\VirtualListDataSource.cs + + + + + + AAAAAAAAAAAAAAAAAQAAAAAAAAQAAAAAAAAAAAAAAAA= + Rendering\Decorations.cs + + + + + + AAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAA= + Rendering\Overlays.cs + + + + + + AAAAAQAAAABAAAAAABAAAAAAAAAAABAAAAAAAAAAAAA= + Rendering\Renderers.cs + + + + + + AAAAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAADAAA= + Rendering\Styles.cs + + + + + + AAAgAAAACAAAAAAAAAAAAAAAAAAQAAAAAAAAAEAAAAA= + CellEditing\CellEditKeyEngine.cs + + + + + + gAEAAAAQCAIAAAAAAIAAAAAAAUAQIABAAEAgAAAgACA= + CellEditing\CellEditKeyEngine.cs + + + + + + AAAAAQAAAAABAAAAAAAAAAAEAAQBBAAAAAIgAAEAAAA= + DragDrop\DropSink.cs + + + + + + AAAAAAAAJAAAAAAAAAAAAAIAAAAAACIEAAAAAAAAAAA= + Filtering\DateTimeClusteringStrategy.cs + + + + + + AEAAAAAAAAAAABAAEAQAARAAIQAAAAQAAAAAAEAAAAA= + Implementation\Groups.cs + + + + + + AgAAgAAAwABAAAAAAAUAAAIAgQAAAAAEACAgAAAFAAA= + Implementation\Groups.cs + + + + + + AAAAAAQAAAAAAAAAAAAAAQAAAAAAEAAAAIAAAAAAAAA= + Implementation\Groups.cs + + + + + + ASAAEEAAAAAAAAQAEAAAAAAAAAAAABAAAAAACAAAEAA= + Implementation\OlvListViewHitTestInfo.cs + + + + + + AAAEAAAAAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAA= + CellEditing\EditorRegistry.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAgA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAQAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAgAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAEAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAQABgAAAAACAQAAAAAAAAAAAAAAAAAAAAAAgAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAABAAAACAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAABAAAAAAgACAAAAACAAAAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA= + Implementation\Delegates.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAABAAAAAAAAAA= + Implementation\Events.cs + + + + \ No newline at end of file diff --git a/ObjectListView/Implementation/Attributes.cs b/ObjectListView/Implementation/Attributes.cs new file mode 100644 index 0000000..363f421 --- /dev/null +++ b/ObjectListView/Implementation/Attributes.cs @@ -0,0 +1,335 @@ +/* + * Attributes - Attributes that can be attached to properties of models to allow columns to be + * built from them directly + * + * Author: Phillip Piper + * Date: 15/08/2009 22:01 + * + * Change log: + * v2.6 + * 2012-08-16 JPP - Added [OLVChildren] and [OLVIgnore] + * - OLV attributes can now only be set on properties + * v2.4 + * 2010-04-14 JPP - Allow Name property to be set + * + * v2.3 + * 2009-08-15 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// This attribute is used to mark a property of a model + /// class that should be noticed by Generator class. + /// + /// + /// All the attributes of this class match their equivilent properties on OLVColumn. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVColumnAttribute : Attribute + { + #region Constructor + + // There are several property where we actually want nullable value (bool?, int?), + // but it seems attribute properties can't be nullable types. + // So we explicitly track if those properties have been set. + + /// + /// Create a new OLVColumnAttribute + /// + public OLVColumnAttribute() { + } + + /// + /// Create a new OLVColumnAttribute with the given title + /// + /// The title of the column + public OLVColumnAttribute(string title) { + this.Title = title; + } + + #endregion + + #region Public properties + + /// + /// + /// + public string AspectToStringFormat { + get { return aspectToStringFormat; } + set { aspectToStringFormat = value; } + } + private string aspectToStringFormat; + + /// + /// + /// + public bool CheckBoxes { + get { return checkBoxes; } + set { + checkBoxes = value; + this.IsCheckBoxesSet = true; + } + } + private bool checkBoxes; + internal bool IsCheckBoxesSet = false; + + /// + /// + /// + public int DisplayIndex { + get { return displayIndex; } + set { displayIndex = value; } + } + private int displayIndex = -1; + + /// + /// + /// + public bool FillsFreeSpace { + get { return fillsFreeSpace; } + set { fillsFreeSpace = value; } + } + private bool fillsFreeSpace; + + /// + /// + /// + public int FreeSpaceProportion { + get { return freeSpaceProportion; } + set { + freeSpaceProportion = value; + IsFreeSpaceProportionSet = true; + } + } + private int freeSpaceProportion; + internal bool IsFreeSpaceProportionSet = false; + + /// + /// An array of IComparables that mark the cutoff points for values when + /// grouping on this column. + /// + public object[] GroupCutoffs { + get { return groupCutoffs; } + set { groupCutoffs = value; } + } + private object[] groupCutoffs; + + /// + /// + /// + public string[] GroupDescriptions { + get { return groupDescriptions; } + set { groupDescriptions = value; } + } + private string[] groupDescriptions; + + /// + /// + /// + public string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// + /// + public string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// + /// + public bool Hyperlink { + get { return hyperlink; } + set { hyperlink = value; } + } + private bool hyperlink; + + /// + /// + /// + public string ImageAspectName { + get { return imageAspectName; } + set { imageAspectName = value; } + } + private string imageAspectName; + + /// + /// + /// + public bool IsEditable { + get { return isEditable; } + set { + isEditable = value; + this.IsEditableSet = true; + } + } + private bool isEditable = true; + internal bool IsEditableSet = false; + + /// + /// + /// + public bool IsVisible { + get { return isVisible; } + set { isVisible = value; } + } + private bool isVisible = true; + + /// + /// + /// + public bool IsTileViewColumn { + get { return isTileViewColumn; } + set { isTileViewColumn = value; } + } + private bool isTileViewColumn; + + /// + /// + /// + public int MaximumWidth { + get { return maximumWidth; } + set { maximumWidth = value; } + } + private int maximumWidth = -1; + + /// + /// + /// + public int MinimumWidth { + get { return minimumWidth; } + set { minimumWidth = value; } + } + private int minimumWidth = -1; + + /// + /// + /// + public String Name { + get { return name; } + set { name = value; } + } + private String name; + + /// + /// + /// + public HorizontalAlignment TextAlign { + get { return this.textAlign; } + set { + this.textAlign = value; + IsTextAlignSet = true; + } + } + private HorizontalAlignment textAlign = HorizontalAlignment.Left; + internal bool IsTextAlignSet = false; + + /// + /// + /// + public String Tag { + get { return tag; } + set { tag = value; } + } + private String tag; + + /// + /// + /// + public String Title { + get { return title; } + set { title = value; } + } + private String title; + + /// + /// + /// + public String ToolTipText { + get { return toolTipText; } + set { toolTipText = value; } + } + private String toolTipText; + + /// + /// + /// + public bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + this.IsTriStateCheckBoxesSet = true; + } + } + private bool triStateCheckBoxes; + internal bool IsTriStateCheckBoxesSet = false; + + /// + /// + /// + public bool UseInitialLetterForGroup { + get { return useInitialLetterForGroup; } + set { useInitialLetterForGroup = value; } + } + private bool useInitialLetterForGroup; + + /// + /// + /// + public int Width { + get { return width; } + set { width = value; } + } + private int width = 150; + + #endregion + } + + /// + /// Properties marked with [OLVChildren] will be used as the children source in a TreeListView. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVChildrenAttribute : Attribute + { + + } + + /// + /// Properties marked with [OLVIgnore] will not have columns generated for them. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OLVIgnoreAttribute : Attribute + { + + } +} diff --git a/ObjectListView/Implementation/Comparers.cs b/ObjectListView/Implementation/Comparers.cs new file mode 100644 index 0000000..ec79f19 --- /dev/null +++ b/ObjectListView/Implementation/Comparers.cs @@ -0,0 +1,291 @@ +/* + * Comparers - Various Comparer classes used within ObjectListView + * + * Author: Phillip Piper + * Date: 25/11/2008 17:15 + * + * Change log: + * v2.3 + * 2009-08-24 JPP - Added OLVGroupComparer + * 2009-06-01 JPP - ModelObjectComparer would crash if secondary sort column was null. + * 2008-12-20 JPP - Fixed bug with group comparisons when a group key was null (SF#2445761) + * 2008-11-25 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// ColumnComparer is the workhorse for all comparison between two values of a particular column. + /// If the column has a specific comparer, use that to compare the values. Otherwise, do + /// a case insensitive string compare of the string representations of the values. + /// + /// This class inherits from both IComparer and its generic counterpart + /// so that it can be used on untyped and typed collections. + public class ColumnComparer : IComparer, IComparer + { + /// + /// Create a ColumnComparer that will order the rows in a list view according + /// to the values in a given column + /// + /// The column whose values will be compared + /// The ordering for column values + public ColumnComparer(OLVColumn col, SortOrder order) + { + this.column = col; + this.sortOrder = order; + } + + /// + /// Create a ColumnComparer that will order the rows in a list view according + /// to the values in a given column, and by a secondary column if the primary + /// column is equal. + /// + /// The column whose values will be compared + /// The ordering for column values + /// The column whose values will be compared for secondary sorting + /// The ordering for secondary column values + public ColumnComparer(OLVColumn col, SortOrder order, OLVColumn col2, SortOrder order2) + : this(col, order) + { + // There is no point in secondary sorting on the same column + if (col != col2) + this.secondComparer = new ColumnComparer(col2, order2); + } + + /// + /// Compare two rows + /// + /// row1 + /// row2 + /// An ordering indication: -1, 0, 1 + public int Compare(object x, object y) + { + return this.Compare((OLVListItem)x, (OLVListItem)y); + } + + /// + /// Compare two rows + /// + /// row1 + /// row2 + /// An ordering indication: -1, 0, 1 + public int Compare(OLVListItem x, OLVListItem y) + { + if (this.sortOrder == SortOrder.None) + return 0; + + int result = 0; + object x1 = this.column.GetValue(x.RowObject); + object y1 = this.column.GetValue(y.RowObject); + + // Handle nulls. Null values come last + bool xIsNull = (x1 == null || x1 == System.DBNull.Value); + bool yIsNull = (y1 == null || y1 == System.DBNull.Value); + if (xIsNull || yIsNull) { + if (xIsNull && yIsNull) + result = 0; + else + result = (xIsNull ? -1 : 1); + } else { + result = this.CompareValues(x1, y1); + } + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + // If the result was equality, use the secondary comparer to resolve it + if (result == 0 && this.secondComparer != null) + result = this.secondComparer.Compare(x, y); + + return result; + } + + /// + /// Compare the actual values to be used for sorting + /// + /// The aspect extracted from the first row + /// The aspect extracted from the second row + /// An ordering indication: -1, 0, 1 + public int CompareValues(object x, object y) + { + // Force case insensitive compares on strings + String xAsString = x as String; + if (xAsString != null) + return String.Compare(xAsString, (String)y, StringComparison.CurrentCultureIgnoreCase); + else { + IComparable comparable = x as IComparable; + if (comparable != null) + return comparable.CompareTo(y); + else + return 0; + } + } + + private OLVColumn column; + private SortOrder sortOrder; + private ColumnComparer secondComparer; + } + + + /// + /// This comparer sort list view groups. OLVGroups have a "SortValue" property, + /// which is used if present. Otherwise, the titles of the groups will be compared. + /// + public class OLVGroupComparer : IComparer + { + /// + /// Create a group comparer + /// + /// The ordering for column values + public OLVGroupComparer(SortOrder order) { + this.sortOrder = order; + } + + /// + /// Compare the two groups. OLVGroups have a "SortValue" property, + /// which is used if present. Otherwise, the titles of the groups will be compared. + /// + /// group1 + /// group2 + /// An ordering indication: -1, 0, 1 + public int Compare(OLVGroup x, OLVGroup y) { + // If we can compare the sort values, do that. + // Otherwise do a case insensitive compare on the group header. + int result; + if (x.SortValue != null && y.SortValue != null) + result = x.SortValue.CompareTo(y.SortValue); + else + result = String.Compare(x.Header, y.Header, StringComparison.CurrentCultureIgnoreCase); + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + return result; + } + + private SortOrder sortOrder; + } + + /// + /// This comparer can be used to sort a collection of model objects by a given column + /// + public class ModelObjectComparer : IComparer, IComparer + { + /// + /// Create a model object comparer + /// + /// + /// + public ModelObjectComparer(OLVColumn col, SortOrder order) + { + this.column = col; + this.sortOrder = order; + } + + /// + /// Create a model object comparer with a secondary sorting column + /// + /// + /// + /// + /// + public ModelObjectComparer(OLVColumn col, SortOrder order, OLVColumn col2, SortOrder order2) + : this(col, order) + { + // There is no point in secondary sorting on the same column + if (col != col2 && col2 != null && order2 != SortOrder.None) + this.secondComparer = new ModelObjectComparer(col2, order2); + } + + /// + /// Compare the two model objects + /// + /// + /// + /// + public int Compare(object x, object y) + { + int result = 0; + object x1 = this.column.GetValue(x); + object y1 = this.column.GetValue(y); + + if (this.sortOrder == SortOrder.None) + return 0; + + // Handle nulls. Null values come last + bool xIsNull = (x1 == null || x1 == System.DBNull.Value); + bool yIsNull = (y1 == null || y1 == System.DBNull.Value); + if (xIsNull || yIsNull) { + if (xIsNull && yIsNull) + result = 0; + else + result = (xIsNull ? -1 : 1); + } else { + result = this.CompareValues(x1, y1); + } + + if (this.sortOrder == SortOrder.Descending) + result = 0 - result; + + // If the result was equality, use the secondary comparer to resolve it + if (result == 0 && this.secondComparer != null) + result = this.secondComparer.Compare(x, y); + + return result; + } + + /// + /// Compare the actual values + /// + /// + /// + /// + public int CompareValues(object x, object y) + { + // Force case insensitive compares on strings + String xStr = x as String; + if (xStr != null) + return String.Compare(xStr, (String)y, StringComparison.CurrentCultureIgnoreCase); + else { + IComparable comparable = x as IComparable; + if (comparable != null) + return comparable.CompareTo(y); + else + return 0; + } + } + + private OLVColumn column; + private SortOrder sortOrder; + private ModelObjectComparer secondComparer; + + #region IComparer Members + + #endregion + } + +} \ No newline at end of file diff --git a/ObjectListView/Implementation/DataSourceAdapter.cs b/ObjectListView/Implementation/DataSourceAdapter.cs new file mode 100644 index 0000000..3dabd6c --- /dev/null +++ b/ObjectListView/Implementation/DataSourceAdapter.cs @@ -0,0 +1,613 @@ +/* + * DataSourceAdapter - A helper class that translates DataSource events for an ObjectListView + * + * Author: Phillip Piper + * Date: 20/09/2010 7:42 AM + * + * Change log: + * v2.6 + * 2012-08-16 JPP - Unify common column creation functionality with Generator when possible + * + * 2010-09-20 JPP - Initial version + * + * Copyright (C) 2010-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing.Design; +using System.Globalization; +using System.Windows.Forms; +using System.Diagnostics; + +namespace BrightIdeasSoftware +{ + /// + /// A helper class that translates DataSource events for an ObjectListView + /// + public class DataSourceAdapter : IDisposable + { + #region Life and death + + /// + /// Make a DataSourceAdapter + /// + public DataSourceAdapter(ObjectListView olv) { + if (olv == null) throw new ArgumentNullException("olv"); + + this.ListView = olv; + this.BindListView(this.ListView); + } + + /// + /// Finalize this object + /// + ~DataSourceAdapter() { + this.Dispose(false); + } + + /// + /// Release all the resources used by this instance + /// + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Release all the resources used by this instance + /// + public virtual void Dispose(bool fromUser) { + this.UnbindListView(this.ListView); + this.UnbindDataSource(); + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets whether or not columns will be automatically generated to show the + /// columns when the DataSource is set. + /// + /// This must be set before the DataSource is set. It has no effect afterwards. + public bool AutoGenerateColumns { + get { return this.autoGenerateColumns; } + set { this.autoGenerateColumns = value; } + } + private bool autoGenerateColumns = true; + + /// + /// Get or set the DataSource that will be displayed in this list view. + /// + public virtual Object DataSource { + get { return dataSource; } + set { + dataSource = value; + this.RebindDataSource(true); + } + } + private Object dataSource; + + /// + /// Gets or sets the name of the list or table in the data source for which the DataListView is displaying data. + /// + /// If the data source is not a DataSet or DataViewManager, this property has no effect + public virtual string DataMember { + get { return dataMember; } + set { + if (dataMember != value) { + dataMember = value; + RebindDataSource(); + } + } + } + private string dataMember = ""; + + /// + /// Gets the ObjectListView upon which this adaptor will operate + /// + public ObjectListView ListView { + get { return listView; } + internal set { listView = value; } + } + private ObjectListView listView; + + #endregion + + #region Implementation properties + + /// + /// Gets or sets the currency manager which is handling our binding context + /// + protected CurrencyManager CurrencyManager { + get { return currencyManager; } + set { currencyManager = value; } + } + private CurrencyManager currencyManager = null; + + #endregion + + #region Binding and unbinding + + /// + /// + /// + /// + protected virtual void BindListView(ObjectListView olv) { + if (olv == null) + return; + + olv.Freezing += new EventHandler(HandleListViewFreezing); + olv.SelectedIndexChanged += new EventHandler(HandleListViewSelectedIndexChanged); + olv.BindingContextChanged += new EventHandler(HandleListViewBindingContextChanged); + } + + /// + /// + /// + /// + protected virtual void UnbindListView(ObjectListView olv) { + if (olv == null) + return; + + olv.Freezing -= new EventHandler(HandleListViewFreezing); + olv.SelectedIndexChanged -= new EventHandler(HandleListViewSelectedIndexChanged); + olv.BindingContextChanged -= new EventHandler(HandleListViewBindingContextChanged); + } + + /// + /// + /// + protected virtual void BindDataSource() { + if (this.CurrencyManager == null) + return; + + this.CurrencyManager.MetaDataChanged += new EventHandler(HandleCurrencyManagerMetaDataChanged); + this.CurrencyManager.PositionChanged += new EventHandler(HandleCurrencyManagerPositionChanged); + this.CurrencyManager.ListChanged += new ListChangedEventHandler(CurrencyManagerListChanged); + } + + /// + /// + /// + protected virtual void UnbindDataSource() { + if (this.CurrencyManager == null) + return; + + this.CurrencyManager.MetaDataChanged -= new EventHandler(HandleCurrencyManagerMetaDataChanged); + this.CurrencyManager.PositionChanged -= new EventHandler(HandleCurrencyManagerPositionChanged); + this.CurrencyManager.ListChanged -= new ListChangedEventHandler(CurrencyManagerListChanged); + } + + #endregion + + #region Initialization + + /// + /// Our data source has changed. Figure out how to handle the new source + /// + protected virtual void RebindDataSource() { + RebindDataSource(false); + } + + /// + /// Our data source has changed. Figure out how to handle the new source + /// + protected virtual void RebindDataSource(bool forceDataInitialization) { + + CurrencyManager tempCurrencyManager = null; + if (this.ListView != null && this.ListView.BindingContext != null && this.DataSource != null) { + tempCurrencyManager = this.ListView.BindingContext[this.DataSource, this.DataMember] as CurrencyManager; + } + + // Has our currency manager changed? + if (this.CurrencyManager != tempCurrencyManager) { + this.UnbindDataSource(); + this.CurrencyManager = tempCurrencyManager; + this.BindDataSource(); + + // Our currency manager has changed so we have to initialize a new data source + forceDataInitialization = true; + } + + if (forceDataInitialization) + InitializeDataSource(); + } + + /// + /// The data source for this control has changed. Reconfigure the control for the new source + /// + protected virtual void InitializeDataSource() { + if (this.ListView.Frozen || this.CurrencyManager == null) + return; + + this.CreateColumnsFromSource(); + this.CreateMissingAspectGettersAndPutters(); + this.SetListContents(); + this.ListView.AutoSizeColumns(); + } + + /// + /// Take the contents of the currently bound list and put them into the control + /// + protected virtual void SetListContents() { + this.ListView.Objects = this.CurrencyManager.List; + } + + /// + /// Create columns for the listview based on what properties are available in the data source + /// + /// + /// This method will create columns if there is not already a column displaying that property. + /// + protected virtual void CreateColumnsFromSource() { + if (this.CurrencyManager == null) + return; + + // Don't generate any columns in design mode. If we do, the user will see them, + // but the Designer won't know about them and won't persist them, which is very confusing + if (this.ListView.IsDesignMode) + return; + + // Don't create columns if we've been told not to + if (!this.AutoGenerateColumns) + return; + + // Use a Generator to create columns + Generator generator = Generator.Instance as Generator ?? new Generator(); + + PropertyDescriptorCollection properties = this.CurrencyManager.GetItemProperties(); + if (properties.Count == 0) + return; + + foreach (PropertyDescriptor property in properties) { + + if (!this.ShouldCreateColumn(property)) + continue; + + // Create a column + OLVColumn column = generator.MakeColumnFromPropertyDescriptor(property); + this.ConfigureColumn(column, property); + + // Add it to our list + this.ListView.AllColumns.Add(column); + } + + generator.PostCreateColumns(this.ListView); + } + + /// + /// Decide if a new column should be added to the control to display + /// the given property + /// + /// + /// + protected virtual bool ShouldCreateColumn(PropertyDescriptor property) { + + // Is there a column that already shows this property? If so, we don't show it again + if (this.ListView.AllColumns.Exists(delegate(OLVColumn x) { return x.AspectName == property.Name; })) + return false; + + // Relationships to other tables turn up as IBindibleLists. Don't make columns to show them. + // CHECK: Is this always true? What other things could be here? Constraints? Triggers? + if (property.PropertyType == typeof(IBindingList)) + return false; + + // Ignore anything marked with [OLVIgnore] + return property.Attributes[typeof(OLVIgnoreAttribute)] == null; + } + + /// + /// Configure the given column to show the given property. + /// The title and aspect name of the column are already filled in. + /// + /// + /// + protected virtual void ConfigureColumn(OLVColumn column, PropertyDescriptor property) { + + column.LastDisplayIndex = this.ListView.AllColumns.Count; + + // If our column is a BLOB, it could be an image, so assign a renderer to draw it. + // CONSIDER: Is this a common enough case to warrant this code? + if (property.PropertyType == typeof(System.Byte[])) + column.Renderer = new ImageRenderer(); + } + + /// + /// Generate aspect getters and putters for any columns that are missing them (and for which we have + /// enough information to actually generate a getter) + /// + protected virtual void CreateMissingAspectGettersAndPutters() { + foreach (OLVColumn x in this.ListView.AllColumns) { + OLVColumn column = x; // stack based variable accessible from closures + if (column.AspectGetter == null && !String.IsNullOrEmpty(column.AspectName)) { + column.AspectGetter = delegate(object row) { + // In most cases, rows will be DataRowView objects + DataRowView drv = row as DataRowView; + if (drv == null) + return column.GetAspectByName(row); + return (drv.Row.RowState == DataRowState.Detached) ? null : drv[column.AspectName]; + }; + } + if (column.IsEditable && column.AspectPutter == null && !String.IsNullOrEmpty(column.AspectName)) { + column.AspectPutter = delegate(object row, object newValue) { + // In most cases, rows will be DataRowView objects + DataRowView drv = row as DataRowView; + if (drv == null) + column.PutAspectByName(row, newValue); + else { + if (drv.Row.RowState != DataRowState.Detached) + drv[column.AspectName] = newValue; + } + }; + } + } + } + + #endregion + + #region Event Handlers + + /// + /// CurrencyManager ListChanged event handler. + /// Deals with fine-grained changes to list items. + /// + /// + /// It's actually difficult to deal with these changes in a fine-grained manner. + /// If our listview is grouped, then any change may make a new group appear or + /// an old group disappear. It is rarely enough to simply update the affected row. + /// + /// + /// + protected virtual void CurrencyManagerListChanged(object sender, ListChangedEventArgs e) { + Debug.Assert(sender == this.CurrencyManager); + + // Ignore changes make while frozen, since we will do a complete rebuild when we unfreeze + if (this.ListView.Frozen) + return; + + //System.Diagnostics.Debug.WriteLine(e.ListChangedType); + //Stopwatch sw = new Stopwatch(); + //sw.Start(); + switch (e.ListChangedType) { + + case ListChangedType.Reset: + this.HandleListChangedReset(e); + break; + + case ListChangedType.ItemChanged: + this.HandleListChangedItemChanged(e); + break; + + case ListChangedType.ItemAdded: + this.HandleListChangedItemAdded(e); + break; + + // An item has gone away. + case ListChangedType.ItemDeleted: + this.HandleListChangedItemDeleted(e); + break; + + // An item has changed its index. + case ListChangedType.ItemMoved: + this.HandleListChangedItemMoved(e); + break; + + // Something has changed in the metadata. + // CHECK: When are these events actually fired? + case ListChangedType.PropertyDescriptorAdded: + case ListChangedType.PropertyDescriptorChanged: + case ListChangedType.PropertyDescriptorDeleted: + this.HandleListChangedMetadataChanged(e); + break; + } + //sw.Stop(); + //System.Diagnostics.Debug.WriteLine(String.Format("Processing {0} event on {1} rows took {2}ms", e.ListChangedType, this.ListView.GetItemCount(), sw.ElapsedMilliseconds)); + + } + + /// + /// Handle PropertyDescriptor* events + /// + /// + protected virtual void HandleListChangedMetadataChanged(ListChangedEventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Handle ItemMoved event + /// + /// + protected virtual void HandleListChangedItemMoved(ListChangedEventArgs e) { + // When is this actually triggered? + this.InitializeDataSource(); + } + + /// + /// Handle the ItemDeleted event + /// + /// + protected virtual void HandleListChangedItemDeleted(ListChangedEventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Handle an ItemAdded event. + /// + /// + protected virtual void HandleListChangedItemAdded(ListChangedEventArgs e) { + // We get this event twice if certain grid controls are used to add a new row to a + // datatable: once when the editing of a new row begins, and once again when that + // editing commits. (If the user cancels the creation of the new row, we never see + // the second creation.) We detect this by seeing if this is a view on a row in a + // DataTable, and if it is, testing to see if it's a new row under creation. + + Object newRow = this.CurrencyManager.List[e.NewIndex]; + DataRowView drv = newRow as DataRowView; + if (drv == null || !drv.IsNew) { + // Either we're not dealing with a view on a data table, or this is the commit + // notification. Either way, this is the final notification, so we want to + // handle the new row now! + this.InitializeDataSource(); + } + } + + /// + /// Handle the Reset event + /// + /// + protected virtual void HandleListChangedReset(ListChangedEventArgs e) { + // The whole list has changed utterly, so reload it. + this.InitializeDataSource(); + } + + /// + /// Handle ItemChanged event. This is triggered when a single item + /// has changed, so just refresh that one item. + /// + /// + /// Even in this simple case, we should probably rebuild the list. + /// For example, the change could put the item into its own new group. + protected virtual void HandleListChangedItemChanged(ListChangedEventArgs e) { + // A single item has changed, so just refresh that. + //System.Diagnostics.Debug.WriteLine(String.Format("HandleListChangedItemChanged: {0}, {1}", e.NewIndex, e.PropertyDescriptor.Name)); + + Object changedRow = this.CurrencyManager.List[e.NewIndex]; + this.ListView.RefreshObject(changedRow); + } + + /// + /// The CurrencyManager calls this if the data source looks + /// different. We just reload everything. + /// + /// + /// + /// + /// CHECK: Do we need this if we are handle ListChanged metadata events? + /// + protected virtual void HandleCurrencyManagerMetaDataChanged(object sender, EventArgs e) { + this.InitializeDataSource(); + } + + /// + /// Called by the CurrencyManager when the currently selected item + /// changes. We update the ListView selection so that we stay in sync + /// with any other controls bound to the same source. + /// + /// + /// + protected virtual void HandleCurrencyManagerPositionChanged(object sender, EventArgs e) { + int index = this.CurrencyManager.Position; + + // Make sure the index is sane (-1 pops up from time to time) + if (index < 0 || index >= this.ListView.GetItemCount()) + return; + + // Avoid recursion. If we are currently changing the index, don't + // start the process again. + if (this.isChangingIndex) + return; + + try { + this.isChangingIndex = true; + + this.ChangePosition(index); + } + finally { + this.isChangingIndex = false; + } + } + private bool isChangingIndex = false; + + /// + /// Change the control's position (which is it's currently selected row) + /// to the nth row in the dataset + /// + /// The index of the row to be selected + protected virtual void ChangePosition(int index) { + // We can't use the index directly, since our listview may be sorted + this.ListView.SelectedObject = this.CurrencyManager.List[index]; + + // THINK: Do we always want to bring it into view? + if (this.ListView.SelectedIndices.Count > 0) + this.ListView.EnsureVisible(this.ListView.SelectedIndices[0]); + } + + #endregion + + #region ObjectListView event handlers + + /// + /// Handle the selection changing in our ListView. + /// We need to tell our currency manager about the new position. + /// + /// + /// + protected virtual void HandleListViewSelectedIndexChanged(object sender, EventArgs e) { + // Prevent recursion + if (this.isChangingIndex) + return; + + // If we are bound to a data source, and only one item is selected, + // tell the currency manager which item is selected. + if (this.ListView.SelectedIndices.Count == 1 && this.CurrencyManager != null) { + try { + this.isChangingIndex = true; + + // We can't use the selectedIndex directly, since our listview may be sorted. + // So we have to find the index of the selected object within the original list. + this.CurrencyManager.Position = this.CurrencyManager.List.IndexOf(this.ListView.SelectedObject); + } finally { + this.isChangingIndex = false; + } + } + } + + /// + /// Handle the frozenness of our ListView changing. + /// + /// + /// + protected virtual void HandleListViewFreezing(object sender, FreezeEventArgs e) { + if (!alreadyFreezing && e.FreezeLevel == 0) { + try { + alreadyFreezing = true; + this.RebindDataSource(true); + } finally { + alreadyFreezing = false; + } + } + } + private bool alreadyFreezing = false; + + /// + /// Handle a change to the BindingContext of our ListView. + /// + /// + /// + protected virtual void HandleListViewBindingContextChanged(object sender, EventArgs e) { + this.RebindDataSource(false); + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/Delegates.cs b/ObjectListView/Implementation/Delegates.cs new file mode 100644 index 0000000..21df5cc --- /dev/null +++ b/ObjectListView/Implementation/Delegates.cs @@ -0,0 +1,151 @@ +/* + * Delegates - All delegate definitions used in ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + #region Delegate declarations + + /// + /// These delegates are used to extract an aspect from a row object + /// + public delegate Object AspectGetterDelegate(Object rowObject); + + /// + /// These delegates are used to put a changed value back into a model object + /// + public delegate void AspectPutterDelegate(Object rowObject, Object newValue); + + /// + /// These delegates can be used to convert an aspect value to a display string, + /// instead of using the default ToString() + /// + public delegate string AspectToStringConverterDelegate(Object value); + + /// + /// These delegates are used to get the tooltip for a cell + /// + public delegate String CellToolTipGetterDelegate(OLVColumn column, Object modelObject); + + /// + /// These delegates are used to the state of the checkbox for a row object. + /// + /// + /// For reasons known only to someone in Microsoft, we can only set + /// a boolean on the ListViewItem to indicate it's "checked-ness", but when + /// we receive update events, we have to use a tristate CheckState. So we can + /// be told about an indeterminate state, but we can't set it ourselves. + /// + /// As of version 2.0, we can now return indeterminate state. + /// + public delegate CheckState CheckStateGetterDelegate(Object rowObject); + + /// + /// These delegates are used to get the state of the checkbox for a row object. + /// + /// + /// + public delegate bool BooleanCheckStateGetterDelegate(Object rowObject); + + /// + /// These delegates are used to put a changed check state back into a model object + /// + public delegate CheckState CheckStatePutterDelegate(Object rowObject, CheckState newValue); + + /// + /// These delegates are used to put a changed check state back into a model object + /// + /// + /// + /// + public delegate bool BooleanCheckStatePutterDelegate(Object rowObject, bool newValue); + + /// + /// The callbacks for RightColumnClick events + /// + public delegate void ColumnRightClickEventHandler(object sender, ColumnClickEventArgs e); + + /// + /// This delegate will be used to own draw header column. + /// + public delegate bool HeaderDrawingDelegate(Graphics g, Rectangle r, int columnIndex, OLVColumn column, bool isPressed, HeaderStateStyle stateStyle); + + /// + /// This delegate is called when a group has been created but not yet made + /// into a real ListViewGroup. The user can take this opportunity to fill + /// in lots of other details about the group. + /// + public delegate void GroupFormatterDelegate(OLVGroup group, GroupingParameters parms); + + /// + /// These delegates are used to retrieve the object that is the key of the group to which the given row belongs. + /// + public delegate Object GroupKeyGetterDelegate(Object rowObject); + + /// + /// These delegates are used to convert a group key into a title for the group + /// + public delegate string GroupKeyToTitleConverterDelegate(Object groupKey); + + /// + /// These delegates are used to get the tooltip for a column header + /// + public delegate String HeaderToolTipGetterDelegate(OLVColumn column); + + /// + /// These delegates are used to fetch the image selector that should be used + /// to choose an image for this column. + /// + public delegate Object ImageGetterDelegate(Object rowObject); + + /// + /// These delegates are used to draw a cell + /// + public delegate bool RenderDelegate(EventArgs e, Graphics g, Rectangle r, Object rowObject); + + /// + /// These delegates are used to fetch a row object for virtual lists + /// + public delegate Object RowGetterDelegate(int rowIndex); + + /// + /// These delegates are used to format a listviewitem before it is added to the control. + /// + public delegate void RowFormatterDelegate(OLVListItem olvItem); + + /// + /// These delegates are used to sort the listview in some custom fashion + /// + public delegate void SortDelegate(OLVColumn column, SortOrder sortOrder); + + #endregion +} diff --git a/ObjectListView/Implementation/DragSource.cs b/ObjectListView/Implementation/DragSource.cs new file mode 100644 index 0000000..f0f4783 --- /dev/null +++ b/ObjectListView/Implementation/DragSource.cs @@ -0,0 +1,407 @@ +/* + * DragSource.cs - Add drag source functionality to an ObjectListView + * + * UNFINISHED + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * v2.3 + * 2009-07-06 JPP - Make sure Link is acceptable as an drop effect by default + * (since MS didn't make it part of the 'All' value) + * v2.2 + * 2009-04-15 JPP - Separated DragSource.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware +{ + /// + /// An IDragSource controls how drag out from the ObjectListView will behave + /// + public interface IDragSource + { + /// + /// A drag operation is beginning. Return the data object that will be used + /// for data transfer. Return null to prevent the drag from starting. + /// + /// + /// The returned object is later passed to the GetAllowedEffect() and EndDrag() + /// methods. + /// + /// What ObjectListView is being dragged from. + /// Which mouse button is down? + /// What item was directly dragged by the user? There may be more than just this + /// item selected. + /// The data object that will be used for data transfer. This will often be a subclass + /// of DataObject, but does not need to be. + Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item); + + /// + /// What operations are possible for this drag? This controls the icon shown during the drag + /// + /// The data object returned by StartDrag() + /// A combination of DragDropEffects flags + DragDropEffects GetAllowedEffects(Object dragObject); + + /// + /// The drag operation is complete. Do whatever is necessary to complete the action. + /// + /// The data object returned by StartDrag() + /// The value returned from GetAllowedEffects() + void EndDrag(Object dragObject, DragDropEffects effect); + } + + /// + /// A do-nothing implementation of IDragSource that can be safely subclassed. + /// + public class AbstractDragSource : IDragSource + { + #region IDragSource Members + + /// + /// See IDragSource documentation + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + return null; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.None; + } + + /// + /// See IDragSource documentation + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + } + + #endregion + } + + /// + /// A reasonable implementation of IDragSource that provides normal + /// drag source functionality. It creates a data object that supports + /// inter-application dragging of text and HTML representation of + /// the dragged rows. It can optionally force a refresh of all dragged + /// rows when the drag is complete. + /// + /// Subclasses can override GetDataObject() to add new + /// data formats to the data transfer object. + public class SimpleDragSource : IDragSource + { + #region Constructors + + /// + /// Construct a SimpleDragSource + /// + public SimpleDragSource() { + } + + /// + /// Construct a SimpleDragSource that refreshes the dragged rows when + /// the drag is complete + /// + /// + public SimpleDragSource(bool refreshAfterDrop) { + this.RefreshAfterDrop = refreshAfterDrop; + } + + #endregion + + #region Public properties + + /// + /// Gets or sets whether the dragged rows should be refreshed when the + /// drag operation is complete. + /// + public bool RefreshAfterDrop { + get { return refreshAfterDrop; } + set { refreshAfterDrop = value; } + } + private bool refreshAfterDrop; + + #endregion + + #region IDragSource Members + + /// + /// Create a DataObject when the user does a left mouse drag operation. + /// See IDragSource for further information. + /// + /// + /// + /// + /// + public virtual Object StartDrag(ObjectListView olv, MouseButtons button, OLVListItem item) { + // We only drag on left mouse + if (button != MouseButtons.Left) + return null; + + return this.CreateDataObject(olv); + } + + /// + /// Which operations are allowed in the operation? By default, all operations are supported. + /// + /// + /// All opertions are supported + public virtual DragDropEffects GetAllowedEffects(Object data) { + return DragDropEffects.All | DragDropEffects.Link; // why didn't MS include 'Link' in 'All'?? + } + + /// + /// The drag operation is finished. Refreshe the dragged rows if so configured. + /// + /// + /// + public virtual void EndDrag(Object dragObject, DragDropEffects effect) { + OLVDataObject data = dragObject as OLVDataObject; + if (data == null) + return; + + if (this.RefreshAfterDrop) + data.ListView.RefreshObjects(data.ModelObjects); + } + + /// + /// Create a data object that will be used to as the data object + /// for the drag operation. + /// + /// + /// Subclasses can override this method add new formats to the data object. + /// + /// The ObjectListView that is the source of the drag + /// A data object for the drag + protected virtual object CreateDataObject(ObjectListView olv) { + OLVDataObject data = new OLVDataObject(olv); + data.CreateTextFormats(); + return data; + } + + #endregion + } + + /// + /// A data transfer object that knows how to transform a list of model + /// objects into a text and HTML representation. + /// + public class OLVDataObject : DataObject + { + #region Life and death + + /// + /// Create a data object from the selected objects in the given ObjectListView + /// + /// The source of the data object + public OLVDataObject(ObjectListView olv) : this(olv, olv.SelectedObjects) { + } + + /// + /// Create a data object which operates on the given model objects + /// in the given ObjectListView + /// + /// The source of the data object + /// The model objects to be put into the data object + public OLVDataObject(ObjectListView olv, IList modelObjects) { + this.objectListView = olv; + this.modelObjects = modelObjects; + this.includeHiddenColumns = olv.IncludeHiddenColumnsInDataTransfer; + this.includeColumnHeaders = olv.IncludeColumnHeadersInCopy; + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the text + /// and HTML representation. If this is false, only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + } + private bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. + /// + public bool IncludeColumnHeaders + { + get { return includeColumnHeaders; } + } + private bool includeColumnHeaders; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// + public ObjectListView ListView { + get { return objectListView; } + } + private ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + } + private IList modelObjects = new ArrayList(); + + #endregion + + /// + /// Put a text and HTML representation of our model objects + /// into the data object. + /// + public void CreateTextFormats() { + IList columns = this.IncludeHiddenColumns ? this.ListView.AllColumns : this.ListView.ColumnsInDisplayOrder; + + // Build text and html versions of the selection + StringBuilder sbText = new StringBuilder(); + StringBuilder sbHtml = new StringBuilder(""); + + // Include column headers + if (includeColumnHeaders) + { + sbHtml.Append(""); + } + + foreach (object modelObject in this.ModelObjects) + { + sbHtml.Append(""); + } + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) + { + if (col != columns[0]) + { + sbText.Append("\t"); + sbHtml.Append(""); + } + string strValue = col.Text; + sbText.Append(strValue); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbText.AppendLine(); + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) { + if (col != columns[0]) { + sbText.Append("\t"); + sbHtml.Append(""); + } + string strValue = col.GetStringValue(modelObject); + sbText.Append(strValue); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbText.AppendLine(); + sbHtml.AppendLine("
"); + + // Put both the text and html versions onto the clipboard. + // For some reason, SetText() with UnicodeText doesn't set the basic CF_TEXT format, + // but using SetData() does. + //this.SetText(sbText.ToString(), TextDataFormat.UnicodeText); + this.SetData(sbText.ToString()); + this.SetText(ConvertToHtmlFragment(sbHtml.ToString()), TextDataFormat.Html); + } + + /// + /// Make a HTML representation of our model objects + /// + public string CreateHtml() { + IList columns = this.ListView.ColumnsInDisplayOrder; + + // Build html version of the selection + StringBuilder sbHtml = new StringBuilder(""); + + foreach (object modelObject in this.ModelObjects) { + sbHtml.Append(""); + } + sbHtml.AppendLine("
"); + foreach (OLVColumn col in columns) { + if (col != columns[0]) { + sbHtml.Append(""); + } + string strValue = col.GetStringValue(modelObject); + sbHtml.Append(strValue); //TODO: Should encode the string value + } + sbHtml.AppendLine("
"); + + return sbHtml.ToString(); + } + + /// + /// Convert the fragment of HTML into the Clipboards HTML format. + /// + /// The HTML format is found here http://msdn2.microsoft.com/en-us/library/aa767917.aspx + /// + /// The HTML to put onto the clipboard. It must be valid HTML! + /// A string that can be put onto the clipboard and will be recognized as HTML + private string ConvertToHtmlFragment(string fragment) { + // Minimal implementation of HTML clipboard format + string source = "http://www.codeproject.com/KB/list/ObjectListView.aspx"; + + const String MARKER_BLOCK = + "Version:1.0\r\n" + + "StartHTML:{0,8}\r\n" + + "EndHTML:{1,8}\r\n" + + "StartFragment:{2,8}\r\n" + + "EndFragment:{3,8}\r\n" + + "StartSelection:{2,8}\r\n" + + "EndSelection:{3,8}\r\n" + + "SourceURL:{4}\r\n" + + "{5}"; + + int prefixLength = String.Format(MARKER_BLOCK, 0, 0, 0, 0, source, "").Length; + + const String DEFAULT_HTML_BODY = + "" + + "{0}"; + + string html = String.Format(DEFAULT_HTML_BODY, fragment); + int startFragment = prefixLength + html.IndexOf(fragment); + int endFragment = startFragment + fragment.Length; + + return String.Format(MARKER_BLOCK, prefixLength, prefixLength + html.Length, startFragment, endFragment, source, html); + } + } +} diff --git a/ObjectListView/Implementation/DropSink.cs b/ObjectListView/Implementation/DropSink.cs new file mode 100644 index 0000000..03818d8 --- /dev/null +++ b/ObjectListView/Implementation/DropSink.cs @@ -0,0 +1,1402 @@ +/* + * DropSink.cs - Add drop sink ability to an ObjectListView + * + * Author: Phillip Piper + * Date: 2009-03-17 5:15 PM + * + * Change log: + * 2010-08-24 JPP - Moved AcceptExternal property up to SimpleDragSource. + * v2.3 + * 2009-09-01 JPP - Correctly handle case where RefreshObjects() is called for + * objects that were children but are now roots. + * 2009-08-27 JPP - Added ModelDropEventArgs.RefreshObjects() to simplify updating after + * a drag-drop operation + * 2009-08-19 JPP - Changed to use OlvHitTest() + * v2.2.1 + * 2007-07-06 JPP - Added StandardDropActionFromKeys property to OlvDropEventArgs + * v2.2 + * 2009-05-17 JPP - Added a Handled flag to OlvDropEventArgs + * - Tweaked the appearance of the drop-on-background feedback + * 2009-04-15 JPP - Separated DragDrop.cs into DropSink.cs + * 2009-03-17 JPP - Initial version + * + * Copyright (C) 2009-2010 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Objects that implement this interface can acts as the receiver for drop + /// operation for an ObjectListView. + /// + public interface IDropSink + { + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + ObjectListView ListView { get; set; } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + void DrawFeedback(Graphics g, Rectangle bounds); + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + void Drop(DragEventArgs args); + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + void Enter(DragEventArgs args); + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// + void GiveFeedback(GiveFeedbackEventArgs args); + + /// + /// The drag has left the bounds of this control + /// + void Leave(); + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + void Over(DragEventArgs args); + + /// + /// Should the drag be allowed to continue? + /// + /// + void QueryContinue(QueryContinueDragEventArgs args); + } + + /// + /// This is a do-nothing implementation of IDropSink that is a useful + /// base class for more sophisticated implementations. + /// + public class AbstractDropSink : IDropSink + { + #region IDropSink Members + + /// + /// Gets or sets the ObjectListView that is the drop sink + /// + public virtual ObjectListView ListView { + get { return listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public virtual void DrawFeedback(Graphics g, Rectangle bounds) { + } + + /// + /// The user has released the drop over this control + /// + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. This value is returned + /// to the originator of the drag. + /// + /// + public virtual void Drop(DragEventArgs args) { + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public virtual void Enter(DragEventArgs args) { + } + + /// + /// The drag has left the bounds of this control + /// + public virtual void Leave() { + this.Cleanup(); + } + + /// + /// The drag is moving over this control. + /// + /// This is where any drop target should be calculated. + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + /// + public virtual void Over(DragEventArgs args) { + } + + /// + /// Change the cursor to reflect the current drag operation. + /// + /// You only need to override this if you want non-standard cursors. + /// The standard cursors are supplied automatically. + /// + public virtual void GiveFeedback(GiveFeedbackEventArgs args) { + args.UseDefaultCursors = true; + } + + /// + /// Should the drag be allowed to continue? + /// + /// + /// You only need to override this if you want the user to be able + /// to end the drop in some non-standard way, e.g. dragging to a + /// certain point even without releasing the mouse, or going outside + /// the bounds of the application. + /// + /// + public virtual void QueryContinue(QueryContinueDragEventArgs args) { + } + + + #endregion + + #region Commands + + /// + /// This is called when the mouse leaves the drop region and after the + /// drop has completed. + /// + protected virtual void Cleanup() { + } + + #endregion + } + + /// + /// The enum indicates which target has been found for a drop operation + /// + [Flags] + public enum DropTargetLocation + { + /// + /// No applicable target has been found + /// + None = 0, + + /// + /// The list itself is the target of the drop + /// + Background = 0x01, + + /// + /// An item is the target + /// + Item = 0x02, + + /// + /// Between two items (or above the top item or below the bottom item) + /// can be the target. This is not actually ever a target, only a value indicate + /// that it is valid to drop between items + /// + BetweenItems = 0x04, + + /// + /// Above an item is the target + /// + AboveItem = 0x08, + + /// + /// Below an item is the target + /// + BelowItem = 0x10, + + /// + /// A subitem is the target of the drop + /// + SubItem = 0x20, + + /// + /// On the right of an item is the target (not currently used) + /// + RightOfItem = 0x40, + + /// + /// On the left of an item is the target (not currently used) + /// + LeftOfItem = 0x80 + } + + /// + /// This class represents a simple implementation of a drop sink. + /// + /// + /// Actually, it's far from simple and can do quite a lot in its own right. + /// + public class SimpleDropSink : AbstractDropSink + { + #region Life and death + + /// + /// Make a new drop sink + /// + public SimpleDropSink() { + this.timer = new Timer(); + this.timer.Interval = 250; + this.timer.Tick += new EventHandler(this.timer_Tick); + + this.CanDropOnItem = true; + //this.CanDropOnSubItem = true; + //this.CanDropOnBackground = true; + //this.CanDropBetween = true; + + this.FeedbackColor = Color.FromArgb(180, Color.MediumBlue); + this.billboard = new BillboardOverlay(); + } + + #endregion + + #region Public properties + + /// + /// Get or set the locations where a drop is allowed to occur (OR-ed together) + /// + public DropTargetLocation AcceptableLocations { + get { return this.acceptableLocations; } + set { this.acceptableLocations = value; } + } + private DropTargetLocation acceptableLocations; + + /// + /// Gets or sets whether this sink allows model objects to be dragged from other lists + /// + public bool AcceptExternal { + get { return this.acceptExternal; } + set { this.acceptExternal = value; } + } + private bool acceptExternal = true; + + /// + /// Gets or sets whether the ObjectListView should scroll when the user drags + /// something near to the top or bottom rows. + /// + public bool AutoScroll { + get { return this.autoScroll; } + set { this.autoScroll = value; } + } + private bool autoScroll = true; + + /// + /// Gets the billboard overlay that will be used to display feedback + /// messages during a drag operation. + /// + /// Set this to null to stop the feedback. + public BillboardOverlay Billboard { + get { return this.billboard; } + set { this.billboard = value; } + } + private BillboardOverlay billboard; + + /// + /// Get or set whether a drop can occur between items of the list + /// + public bool CanDropBetween { + get { return (this.AcceptableLocations & DropTargetLocation.BetweenItems) == DropTargetLocation.BetweenItems; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.BetweenItems; + else + this.AcceptableLocations &= ~DropTargetLocation.BetweenItems; + } + } + + /// + /// Get or set whether a drop can occur on the listview itself + /// + public bool CanDropOnBackground { + get { return (this.AcceptableLocations & DropTargetLocation.Background) == DropTargetLocation.Background; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Background; + else + this.AcceptableLocations &= ~DropTargetLocation.Background; + } + } + + /// + /// Get or set whether a drop can occur on items in the list + /// + public bool CanDropOnItem { + get { return (this.AcceptableLocations & DropTargetLocation.Item) == DropTargetLocation.Item; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.Item; + else + this.AcceptableLocations &= ~DropTargetLocation.Item; + } + } + + /// + /// Get or set whether a drop can occur on a subitem in the list + /// + public bool CanDropOnSubItem { + get { return (this.AcceptableLocations & DropTargetLocation.SubItem) == DropTargetLocation.SubItem; } + set { + if (value) + this.AcceptableLocations |= DropTargetLocation.SubItem; + else + this.AcceptableLocations &= ~DropTargetLocation.SubItem; + } + } + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { + if (this.dropTargetIndex != value) { + this.dropTargetIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + } + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { + if (this.dropTargetLocation != value) { + this.dropTargetLocation = value; + this.ListView.Invalidate(); + } + } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { + if (this.dropTargetSubItemIndex != value) { + this.dropTargetSubItemIndex = value; + this.ListView.Invalidate(); + } + } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get or set the color that will be used to provide drop feedback + /// + public Color FeedbackColor { + get { return this.feedbackColor; } + set { this.feedbackColor = value; } + } + private Color feedbackColor; + + /// + /// Get whether the alt key was down during this drop event + /// + public bool IsAltDown { + get { return (this.KeyState & 32) == 32; } + } + + /// + /// Get whether any modifier key was down during this drop event + /// + public bool IsAnyModifierDown { + get { return (this.KeyState & (4 + 8 + 32)) != 0; } + } + + /// + /// Get whether the control key was down during this drop event + /// + public bool IsControlDown { + get { return (this.KeyState & 8) == 8; } + } + + /// + /// Get whether the left mouse button was down during this drop event + /// + public bool IsLeftMouseButtonDown { + get { return (this.KeyState & 1) == 1; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsMiddleMouseButtonDown { + get { return (this.KeyState & 16) == 16; } + } + + /// + /// Get whether the right mouse button was down during this drop event + /// + public bool IsRightMouseButtonDown { + get { return (this.KeyState & 2) == 2; } + } + + /// + /// Get whether the shift key was down during this drop event + /// + public bool IsShiftDown { + get { return (this.KeyState & 4) == 4; } + } + + /// + /// Get or set the state of the keys during this drop event + /// + public int KeyState { + get { return this.keyState; } + set { this.keyState = value; } + } + private int keyState; + + #endregion + + #region Events + + /// + /// Triggered when the sink needs to know if a drop can occur. + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler CanDrop; + + /// + /// Triggered when the drop is made. + /// + public event EventHandler Dropped; + + /// + /// Triggered when the sink needs to know if a drop can occur + /// AND the source is an ObjectListView + /// + /// + /// Handlers should set Effect to indicate what is possible. + /// Handlers can change any of the DropTarget* setttings to change + /// the target of the drop. + /// + public event EventHandler ModelCanDrop; + + /// + /// Triggered when the drop is made. + /// AND the source is an ObjectListView + /// + public event EventHandler ModelDropped; + + #endregion + + #region DropSink Interface + + /// + /// Cleanup the drop sink when the mouse has left the control or + /// the drag has finished. + /// + protected override void Cleanup() { + this.DropTargetLocation = DropTargetLocation.None; + this.ListView.FullRowSelect = this.originalFullRowSelect; + this.Billboard.Text = null; + } + + /// + /// Draw any feedback that is appropriate to the current drop state. + /// + /// + /// Any drawing is done over the top of the ListView. This operation should disturb + /// the Graphic as little as possible. Specifically, do not erase the area into which + /// you draw. + /// + /// A Graphic for drawing + /// The contents bounds of the ListView (not including any header) + public override void DrawFeedback(Graphics g, Rectangle bounds) { + g.SmoothingMode = ObjectListView.SmoothingMode; + + switch (this.DropTargetLocation) { + case DropTargetLocation.Background: + this.DrawFeedbackBackgroundTarget(g, bounds); + break; + case DropTargetLocation.Item: + this.DrawFeedbackItemTarget(g, bounds); + break; + case DropTargetLocation.AboveItem: + this.DrawFeedbackAboveItemTarget(g, bounds); + break; + case DropTargetLocation.BelowItem: + this.DrawFeedbackBelowItemTarget(g, bounds); + break; + } + + if (this.Billboard != null) { + this.Billboard.Draw(this.ListView, g, bounds); + } + } + + /// + /// The user has released the drop over this control + /// + /// + public override void Drop(DragEventArgs args) { + this.TriggerDroppedEvent(args); + this.timer.Stop(); + this.Cleanup(); + } + + /// + /// A drag has entered this control. + /// + /// Implementators should set args.Effect to the appropriate DragDropEffects. + /// + public override void Enter(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Enter"); + + /* + * When FullRowSelect is true, we have two problems: + * 1) GetItemRect(ItemOnly) returns the whole row rather than just the icon/text, which messes + * up our calculation of the drop rectangle. + * 2) during the drag, the Timer events will not fire! This is the major problem, since without + * those events we can't autoscroll. + * + * The first problem we can solve through coding, but the second is more difficult. + * We avoid both problems by turning off FullRowSelect during the drop operation. + */ + this.originalFullRowSelect = this.ListView.FullRowSelect; + this.ListView.FullRowSelect = false; + + // Setup our drop event args block + this.dropEventArgs = new ModelDropEventArgs(); + this.dropEventArgs.DropSink = this; + this.dropEventArgs.ListView = this.ListView; + this.dropEventArgs.DataObject = args.Data; + OLVDataObject olvData = args.Data as OLVDataObject; + if (olvData != null) { + this.dropEventArgs.SourceListView = olvData.ListView; + this.dropEventArgs.SourceModels = olvData.ModelObjects; + } + + this.Over(args); + } + + /// + /// The drag is moving over this control. + /// + /// + public override void Over(DragEventArgs args) { + //System.Diagnostics.Debug.WriteLine("Over"); + this.KeyState = args.KeyState; + Point pt = this.ListView.PointToClient(new Point(args.X, args.Y)); + args.Effect = this.CalculateDropAction(args, pt); + this.CheckScrolling(pt); + } + + #endregion + + #region Events + + /// + /// Trigger the Dropped events + /// + /// + protected virtual void TriggerDroppedEvent(DragEventArgs args) { + this.dropEventArgs.Handled = false; + + // If the source is an ObjectListView, trigger the ModelDropped event + if (this.dropEventArgs.SourceListView != null) + this.OnModelDropped(this.dropEventArgs); + + if (!this.dropEventArgs.Handled) + this.OnDropped(this.dropEventArgs); + } + + /// + /// Trigger CanDrop + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// Trigger Dropped + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// Trigger ModelCanDrop + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != null && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + return; + } + + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// Trigger ModelDropped + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + #endregion + + #region Implementation + + private void timer_Tick(object sender, EventArgs e) { + this.HandleTimerTick(); + } + + /// + /// Handle the timer tick event, which is sent when the listview should + /// scroll + /// + protected virtual void HandleTimerTick() { + + // If the mouse has been released, stop scrolling. + // This is only necessary if the mouse is released outside of the control. + // If the mouse is released inside the control, we would receive a Drop event. + if ((this.IsLeftMouseButtonDown && (Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left) || + (this.IsMiddleMouseButtonDown && (Control.MouseButtons & MouseButtons.Middle) != MouseButtons.Middle) || + (this.IsRightMouseButtonDown && (Control.MouseButtons & MouseButtons.Right) != MouseButtons.Right)) { + this.timer.Stop(); + this.Cleanup(); + return; + } + + // Auto scrolling will continune while the mouse is close to the ListView + const int GRACE_PERIMETER = 30; + + Point pt = this.ListView.PointToClient(Cursor.Position); + Rectangle r2 = this.ListView.ClientRectangle; + r2.Inflate(GRACE_PERIMETER, GRACE_PERIMETER); + if (r2.Contains(pt)) { + this.ListView.LowLevelScroll(0, this.scrollAmount); + } + } + + /// + /// When the mouse is at the given point, what should the target of the drop be? + /// + /// This method should update the DropTarget* members of the given arg block + /// + /// The mouse point, in client co-ordinates + protected virtual void CalculateDropTarget(OlvDropEventArgs args, Point pt) { + const int SMALL_VALUE = 3; + DropTargetLocation location = DropTargetLocation.None; + int targetIndex = -1; + int targetSubIndex = 0; + + if (this.CanDropOnBackground) + location = DropTargetLocation.Background; + + // Which item is the mouse over? + // If it is not over any item, it's over the background. + //ListViewHitTestInfo info = this.ListView.HitTest(pt.X, pt.Y); + OlvListViewHitTestInfo info = this.ListView.OlvHitTest(pt.X, pt.Y); + if (info.Item != null && this.CanDropOnItem) { + location = DropTargetLocation.Item; + targetIndex = info.Item.Index; + if (info.SubItem != null && this.CanDropOnSubItem) + targetSubIndex = info.Item.SubItems.IndexOf(info.SubItem); + } + + // Check to see if the mouse is "between" rows. + // ("between" is somewhat loosely defined) + if (this.CanDropBetween && this.ListView.GetItemCount() > 0) { + + // If the mouse is over an item, check to see if it is near the top or bottom + if (location == DropTargetLocation.Item) { + if (pt.Y - SMALL_VALUE <= info.Item.Bounds.Top) + location = DropTargetLocation.AboveItem; + if (pt.Y + SMALL_VALUE >= info.Item.Bounds.Bottom) + location = DropTargetLocation.BelowItem; + } else { + // Is there an item a little below the mouse? + // If so, we say the drop point is above that row + info = this.ListView.OlvHitTest(pt.X, pt.Y + SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.AboveItem; + } else { + // Is there an item a little above the mouse? + info = this.ListView.OlvHitTest(pt.X, pt.Y - SMALL_VALUE); + if (info.Item != null) { + targetIndex = info.Item.Index; + location = DropTargetLocation.BelowItem; + } + } + } + } + + args.DropTargetLocation = location; + args.DropTargetIndex = targetIndex; + args.DropTargetSubItemIndex = targetSubIndex; + } + + /// + /// What sort of action is possible when the mouse is at the given point? + /// + /// + /// + /// + /// + /// + public virtual DragDropEffects CalculateDropAction(DragEventArgs args, Point pt) { + this.CalculateDropTarget(this.dropEventArgs, pt); + + this.dropEventArgs.MouseLocation = pt; + this.dropEventArgs.InfoMessage = null; + this.dropEventArgs.Handled = false; + + if (this.dropEventArgs.SourceListView != null) { + this.dropEventArgs.TargetModel = this.ListView.GetModelObject(this.dropEventArgs.DropTargetIndex); + this.OnModelCanDrop(this.dropEventArgs); + } + + if (!this.dropEventArgs.Handled) + this.OnCanDrop(this.dropEventArgs); + + this.UpdateAfterCanDropEvent(this.dropEventArgs); + + return this.dropEventArgs.Effect; + } + + /// + /// Based solely on the state of the modifier keys, what drop operation should + /// be used? + /// + /// The drop operation that matches the state of the keys + public DragDropEffects CalculateStandardDropActionFromKeys() { + if (this.IsControlDown) { + if (this.IsShiftDown) + return DragDropEffects.Link; + else + return DragDropEffects.Copy; + } else { + return DragDropEffects.Move; + } + } + + /// + /// Should the listview be made to scroll when the mouse is at the given point? + /// + /// + protected virtual void CheckScrolling(Point pt) { + if (!this.AutoScroll) + return; + + Rectangle r = this.ListView.ContentRectangle; + int rowHeight = this.ListView.RowHeightEffective; + int close = rowHeight; + + // In Tile view, using the whole row height is too much + if (this.ListView.View == View.Tile) + close /= 2; + + if (pt.Y <= (r.Top + close)) { + // Scroll faster if the mouse is closer to the top + this.timer.Interval = ((pt.Y <= (r.Top + close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = -rowHeight; + } else { + if (pt.Y >= (r.Bottom - close)) { + this.timer.Interval = ((pt.Y >= (r.Bottom - close / 2)) ? 100 : 350); + this.timer.Start(); + this.scrollAmount = rowHeight; + } else { + this.timer.Stop(); + } + } + } + + /// + /// Update the state of our sink to reflect the information that + /// may have been written into the drop event args. + /// + /// + protected virtual void UpdateAfterCanDropEvent(OlvDropEventArgs args) { + this.DropTargetIndex = args.DropTargetIndex; + this.DropTargetLocation = args.DropTargetLocation; + this.DropTargetSubItemIndex = args.DropTargetSubItemIndex; + + if (this.Billboard != null) { + Point pt = args.MouseLocation; + pt.Offset(5, 5); + if (this.Billboard.Text != this.dropEventArgs.InfoMessage || this.Billboard.Location != pt) { + this.Billboard.Text = this.dropEventArgs.InfoMessage; + this.Billboard.Location = pt; + this.ListView.Invalidate(); + } + } + } + + #endregion + + #region Rendering + + /// + /// Draw the feedback that shows that the background is the target + /// + /// + /// + protected virtual void DrawFeedbackBackgroundTarget(Graphics g, Rectangle bounds) { + float penWidth = 12.0f; + Rectangle r = bounds; + r.Inflate((int)-penWidth / 2, (int)-penWidth / 2); + using (Pen p = new Pen(Color.FromArgb(128, this.FeedbackColor), penWidth)) { + using (GraphicsPath path = this.GetRoundedRect(r, 30.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows that an item (or a subitem) is the target + /// + /// + /// + /// + /// DropTargetItem and DropTargetSubItemIndex tells what is the target + /// + protected virtual void DrawFeedbackItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + r.Inflate(1, 1); + float diameter = r.Height / 3; + using (GraphicsPath path = this.GetRoundedRect(r, diameter)) { + using (SolidBrush b = new SolidBrush(Color.FromArgb(48, this.FeedbackColor))) { + g.FillPath(b, path); + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawPath(p, path); + } + } + } + + /// + /// Draw the feedback that shows the drop will occur before target + /// + /// + /// + protected virtual void DrawFeedbackAboveItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Top, r.Right, r.Top); + } + + /// + /// Draw the feedback that shows the drop will occur after target + /// + /// + /// + protected virtual void DrawFeedbackBelowItemTarget(Graphics g, Rectangle bounds) { + if (this.DropTargetItem == null) + return; + + Rectangle r = this.CalculateDropTargetRectangle(this.DropTargetItem, this.DropTargetSubItemIndex); + this.DrawBetweenLine(g, r.Left, r.Bottom, r.Right, r.Bottom); + } + + /// + /// Return a GraphicPath that is round corner rectangle. + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + + return path; + } + + /// + /// Calculate the target rectangle when the given item (and possible subitem) + /// is the target of the drop. + /// + /// + /// + /// + protected virtual Rectangle CalculateDropTargetRectangle(OLVListItem item, int subItem) { + if (subItem > 0) + return item.SubItems[subItem].Bounds; + + Rectangle r = this.ListView.CalculateCellTextBounds(item, subItem); + + // Allow for indent + if (item.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width; + r.X += (indentWidth * item.IndentCount); + r.Width -= (indentWidth * item.IndentCount); + } + + return r; + } + + /// + /// Draw a "between items" line at the given co-ordinates + /// + /// + /// + /// + /// + /// + protected virtual void DrawBetweenLine(Graphics g, int x1, int y1, int x2, int y2) { + using (Brush b = new SolidBrush(this.FeedbackColor)) { + int x = x1; + int y = y1; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 5, + x, y - 5); + gp.AddBezier( + x, y - 6, + x + 3, y - 2, + x + 6, y - 1, + x + 11, y); + gp.AddBezier( + x + 11, y, + x + 6, y + 1, + x + 3, y + 2, + x, y + 6); + gp.CloseFigure(); + g.FillPath(b, gp); + } + x = x2; + y = y2; + using (GraphicsPath gp = new GraphicsPath()) { + gp.AddLine( + x, y + 6, + x, y - 6); + gp.AddBezier( + x, y - 7, + x - 3, y - 2, + x - 6, y - 1, + x - 11, y); + gp.AddBezier( + x - 11, y, + x - 6, y + 1, + x - 3, y + 2, + x, y + 7); + gp.CloseFigure(); + g.FillPath(b, gp); + } + } + using (Pen p = new Pen(this.FeedbackColor, 3.0f)) { + g.DrawLine(p, x1, y1, x2, y2); + } + } + + #endregion + + private Timer timer; + private int scrollAmount; + private bool originalFullRowSelect; + private ModelDropEventArgs dropEventArgs; + } + + /// + /// This drop sink allows items within the same list to be rearranged, + /// as well as allowing items to be dropped from other lists. + /// + /// + /// + /// This class can only be used on plain ObjectListViews and FastObjectListViews. + /// The other flavours have no way to implement the insert operation that is required. + /// + /// + /// This class does not work with grouping. + /// + /// + /// This class works when the OLV is sorted, but it is up to the programmer + /// to decide what rearranging such lists "means". Example: if the control is sorting + /// students by academic grade, and the user drags a "Fail" grade student up amonst the "A+" + /// students, it is the responsibility of the programmer to makes the appropriate changes + /// to the model and redraw/rebuild the control so that the users action makes sense. + /// + /// + /// Users of this class should listen for the CanDrop event to decide + /// if models from another OLV can be moved to OLV under this sink. + /// + /// + public class RearrangingDropSink : SimpleDropSink + { + /// + /// Create a RearrangingDropSink + /// + public RearrangingDropSink() { + this.CanDropBetween = true; + this.CanDropOnBackground = true; + this.CanDropOnItem = false; + } + + /// + /// Create a RearrangingDropSink + /// + /// + public RearrangingDropSink(bool acceptDropsFromOtherLists) + : this() { + this.AcceptExternal = acceptDropsFromOtherLists; + } + + /// + /// Trigger OnModelCanDrop + /// + /// + protected override void OnModelCanDrop(ModelDropEventArgs args) { + base.OnModelCanDrop(args); + + if (args.Handled) + return; + + args.Effect = DragDropEffects.Move; + + // Don't allow drops from other list, if that's what's configured + if (!this.AcceptExternal && args.SourceListView != this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + args.InfoMessage = "This list doesn't accept drops from other lists"; + } + + // If we are rearranging a list, don't allow drops on the background + if (args.DropTargetLocation == DropTargetLocation.Background && args.SourceListView == this.ListView) { + args.Effect = DragDropEffects.None; + args.DropTargetLocation = DropTargetLocation.None; + } + } + + /// + /// Trigger OnModelDropped + /// + /// + protected override void OnModelDropped(ModelDropEventArgs args) { + base.OnModelDropped(args); + + if (!args.Handled) + this.RearrangeModels(args); + } + + /// + /// Do the work of processing the dropped items + /// + /// + public virtual void RearrangeModels(ModelDropEventArgs args) { + switch (args.DropTargetLocation) { + case DropTargetLocation.AboveItem: + this.ListView.MoveObjects(args.DropTargetIndex, args.SourceModels); + break; + case DropTargetLocation.BelowItem: + this.ListView.MoveObjects(args.DropTargetIndex + 1, args.SourceModels); + break; + case DropTargetLocation.Background: + this.ListView.AddObjects(args.SourceModels); + break; + default: + return; + } + + if (args.SourceListView != this.ListView) { + args.SourceListView.RemoveObjects(args.SourceModels); + } + } + } + + /// + /// When a drop sink needs to know if something can be dropped, or + /// to notify that a drop has occured, it uses an instance of this class. + /// + public class OlvDropEventArgs : EventArgs + { + /// + /// Create a OlvDropEventArgs + /// + public OlvDropEventArgs() { + } + + #region Data Properties + + /// + /// Get the data object that is being dragged + /// + public object DataObject { + get { return this.dataObject; } + internal set { this.dataObject = value; } + } + private object dataObject; + + /// + /// Get the drop sink that originated this event + /// + public SimpleDropSink DropSink { + get { return this.dropSink; } + internal set { this.dropSink = value; } + } + private SimpleDropSink dropSink; + + /// + /// Get or set the index of the item that is the target of the drop + /// + public int DropTargetIndex { + get { return dropTargetIndex; } + set { this.dropTargetIndex = value; } + } + private int dropTargetIndex = -1; + + /// + /// Get or set the location of the target of the drop + /// + public DropTargetLocation DropTargetLocation { + get { return dropTargetLocation; } + set { this.dropTargetLocation = value; } + } + private DropTargetLocation dropTargetLocation; + + /// + /// Get or set the index of the subitem that is the target of the drop + /// + public int DropTargetSubItemIndex { + get { return dropTargetSubItemIndex; } + set { this.dropTargetSubItemIndex = value; } + } + private int dropTargetSubItemIndex = -1; + + /// + /// Get the item that is the target of the drop + /// + public OLVListItem DropTargetItem { + get { + return this.ListView.GetItem(this.DropTargetIndex); + } + set { + if (value == null) + this.DropTargetIndex = -1; + else + this.DropTargetIndex = value.Index; + } + } + + /// + /// Get or set the drag effect that should be used for this operation + /// + public DragDropEffects Effect { + get { return this.effect; } + set { this.effect = value; } + } + private DragDropEffects effect; + + /// + /// Get or set if this event was handled. No further processing will be done for a handled event. + /// + public bool Handled { + get { return this.handled; } + set { this.handled = value; } + } + private bool handled; + + /// + /// Get or set the feedback message for this operation + /// + /// + /// If this is not null, it will be displayed as a feedback message + /// during the drag. + /// + public string InfoMessage { + get { return this.infoMessage; } + set { this.infoMessage = value; } + } + private string infoMessage; + + /// + /// Get the ObjectListView that is being dropped on + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Get the location of the mouse (in target ListView co-ords) + /// + public Point MouseLocation { + get { return this.mouseLocation; } + internal set { this.mouseLocation = value; } + } + private Point mouseLocation; + + /// + /// Get the drop action indicated solely by the state of the modifier keys + /// + public DragDropEffects StandardDropActionFromKeys { + get { + return this.DropSink.CalculateStandardDropActionFromKeys(); + } + } + + #endregion + } + + /// + /// These events are triggered when the drag source is an ObjectListView. + /// + public class ModelDropEventArgs : OlvDropEventArgs + { + /// + /// Create a ModelDropEventArgs + /// + public ModelDropEventArgs() + { + } + + /// + /// Gets the model objects that are being dragged. + /// + public IList SourceModels { + get { return this.dragModels; } + internal set { + this.dragModels = value; + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + } + } + private IList dragModels; + private ArrayList toBeRefreshed = new ArrayList(); + + /// + /// Gets the ObjectListView that is the source of the dragged objects. + /// + public ObjectListView SourceListView { + get { return this.sourceListView; } + internal set { this.sourceListView = value; } + } + private ObjectListView sourceListView; + + /// + /// Get the model object that is being dropped upon. + /// + /// This is only value for TargetLocation == Item + public object TargetModel { + get { return this.targetModel; } + internal set { this.targetModel = value; } + } + private object targetModel; + + /// + /// Refresh all the objects involved in the operation + /// + public void RefreshObjects() { + TreeListView tlv = this.SourceListView as TreeListView; + if (tlv != null) { + foreach (object model in this.SourceModels) { + object parent = tlv.GetParent(model); + if (!toBeRefreshed.Contains(parent)) + toBeRefreshed.Add(parent); + } + } + toBeRefreshed.AddRange(this.SourceModels); + if (this.ListView == this.SourceListView) { + toBeRefreshed.Add(this.TargetModel); + this.ListView.RefreshObjects(toBeRefreshed); + } else { + this.SourceListView.RefreshObjects(toBeRefreshed); + this.ListView.RefreshObject(this.TargetModel); + } + } + } +} diff --git a/ObjectListView/Implementation/Enums.cs b/ObjectListView/Implementation/Enums.cs new file mode 100644 index 0000000..9a02207 --- /dev/null +++ b/ObjectListView/Implementation/Enums.cs @@ -0,0 +1,99 @@ +/* + * Enums - All enum definitions used in ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace BrightIdeasSoftware { + + public partial class ObjectListView { + /// + /// How does a user indicate that they want to edit cells? + /// + public enum CellEditActivateMode { + /// + /// This list cannot be edited. F2 does nothing. + /// + None = 0, + + /// + /// A single click on a subitem will edit the value. Single clicking the primary column, + /// selects the row just like normal. The user must press F2 to edit the primary column. + /// + SingleClick = 1, + + /// + /// Double clicking a subitem or the primary column will edit that cell. + /// F2 will edit the primary column. + /// + DoubleClick = 2, + + /// + /// Pressing F2 is the only way to edit the cells. Once the primary column is being edited, + /// the other cells in the row can be edited by pressing Tab. + /// + F2Only = 3 + } + + /// + /// These values specify how column selection will be presented to the user + /// + public enum ColumnSelectBehaviour { + /// + /// No column selection will be presented + /// + None, + + /// + /// The columns will be show in the main menu + /// + InlineMenu, + + /// + /// The columns will be shown in a submenu + /// + Submenu, + + /// + /// A model dialog will be presented to allow the user to choose columns + /// + ModelDialog, + + /* + * NonModelDialog is just a little bit tricky since the OLV can change views while the dialog is showing + * So, just comment this out for the time being. + + /// + /// A non-model dialog will be presented to allow the user to choose columns + /// + NonModelDialog + * + */ + } + } +} \ No newline at end of file diff --git a/ObjectListView/Implementation/Events.cs b/ObjectListView/Implementation/Events.cs new file mode 100644 index 0000000..cd9bd0c --- /dev/null +++ b/ObjectListView/Implementation/Events.cs @@ -0,0 +1,2422 @@ +/* + * Events - All the events that can be triggered within an ObjectListView. + * + * Author: Phillip Piper + * Date: 17/10/2008 9:15 PM + * + * Change log: + * v2.8.0 + * 2014-05-20 JPP - Added IsHyperlinkEventArgs.IsHyperlink + * v2.6 + * 2012-04-17 JPP - Added group state change and group expansion events + * v2.5 + * 2010-08-08 JPP - CellEdit validation and finish events now have NewValue property. + * v2.4 + * 2010-03-04 JPP - Added filtering events + * v2.3 + * 2009-08-16 JPP - Added group events + * 2009-08-08 JPP - Added HotItem event + * 2009-07-24 JPP - Added Hyperlink events + * - Added Formatting events + * v2.2.1 + * 2009-06-13 JPP - Added Cell events + * - Moved all event parameter blocks to this file. + * - Added Handled property to AfterSearchEventArgs + * v2.2 + * 2009-06-01 JPP - Added ColumnToGroupBy and GroupByOrder to sorting events + - Gave all event descriptions + * 2009-04-23 JPP - Added drag drop events + * v2.1 + * 2009-01-18 JPP - Moved SelectionChanged event to this file + * v2.0 + * 2008-12-06 JPP - Added searching events + * 2008-12-01 JPP - Added secondary sort information to Before/AfterSorting events + * 2008-10-17 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// The callbacks for CellEditing events + /// + /// this + /// We could replace this with EventHandler<CellEditEventArgs> but that would break all + /// cell editing event code from v1.x. + /// + public delegate void CellEditEventHandler(object sender, CellEditEventArgs e); + + public partial class ObjectListView + { + //----------------------------------------------------------------------------------- + #region Events + + /// + /// Triggered after a ObjectListView has been searched by the user typing into the list + /// + [Category("ObjectListView"), + Description("This event is triggered after the control has done a search-by-typing action.")] + public event EventHandler AfterSearching; + + /// + /// Triggered after a ObjectListView has been sorted + /// + [Category("ObjectListView"), + Description("This event is triggered after the items in the list have been sorted.")] + public event EventHandler AfterSorting; + + /// + /// Triggered before a ObjectListView is searched by the user typing into the list + /// + /// + /// Set Cancelled to true to prevent the searching from taking place. + /// Changing StringToFind or StartSearchFrom will change the subsequent search. + /// + [Category("ObjectListView"), + Description("This event is triggered before the control does a search-by-typing action.")] + public event EventHandler BeforeSearching; + + /// + /// Triggered before a ObjectListView is sorted + /// + /// + /// Set Cancelled to true to prevent the sort from taking place. + /// Changing ColumnToSort or SortOrder will change the subsequent sort. + /// + [Category("ObjectListView"), + Description("This event is triggered before the items in the list are sorted.")] + public event EventHandler BeforeSorting; + + /// + /// Triggered after a ObjectListView has created groups + /// + [Category("ObjectListView"), + Description("This event is triggered after the groups are created.")] + public event EventHandler AfterCreatingGroups; + + /// + /// Triggered before a ObjectListView begins to create groups + /// + /// + /// Set Groups to prevent the default group creation process + /// + [Category("ObjectListView"), + Description("This event is triggered before the groups are created.")] + public event EventHandler BeforeCreatingGroups; + + /// + /// Triggered just before a ObjectListView creates groups + /// + /// + /// You can make changes to the groups, which have been created, before those + /// groups are created within the listview. + /// + [Category("ObjectListView"), + Description("This event is triggered when the groups are just about to be created.")] + public event EventHandler AboutToCreateGroups; + + /// + /// This event is triggered when the user moves a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler. + /// + /// + /// Handlers for this event should set the Effect argument and optionally the + /// InfoMsg property. They can also change any of the DropTarget* setttings to change + /// the target of the drop. + /// + [Category("ObjectListView"), + Description("Can the user drop the currently dragged items at the current mouse location?")] + public event EventHandler CanDrop; + + /// + /// Triggered when a cell is about to finish being edited. + /// + /// If Cancel is already true, the user is cancelling the edit operation. + /// Set Cancel to true to prevent the value from the cell being written into the model. + /// You cannot prevent the editing from finishing within this event -- you need + /// the CellEditValidating event for that. + [Category("ObjectListView"), + Description("This event is triggered cell edit operation is finishing.")] + public event CellEditEventHandler CellEditFinishing; + + /// + /// Triggered when a cell is about to be edited. + /// + /// Set Cancel to true to prevent the cell being edited. + /// You can change the the Control to be something completely different. + [Category("ObjectListView"), + Description("This event is triggered when cell edit is about to begin.")] + public event CellEditEventHandler CellEditStarting; + + /// + /// Triggered when a cell editor needs to be validated + /// + /// + /// If this event is cancelled, focus will remain on the cell editor. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell editor is about to lose focus and its new contents need to be validated.")] + public event CellEditEventHandler CellEditValidating; + + /// + /// Triggered when a cell is left clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user left clicks a cell.")] + public event EventHandler CellClick; + + /// + /// Triggered when the mouse is above a cell. + /// + [Category("ObjectListView"), + Description("This event is triggered when the mouse is over a cell.")] + public event EventHandler CellOver; + + /// + /// Triggered when a cell is right clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user right clicks a cell.")] + public event EventHandler CellRightClick; + + /// + /// This event is triggered when a cell needs a tool tip. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell needs a tool tip.")] + public event EventHandler CellToolTipShowing; + + /// + /// This event is triggered when a checkbox is checked/unchecked on a subitem + /// + [Category("ObjectListView"), + Description("This event is triggered when a checkbox is checked/unchecked on a subitem.")] + public event EventHandler SubItemChecking; + + /// + /// Triggered when a column header is right clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user right clicks a column header.")] + public event ColumnRightClickEventHandler ColumnRightClick; + + /// + /// This event is triggered when the user releases a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler. + /// + [Category("ObjectListView"), + Description("This event is triggered when the user dropped items onto the control.")] + public event EventHandler Dropped; + + /// + /// This event is triggered when the control needs to filter its collection of objects. + /// + [Category("ObjectListView"), + Description("This event is triggered when the control needs to filter its collection of objects.")] + public event EventHandler Filter; + + /// + /// This event is triggered when a cell needs to be formatted. + /// + [Category("ObjectListView"), + Description("This event is triggered when a cell needs to be formatted.")] + public event EventHandler FormatCell; + + /// + /// This event is triggered when the frozeness of the control changes. + /// + [Category("ObjectListView"), + Description("This event is triggered when frozeness of the control changes.")] + public event EventHandler Freezing; + + /// + /// This event is triggered when a row needs to be formatted. + /// + [Category("ObjectListView"), + Description("This event is triggered when a row needs to be formatted.")] + public event EventHandler FormatRow; + + /// + /// This event is triggered when a group is about to collapse or expand. + /// This can be cancelled to prevent the expansion. + /// + [Category("ObjectListView"), + Description("This event is triggered when a group is about to collapse or expand.")] + public event EventHandler GroupExpandingCollapsing; + + /// + /// This event is triggered when a group changes state. + /// + [Category("ObjectListView"), + Description("This event is triggered when a group changes state.")] + public event EventHandler GroupStateChanged; + + /// + /// This event is triggered when a header checkbox is changing value + /// + [Category("ObjectListView"), + Description("This event is triggered when a header checkbox changes value.")] + public event EventHandler HeaderCheckBoxChanging; + + /// + /// This event is triggered when a header needs a tool tip. + /// + [Category("ObjectListView"), + Description("This event is triggered when a header needs a tool tip.")] + public event EventHandler HeaderToolTipShowing; + + /// + /// Triggered when the "hot" item changes + /// + [Category("ObjectListView"), + Description("This event is triggered when the hot item changed.")] + public event EventHandler HotItemChanged; + + /// + /// Triggered when a hyperlink cell is clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when a hyperlink cell is clicked.")] + public event EventHandler HyperlinkClicked; + + /// + /// Triggered when the task text of a group is clicked. + /// + [Category("ObjectListView"), + Description("This event is triggered when the task text of a group is clicked.")] + public event EventHandler GroupTaskClicked; + + /// + /// Is the value in the given cell a hyperlink. + /// + [Category("ObjectListView"), + Description("This event is triggered when the control needs to know if a given cell contains a hyperlink.")] + public event EventHandler IsHyperlink; + + /// + /// Some new objects are about to be added to an ObjectListView. + /// + [Category("ObjectListView"), + Description("This event is triggered when objects are about to be added to the control")] + public event EventHandler ItemsAdding; + + /// + /// The contents of the ObjectListView has changed. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the control have changed.")] + public event EventHandler ItemsChanged; + + /// + /// The contents of the ObjectListView is about to change via a SetObjects call + /// + /// + /// Set Cancelled to true to prevent the contents of the list changing. This does not work with virtual lists. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the control changes.")] + public event EventHandler ItemsChanging; + + /// + /// Some objects are about to be removed from an ObjectListView. + /// + [Category("ObjectListView"), + Description("This event is triggered when objects are removed from the control.")] + public event EventHandler ItemsRemoving; + + /// + /// This event is triggered when the user moves a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler, and when the source control + /// for the drag was an ObjectListView. + /// + /// + /// Handlers for this event should set the Effect argument and optionally the + /// InfoMsg property. They can also change any of the DropTarget* setttings to change + /// the target of the drop. + /// + [Category("ObjectListView"), + Description("Can the dragged collection of model objects be dropped at the current mouse location")] + public event EventHandler ModelCanDrop; + + /// + /// This event is triggered when the user releases a drag over an ObjectListView that + /// has a SimpleDropSink installed as the drop handler and when the source control + /// for the drag was an ObjectListView. + /// + [Category("ObjectListView"), + Description("A collection of model objects from a ObjectListView has been dropped on this control")] + public event EventHandler ModelDropped; + + /// + /// This event is triggered once per user action that changes the selection state + /// of one or more rows. + /// + [Category("ObjectListView"), + Description("This event is triggered once per user action that changes the selection state of one or more rows.")] + public event EventHandler SelectionChanged; + + /// + /// This event is triggered when the contents of the ObjectListView has scrolled. + /// + [Category("ObjectListView"), + Description("This event is triggered when the contents of the ObjectListView has scrolled.")] + public event EventHandler Scroll; + + #endregion + + //----------------------------------------------------------------------------------- + #region OnEvents + + /// + /// + /// + /// + protected virtual void OnAboutToCreateGroups(CreateGroupsEventArgs e) { + if (this.AboutToCreateGroups != null) + this.AboutToCreateGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeCreatingGroups(CreateGroupsEventArgs e) { + if (this.BeforeCreatingGroups != null) + this.BeforeCreatingGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterCreatingGroups(CreateGroupsEventArgs e) { + if (this.AfterCreatingGroups != null) + this.AfterCreatingGroups(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterSearching(AfterSearchingEventArgs e) { + if (this.AfterSearching != null) + this.AfterSearching(this, e); + } + + /// + /// + /// + /// + protected virtual void OnAfterSorting(AfterSortingEventArgs e) { + if (this.AfterSorting != null) + this.AfterSorting(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeSearching(BeforeSearchingEventArgs e) { + if (this.BeforeSearching != null) + this.BeforeSearching(this, e); + } + + /// + /// + /// + /// + protected virtual void OnBeforeSorting(BeforeSortingEventArgs e) { + if (this.BeforeSorting != null) + this.BeforeSorting(this, e); + } + + /// + /// + /// + /// + protected virtual void OnCanDrop(OlvDropEventArgs args) { + if (this.CanDrop != null) + this.CanDrop(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellClick(CellClickEventArgs args) { + if (this.CellClick != null) + this.CellClick(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellOver(CellOverEventArgs args) { + if (this.CellOver != null) + this.CellOver(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellRightClick(CellRightClickEventArgs args) { + if (this.CellRightClick != null) + this.CellRightClick(this, args); + } + + /// + /// + /// + /// + protected virtual void OnCellToolTip(ToolTipShowingEventArgs args) { + if (this.CellToolTipShowing != null) + this.CellToolTipShowing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnSubItemChecking(SubItemCheckingEventArgs args) { + if (this.SubItemChecking != null) + this.SubItemChecking(this, args); + } + + /// + /// + /// + /// + protected virtual void OnColumnRightClick(ColumnClickEventArgs e) { + if (this.ColumnRightClick != null) + this.ColumnRightClick(this, e); + } + + /// + /// + /// + /// + protected virtual void OnDropped(OlvDropEventArgs args) { + if (this.Dropped != null) + this.Dropped(this, args); + } + + /// + /// + /// + /// + protected virtual void OnFilter(FilterEventArgs e) { + if (this.Filter != null) + this.Filter(this, e); + } + + /// + /// + /// + /// + protected virtual void OnFormatCell(FormatCellEventArgs args) { + if (this.FormatCell != null) + this.FormatCell(this, args); + } + + /// + /// + /// + /// + protected virtual void OnFormatRow(FormatRowEventArgs args) { + if (this.FormatRow != null) + this.FormatRow(this, args); + } + + /// + /// + /// + /// + protected virtual void OnFreezing(FreezeEventArgs args) { + if (this.Freezing != null) + this.Freezing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnGroupExpandingCollapsing(GroupExpandingCollapsingEventArgs args) + { + if (this.GroupExpandingCollapsing != null) + this.GroupExpandingCollapsing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnGroupStateChanged(GroupStateChangedEventArgs args) + { + if (this.GroupStateChanged != null) + this.GroupStateChanged(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHeaderCheckBoxChanging(HeaderCheckBoxChangingEventArgs args) + { + if (this.HeaderCheckBoxChanging != null) + this.HeaderCheckBoxChanging(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHeaderToolTip(ToolTipShowingEventArgs args) + { + if (this.HeaderToolTipShowing != null) + this.HeaderToolTipShowing(this, args); + } + + /// + /// + /// + /// + protected virtual void OnHotItemChanged(HotItemChangedEventArgs e) { + if (this.HotItemChanged != null) + this.HotItemChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnHyperlinkClicked(HyperlinkClickedEventArgs e) { + if (this.HyperlinkClicked != null) + this.HyperlinkClicked(this, e); + } + + /// + /// + /// + /// + protected virtual void OnGroupTaskClicked(GroupTaskClickedEventArgs e) { + if (this.GroupTaskClicked != null) + this.GroupTaskClicked(this, e); + } + + /// + /// + /// + /// + protected virtual void OnIsHyperlink(IsHyperlinkEventArgs e) { + if (this.IsHyperlink != null) + this.IsHyperlink(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsAdding(ItemsAddingEventArgs e) { + if (this.ItemsAdding != null) + this.ItemsAdding(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsChanged(ItemsChangedEventArgs e) { + if (this.ItemsChanged != null) + this.ItemsChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsChanging(ItemsChangingEventArgs e) { + if (this.ItemsChanging != null) + this.ItemsChanging(this, e); + } + + /// + /// + /// + /// + protected virtual void OnItemsRemoving(ItemsRemovingEventArgs e) { + if (this.ItemsRemoving != null) + this.ItemsRemoving(this, e); + } + + /// + /// + /// + /// + protected virtual void OnModelCanDrop(ModelDropEventArgs args) { + if (this.ModelCanDrop != null) + this.ModelCanDrop(this, args); + } + + /// + /// + /// + /// + protected virtual void OnModelDropped(ModelDropEventArgs args) { + if (this.ModelDropped != null) + this.ModelDropped(this, args); + } + + /// + /// + /// + /// + protected virtual void OnSelectionChanged(EventArgs e) { + if (this.SelectionChanged != null) + this.SelectionChanged(this, e); + } + + /// + /// + /// + /// + protected virtual void OnScroll(ScrollEventArgs e) { + if (this.Scroll != null) + this.Scroll(this, e); + } + + + /// + /// Tell the world when a cell is about to be edited. + /// + protected virtual void OnCellEditStarting(CellEditEventArgs e) { + if (this.CellEditStarting != null) + this.CellEditStarting(this, e); + } + + /// + /// Tell the world when a cell is about to finish being edited. + /// + protected virtual void OnCellEditorValidating(CellEditEventArgs e) { + // Hack. ListView is an imperfect control container. It does not manage validation + // perfectly. If the ListView is part of a TabControl, and the cell editor loses + // focus by the user clicking on another tab, the TabControl processes the click + // and switches tabs, even if this Validating event cancels. This results in the + // strange situation where the cell editor is active, but isn't visible. When the + // user switches back to the tab with the ListView, composite controls like spin + // controls, DateTimePicker and ComboBoxes do not work properly. Specifically, + // keyboard input still works fine, but the controls do not respond to mouse + // input. SO, if the validation fails, we have to specifically give focus back to + // the cell editor. (this is the Select() call in the code below). + // But (there is always a 'but'), doing that changes the focus so the cell editor + // triggers another Validating event -- which fails again. From the user's point + // of view, they click away from the cell editor, and the validating code + // complains twice. So we only trigger a Validating event if more than 0.1 seconds + // has elapsed since the last validate event. + // I know it's a hack. I'm very open to hear a neater solution. + + // Also, this timed response stops us from sending a series of validation events + // if the user clicks and holds on the OLV scroll bar. + //System.Diagnostics.Debug.WriteLine(Environment.TickCount - lastValidatingEvent); + if ((Environment.TickCount - lastValidatingEvent) < 100) { + e.Cancel = true; + } else { + lastValidatingEvent = Environment.TickCount; + if (this.CellEditValidating != null) + this.CellEditValidating(this, e); + } + lastValidatingEvent = Environment.TickCount; + } + private int lastValidatingEvent = 0; + + /// + /// Tell the world when a cell is about to finish being edited. + /// + protected virtual void OnCellEditFinishing(CellEditEventArgs e) { + if (this.CellEditFinishing != null) + this.CellEditFinishing(this, e); + } + + #endregion + } + + public partial class TreeListView + { + + #region Events + + /// + /// This event is triggered when user input requests the expansion of a list item. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch is about to expand.")] + public event EventHandler Expanding; + + /// + /// This event is triggered when user input requests the collapse of a list item. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch is about to collapsed.")] + public event EventHandler Collapsing; + + /// + /// This event is triggered after the expansion of a list item due to user input. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch has been expanded.")] + public event EventHandler Expanded; + + /// + /// This event is triggered after the collapse of a list item due to user input. + /// + [Category("ObjectListView"), + Description("This event is triggered when a branch has been collapsed.")] + public event EventHandler Collapsed; + + #endregion + + #region OnEvents + + /// + /// Trigger the expanding event + /// + /// + protected virtual void OnExpanding(TreeBranchExpandingEventArgs e) { + if (this.Expanding != null) + this.Expanding(this, e); + } + + /// + /// Trigger the collapsing event + /// + /// + protected virtual void OnCollapsing(TreeBranchCollapsingEventArgs e) { + if (this.Collapsing != null) + this.Collapsing(this, e); + } + + /// + /// Trigger the expanded event + /// + /// + protected virtual void OnExpanded(TreeBranchExpandedEventArgs e) { + if (this.Expanded != null) + this.Expanded(this, e); + } + + /// + /// Trigger the collapsed event + /// + /// + protected virtual void OnCollapsed(TreeBranchCollapsedEventArgs e) { + if (this.Collapsed != null) + this.Collapsed(this, e); + } + + #endregion + } + + //----------------------------------------------------------------------------------- + #region Event Parameter Blocks + + /// + /// Let the world know that a cell edit operation is beginning or ending + /// + public class CellEditEventArgs : EventArgs + { + /// + /// Create an event args + /// + /// + /// + /// + /// + /// + public CellEditEventArgs(OLVColumn column, Control control, Rectangle r, OLVListItem item, int subItemIndex) { + this.Control = control; + this.column = column; + this.cellBounds = r; + this.listViewItem = item; + this.rowObject = item.RowObject; + this.subItemIndex = subItemIndex; + this.value = column.GetValue(item.RowObject); + } + + /// + /// Change this to true to cancel the cell editing operation. + /// + /// + /// During the CellEditStarting event, setting this to true will prevent the cell from being edited. + /// During the CellEditFinishing event, if this value is already true, this indicates that the user has + /// cancelled the edit operation and that the handler should perform cleanup only. Setting this to true, + /// will prevent the ObjectListView from trying to write the new value into the model object. + /// + public bool Cancel; + + /// + /// During the CellEditStarting event, this can be modified to be the control that you want + /// to edit the value. You must fully configure the control before returning from the event, + /// including its bounds and the value it is showing. + /// During the CellEditFinishing event, you can use this to get the value that the user + /// entered and commit that value to the model. Changing the control during the finishing + /// event has no effect. + /// + public Control Control; + + /// + /// The column of the cell that is going to be or has been edited. + /// + public OLVColumn Column { + get { return this.column; } + } + private OLVColumn column; + + /// + /// The model object of the row of the cell that is going to be or has been edited. + /// + public Object RowObject { + get { return this.rowObject; } + } + private Object rowObject; + + /// + /// The listview item of the cell that is going to be or has been edited. + /// + public OLVListItem ListViewItem { + get { return this.listViewItem; } + } + private OLVListItem listViewItem; + + /// + /// The data value of the cell as it stands in the control. + /// + /// Only validate during Validating and Finishing events. + public Object NewValue { + get { return this.newValue; } + set { this.newValue = value; } + } + private Object newValue; + + /// + /// The index of the cell that is going to be or has been edited. + /// + public int SubItemIndex { + get { return this.subItemIndex; } + } + private int subItemIndex; + + /// + /// The data value of the cell before the edit operation began. + /// + public Object Value { + get { return this.value; } + } + private Object value; + + /// + /// The bounds of the cell that is going to be or has been edited. + /// + public Rectangle CellBounds { + get { return this.cellBounds; } + } + private Rectangle cellBounds; + + /// + /// Gets or sets whether the control used for editing should be auto matically disposed + /// when the cell edit operation finishes. Defaults to true + /// + /// If the control is expensive to create, you might want to cache it and reuse for + /// for various cells. If so, you don't want ObjectListView to dispose of the control automatically + public bool AutoDispose { + get { return autoDispose; } + set { autoDispose = value; } + } + private bool autoDispose = true; + } + + /// + /// Event blocks for events that can be cancelled + /// + public class CancellableEventArgs : EventArgs + { + /// + /// Has this event been cancelled by the event handler? + /// + public bool Canceled; + } + + /// + /// BeforeSorting + /// + public class BeforeSortingEventArgs : CancellableEventArgs + { + /// + /// Create BeforeSortingEventArgs + /// + /// + /// + /// + /// + public BeforeSortingEventArgs(OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.ColumnToGroupBy = column; + this.GroupByOrder = order; + this.ColumnToSort = column; + this.SortOrder = order; + this.SecondaryColumnToSort = column2; + this.SecondarySortOrder = order2; + } + + /// + /// Create BeforeSortingEventArgs + /// + /// + /// + /// + /// + /// + /// + public BeforeSortingEventArgs(OLVColumn groupColumn, SortOrder groupOrder, OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.ColumnToGroupBy = groupColumn; + this.GroupByOrder = groupOrder; + this.ColumnToSort = column; + this.SortOrder = order; + this.SecondaryColumnToSort = column2; + this.SecondarySortOrder = order2; + } + + /// + /// Did the event handler already do the sorting for us? + /// + public bool Handled; + + /// + /// What column will be used for grouping + /// + public OLVColumn ColumnToGroupBy; + + /// + /// How will groups be ordered + /// + public SortOrder GroupByOrder; + + /// + /// What column will be used for sorting + /// + public OLVColumn ColumnToSort; + + /// + /// What order will be used for sorting. None means no sorting. + /// + public SortOrder SortOrder; + + /// + /// What column will be used for secondary sorting? + /// + public OLVColumn SecondaryColumnToSort; + + /// + /// What order will be used for secondary sorting? + /// + public SortOrder SecondarySortOrder; + } + + /// + /// Sorting has just occurred. + /// + public class AfterSortingEventArgs : EventArgs + { + /// + /// Create a AfterSortingEventArgs + /// + /// + /// + /// + /// + /// + /// + public AfterSortingEventArgs(OLVColumn groupColumn, SortOrder groupOrder, OLVColumn column, SortOrder order, OLVColumn column2, SortOrder order2) { + this.columnToGroupBy = groupColumn; + this.groupByOrder = groupOrder; + this.columnToSort = column; + this.sortOrder = order; + this.secondaryColumnToSort = column2; + this.secondarySortOrder = order2; + } + + /// + /// Create a AfterSortingEventArgs + /// + /// + public AfterSortingEventArgs(BeforeSortingEventArgs args) { + this.columnToGroupBy = args.ColumnToGroupBy; + this.groupByOrder = args.GroupByOrder; + this.columnToSort = args.ColumnToSort; + this.sortOrder = args.SortOrder; + this.secondaryColumnToSort = args.SecondaryColumnToSort; + this.secondarySortOrder = args.SecondarySortOrder; + } + + /// + /// What column was used for grouping? + /// + public OLVColumn ColumnToGroupBy { + get { return columnToGroupBy; } + } + private OLVColumn columnToGroupBy; + + /// + /// What ordering was used for grouping? + /// + public SortOrder GroupByOrder { + get { return groupByOrder; } + } + private SortOrder groupByOrder; + + /// + /// What column was used for sorting? + /// + public OLVColumn ColumnToSort { + get { return columnToSort; } + } + private OLVColumn columnToSort; + + /// + /// What ordering was used for sorting? + /// + public SortOrder SortOrder { + get { return sortOrder; } + } + private SortOrder sortOrder; + + /// + /// What column was used for secondary sorting? + /// + public OLVColumn SecondaryColumnToSort { + get { return secondaryColumnToSort; } + } + private OLVColumn secondaryColumnToSort; + + /// + /// What order was used for secondary sorting? + /// + public SortOrder SecondarySortOrder { + get { return secondarySortOrder; } + } + private SortOrder secondarySortOrder; + } + + /// + /// This event is triggered when the contents of a list have changed + /// and we want the world to have a chance to filter the list. + /// + public class FilterEventArgs : EventArgs + { + /// + /// Create a FilterEventArgs + /// + /// + public FilterEventArgs(IEnumerable objects) { + this.Objects = objects; + } + + /// + /// Gets or sets what objects are being filtered + /// + public IEnumerable Objects; + + /// + /// Gets or sets what objects survived the filtering + /// + public IEnumerable FilteredObjects; + } + + /// + /// This event is triggered after the items in the list have been changed, + /// either through SetObjects, AddObjects or RemoveObjects. + /// + public class ItemsChangedEventArgs : EventArgs + { + /// + /// Create a ItemsChangedEventArgs + /// + public ItemsChangedEventArgs() { + } + + /// + /// Constructor for this event when used by a virtual list + /// + /// + /// + public ItemsChangedEventArgs(int oldObjectCount, int newObjectCount) { + this.oldObjectCount = oldObjectCount; + this.newObjectCount = newObjectCount; + } + + /// + /// Gets how many items were in the list before it changed + /// + public int OldObjectCount { + get { return oldObjectCount; } + } + private int oldObjectCount; + + /// + /// Gets how many objects are in the list after the change. + /// + public int NewObjectCount { + get { return newObjectCount; } + } + private int newObjectCount; + } + + /// + /// This event is triggered by AddObjects before any change has been made to the list. + /// + public class ItemsAddingEventArgs : CancellableEventArgs + { + /// + /// Create an ItemsAddingEventArgs + /// + /// + public ItemsAddingEventArgs(ICollection objectsToAdd) { + this.ObjectsToAdd = objectsToAdd; + } + + /// + /// Gets or sets the objects to be added to the list + /// + public ICollection ObjectsToAdd; + } + + /// + /// This event is triggered by SetObjects before any change has been made to the list. + /// + /// + /// When used with a virtual list, OldObjects will always be null. + /// + public class ItemsChangingEventArgs : CancellableEventArgs + { + /// + /// Create ItemsChangingEventArgs + /// + /// + /// + public ItemsChangingEventArgs(IEnumerable oldObjects, IEnumerable newObjects) { + this.oldObjects = oldObjects; + this.NewObjects = newObjects; + } + + /// + /// Gets the objects that were in the list before it change. + /// For virtual lists, this will always be null. + /// + public IEnumerable OldObjects { + get { return oldObjects; } + } + private IEnumerable oldObjects; + + /// + /// Gets or sets the objects that will be in the list after it changes. + /// + public IEnumerable NewObjects; + } + + /// + /// This event is triggered by RemoveObjects before any change has been made to the list. + /// + public class ItemsRemovingEventArgs : CancellableEventArgs + { + /// + /// Create an ItemsRemovingEventArgs + /// + /// + public ItemsRemovingEventArgs(ICollection objectsToRemove) { + this.ObjectsToRemove = objectsToRemove; + } + + /// + /// Gets or sets the objects that will be removed + /// + public ICollection ObjectsToRemove; + } + + /// + /// Triggered after the user types into a list + /// + public class AfterSearchingEventArgs : EventArgs + { + /// + /// Create an AfterSearchingEventArgs + /// + /// + /// + public AfterSearchingEventArgs(string stringToFind, int indexSelected) { + this.stringToFind = stringToFind; + this.indexSelected = indexSelected; + } + + /// + /// Gets the string that was actually searched for + /// + public string StringToFind { + get { return this.stringToFind; } + } + private string stringToFind; + + /// + /// Gets or sets whether an the event handler already handled this event + /// + public bool Handled; + + /// + /// Gets the index of the row that was selected by the search. + /// -1 means that no row was matched + /// + public int IndexSelected { + get { return this.indexSelected; } + } + private int indexSelected; + } + + /// + /// Triggered when the user types into a list + /// + public class BeforeSearchingEventArgs : CancellableEventArgs + { + /// + /// Create BeforeSearchingEventArgs + /// + /// + /// + public BeforeSearchingEventArgs(string stringToFind, int startSearchFrom) { + this.StringToFind = stringToFind; + this.StartSearchFrom = startSearchFrom; + } + + /// + /// Gets or sets the string that will be found by the search routine + /// + /// Modifying this value does not modify the memory of what the user has typed. + /// When the user next presses a character, the search string will revert to what + /// the user has actually typed. + public string StringToFind; + + /// + /// Gets or sets the index of the first row that will be considered to matching. + /// + public int StartSearchFrom; + } + + /// + /// The parameter block when telling the world about a cell based event + /// + public class CellEventArgs : EventArgs + { + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + /// This is null for events triggered by the header. + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + private object model; + + /// + /// Gets the row index of the cell + /// + /// This is -1 for events triggered by the header. + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + private int rowIndex = -1; + + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + private OLVColumn column; + + /// + /// Gets the location of the mouse at the time of the event + /// + public Point Location { + get { return this.location; } + internal set { this.location = value; } + } + private Point location; + + /// + /// Gets the state of the modifier keys at the time of the event + /// + public Keys ModifierKeys { + get { return this.modifierKeys; } + internal set { this.modifierKeys = value; } + } + private Keys modifierKeys; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + private OLVListItem item; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view and + /// for event triggered by the header + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + private OLVListSubItem subItem; + + /// + /// Gets the HitTest object that determined which cell was hit + /// + public OlvListViewHitTestInfo HitTest { + get { return hitTest; } + internal set { hitTest = value; } + } + private OlvListViewHitTestInfo hitTest; + + /// + /// Gets or set if this event completelely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled; + } + + /// + /// Tells the world that a cell was clicked + /// + public class CellClickEventArgs : CellEventArgs + { + /// + /// Gets or sets the number of clicks associated with this event + /// + public int ClickCount { + get { return this.clickCount; } + set { this.clickCount = value; } + } + private int clickCount; + } + + /// + /// Tells the world that a cell was right clicked + /// + public class CellRightClickEventArgs : CellEventArgs + { + /// + /// Gets or sets the menu that should be displayed as a result of this event. + /// + /// The menu will be positioned at Location, so changing that property changes + /// where the menu will be displayed. + public ContextMenuStrip MenuStrip; + } + + /// + /// Tell the world that the mouse is over a given cell + /// + public class CellOverEventArgs : CellEventArgs + { + } + + /// + /// Tells the world that the frozen-ness of the ObjectListView has changed. + /// + public class FreezeEventArgs : EventArgs + { + /// + /// Make a FreezeEventArgs + /// + /// + public FreezeEventArgs(int freeze) { + this.FreezeLevel = freeze; + } + + /// + /// How frozen is the control? 0 means that the control is unfrozen, + /// more than 0 indicates froze. + /// + public int FreezeLevel { + get { return this.freezeLevel; } + set { this.freezeLevel = value; } + } + private int freezeLevel; + } + + /// + /// The parameter block when telling the world that a tool tip is about to be shown. + /// + public class ToolTipShowingEventArgs : CellEventArgs + { + /// + /// Gets the tooltip control that is triggering the tooltip event + /// + public ToolTipControl ToolTipControl { + get { return this.toolTipControl; } + internal set { this.toolTipControl = value; } + } + private ToolTipControl toolTipControl; + + /// + /// Gets or sets the text should be shown on the tooltip for this event + /// + /// Setting this to empty or null prevents any tooltip from showing + public string Text; + + /// + /// In what direction should the text for this tooltip be drawn? + /// + public RightToLeft RightToLeft; + + /// + /// Should the tooltip for this event been shown in bubble style? + /// + /// This doesn't work reliable under Vista + public bool? IsBalloon; + + /// + /// What color should be used for the background of the tooltip + /// + /// Setting this does nothing under Vista + public Color? BackColor; + + /// + /// What color should be used for the foreground of the tooltip + /// + /// Setting this does nothing under Vista + public Color? ForeColor; + + /// + /// What string should be used as the title for the tooltip for this event? + /// + public string Title; + + /// + /// Which standard icon should be used for the tooltip for this event + /// + public ToolTipControl.StandardIcons? StandardIcon; + + /// + /// How many milliseconds should the tooltip remain before it automatically + /// disappears. + /// + public int? AutoPopDelay; + + /// + /// What font should be used to draw the text of the tooltip? + /// + public Font Font; + } + + /// + /// Common information to all hyperlink events + /// + public class HyperlinkEventArgs : EventArgs + { + //TODO: Unified with CellEventArgs + + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + private object model; + + /// + /// Gets the row index of the cell + /// + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + private int rowIndex = -1; + + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + private OLVColumn column; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + private OLVListItem item; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + private OLVListSubItem subItem; + + /// + /// Gets the ObjectListView that is the source of the event + /// + public string Url { + get { return this.url; } + internal set { this.url = value; } + } + private string url; + + /// + /// Gets or set if this event completelely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled + { + get { return handled; } + set { handled = value; } + } + private bool handled; + + } + + /// + /// + /// + public class IsHyperlinkEventArgs : EventArgs + { + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.model; } + internal set { this.model = value; } + } + private object model; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + private OLVColumn column; + + /// + /// Gets the text of the cell + /// + public string Text + { + get { return this.text; } + internal set { this.text = value; } + } + private string text; + + /// + /// Gets or sets whether or not this cell is a hyperlink. + /// Defaults to true for enabled rows and false for disabled rows. + /// + public bool IsHyperlink + { + get { return this.isHyperlink; } + set { this.isHyperlink = value; } + } + private bool isHyperlink; + + /// + /// Gets or sets the url that should be invoked when this cell is clicked. + /// + /// Setting this to None or String.Empty means that this cell is not a hyperlink + public string Url; + } + + /// + /// + public class FormatRowEventArgs : EventArgs + { + //TODO: Unified with CellEventArgs + + /// + /// Gets the ObjectListView that is the source of the event + /// + public ObjectListView ListView { + get { return this.listView; } + internal set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the item of the cell + /// + public OLVListItem Item { + get { return item; } + internal set { this.item = value; } + } + private OLVListItem item; + + /// + /// Gets the model object under the cell + /// + public object Model { + get { return this.Item.RowObject; } + } + + /// + /// Gets the row index of the cell + /// + public int RowIndex { + get { return this.rowIndex; } + internal set { this.rowIndex = value; } + } + private int rowIndex = -1; + + /// + /// Gets the display index of the row + /// + public int DisplayIndex { + get { return this.displayIndex; } + internal set { this.displayIndex = value; } + } + private int displayIndex = -1; + + /// + /// Should events be triggered for each cell in this row? + /// + public bool UseCellFormatEvents + { + get { return useCellFormatEvents; } + set { useCellFormatEvents = value; } + } + private bool useCellFormatEvents; + } + + /// + /// Parameter block for FormatCellEvent + /// + public class FormatCellEventArgs : FormatRowEventArgs + { + /// + /// Gets the column index of the cell + /// + /// This is -1 when the view is not in details view. + public int ColumnIndex { + get { return this.columnIndex; } + internal set { this.columnIndex = value; } + } + private int columnIndex = -1; + + /// + /// Gets the column of the cell + /// + /// This is null when the view is not in details view. + public OLVColumn Column { + get { return this.column; } + internal set { this.column = value; } + } + private OLVColumn column; + + /// + /// Gets the subitem of the cell + /// + /// This is null when the view is not in details view + public OLVListSubItem SubItem { + get { return subItem; } + internal set { this.subItem = value; } + } + private OLVListSubItem subItem; + + /// + /// Gets the model value that is being displayed by the cell. + /// + /// This is null when the view is not in details view + public object CellValue { + get { return this.SubItem == null ? null : this.SubItem.ModelValue; } + } + } + + /// + /// The event args when a hyperlink is clicked + /// + public class HyperlinkClickedEventArgs : CellEventArgs + { + /// + /// Gets the url that was associated with this cell. + /// + public string Url + { + get { return url; } + set { url = value; } + } + private string url; + + } + + /// + /// The event args when the check box in a column header is changing + /// + public class HeaderCheckBoxChangingEventArgs : CancelEventArgs { + + /// + /// Get the column whose checkbox is changing + /// + public OLVColumn Column { + get { return column; } + internal set { column = value; } + } + private OLVColumn column; + + /// + /// Get or set the new state that should be used by the column + /// + public CheckState NewCheckState { + get { return newCheckState; } + set { newCheckState = value; } + } + private CheckState newCheckState; + } + + /// + /// The event args when the hot item changed + /// + public class HotItemChangedEventArgs : EventArgs + { + /// + /// Gets or set if this event completelely handled. If it was, no further processing + /// will be done for it. + /// + public bool Handled + { + get { return handled; } + set { handled = value; } + } + private bool handled; + + /// + /// Gets the part of the cell that the mouse is over + /// + public HitTestLocation HotCellHitLocation { + get { return newHotCellHitLocation; } + internal set { newHotCellHitLocation = value; } + } + private HitTestLocation newHotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse is currently over + /// + public virtual HitTestLocationEx HotCellHitLocationEx + { + get { return this.hotCellHitLocationEx; } + internal set { this.hotCellHitLocationEx = value; } + } + private HitTestLocationEx hotCellHitLocationEx; + + /// + /// Gets the index of the column that the mouse is over + /// + /// In non-details view, this will always be 0. + public int HotColumnIndex { + get { return newHotColumnIndex; } + internal set { newHotColumnIndex = value; } + } + private int newHotColumnIndex; + + /// + /// Gets the index of the row that the mouse is over + /// + public int HotRowIndex { + get { return newHotRowIndex; } + internal set { newHotRowIndex = value; } + } + private int newHotRowIndex; + + /// + /// Gets the group that the mouse is over + /// + public OLVGroup HotGroup + { + get { return hotGroup; } + internal set { hotGroup = value; } + } + private OLVGroup hotGroup; + + /// + /// Gets the part of the cell that the mouse used to be over + /// + public HitTestLocation OldHotCellHitLocation { + get { return oldHotCellHitLocation; } + internal set { oldHotCellHitLocation = value; } + } + private HitTestLocation oldHotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse used to be over + /// + public virtual HitTestLocationEx OldHotCellHitLocationEx + { + get { return this.oldHotCellHitLocationEx; } + internal set { this.oldHotCellHitLocationEx = value; } + } + private HitTestLocationEx oldHotCellHitLocationEx; + + /// + /// Gets the index of the column that the mouse used to be over + /// + public int OldHotColumnIndex { + get { return oldHotColumnIndex; } + internal set { oldHotColumnIndex = value; } + } + private int oldHotColumnIndex; + + /// + /// Gets the index of the row that the mouse used to be over + /// + public int OldHotRowIndex { + get { return oldHotRowIndex; } + internal set { oldHotRowIndex = value; } + } + private int oldHotRowIndex; + + /// + /// Gets the group that the mouse used to be over + /// + public OLVGroup OldHotGroup + { + get { return oldHotGroup; } + internal set { oldHotGroup = value; } + } + private OLVGroup oldHotGroup; + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() { + return string.Format("NewHotCellHitLocation: {0}, HotCellHitLocationEx: {1}, NewHotColumnIndex: {2}, NewHotRowIndex: {3}, HotGroup: {4}", this.newHotCellHitLocation, this.hotCellHitLocationEx, this.newHotColumnIndex, this.newHotRowIndex, this.hotGroup); + } + } + + /// + /// Let the world know that a checkbox on a subitem is changing + /// + public class SubItemCheckingEventArgs : CancellableEventArgs + { + /// + /// Create a new event block + /// + /// + /// + /// + /// + /// + public SubItemCheckingEventArgs(OLVColumn column, OLVListItem item, int subItemIndex, CheckState currentValue, CheckState newValue) { + this.column = column; + this.listViewItem = item; + this.subItemIndex = subItemIndex; + this.currentValue = currentValue; + this.newValue = newValue; + } + + /// + /// The column of the cell that is having its checkbox changed. + /// + public OLVColumn Column { + get { return this.column; } + } + private OLVColumn column; + + /// + /// The model object of the row of the cell that is having its checkbox changed. + /// + public Object RowObject { + get { return this.listViewItem.RowObject; } + } + + /// + /// The listview item of the cell that is having its checkbox changed. + /// + public OLVListItem ListViewItem { + get { return this.listViewItem; } + } + private OLVListItem listViewItem; + + /// + /// The current check state of the cell. + /// + public CheckState CurrentValue { + get { return this.currentValue; } + } + private CheckState currentValue; + + /// + /// The proposed new check state of the cell. + /// + public CheckState NewValue { + get { return this.newValue; } + set { this.newValue = value; } + } + private CheckState newValue; + + /// + /// The index of the cell that is going to be or has been edited. + /// + public int SubItemIndex { + get { return this.subItemIndex; } + } + private int subItemIndex; + } + + /// + /// This event argument block is used when groups are created for a list. + /// + public class CreateGroupsEventArgs : EventArgs + { + /// + /// Create a CreateGroupsEventArgs + /// + /// + public CreateGroupsEventArgs(GroupingParameters parms) { + this.parameters = parms; + } + + /// + /// Gets the settings that control the creation of groups + /// + public GroupingParameters Parameters { + get { return this.parameters; } + } + private GroupingParameters parameters; + + /// + /// Gets or sets the groups that should be used + /// + public IList Groups { + get { return this.groups; } + set { this.groups = value; } + } + private IList groups; + + /// + /// Has this event been cancelled by the event handler? + /// + public bool Canceled + { + get { return canceled; } + set { canceled = value; } + } + private bool canceled; + + } + + /// + /// This event argument block is used when the text of a group task is clicked + /// + public class GroupTaskClickedEventArgs : EventArgs + { + /// + /// Create a GroupTaskClickedEventArgs + /// + /// + public GroupTaskClickedEventArgs(OLVGroup group) + { + this.group = group; + } + + /// + /// Gets which group was clicked + /// + public OLVGroup Group + { + get { return this.group; } + } + private readonly OLVGroup group; + } + + /// + /// This event argument block is used when a group is about to expand or collapse + /// + public class GroupExpandingCollapsingEventArgs : CancellableEventArgs + { + /// + /// Create a GroupExpandingCollapsingEventArgs + /// + /// + public GroupExpandingCollapsingEventArgs(OLVGroup group) { + if (group == null) throw new ArgumentNullException("group"); + this.olvGroup = group; + } + + /// + /// Gets which group is expanding/collapsing + /// + public OLVGroup Group + { + get { return this.olvGroup; } + } + private readonly OLVGroup olvGroup; + + /// + /// Gets whether this event is going to expand the group. + /// If this is false, the group must be collapsing. + /// + public bool IsExpanding { + get { return this.Group.Collapsed; } + } + } + + /// + /// This event argument block is used when the state of group has changed (collapsed, selected) + /// + public class GroupStateChangedEventArgs : EventArgs { + /// + /// Create a GroupStateChangedEventArgs + /// + /// + /// + /// + public GroupStateChangedEventArgs(OLVGroup group, GroupState oldState, GroupState newState) { + this.group = group; + this.oldState = oldState; + this.newState = newState; + } + + /// + /// Gets whether the group was collapsed by this event + /// + public bool Collapsed { + get { + return ((oldState & GroupState.LVGS_COLLAPSED) != GroupState.LVGS_COLLAPSED) && + ((newState & GroupState.LVGS_COLLAPSED) == GroupState.LVGS_COLLAPSED); + } + } + + /// + /// Gets whether the group was focused by this event + /// + public bool Focused { + get { + return ((oldState & GroupState.LVGS_FOCUSED) != GroupState.LVGS_FOCUSED) && + ((newState & GroupState.LVGS_FOCUSED) == GroupState.LVGS_FOCUSED); + } + } + + /// + /// Gets whether the group was selected by this event + /// + public bool Selected { + get { + return ((oldState & GroupState.LVGS_SELECTED) != GroupState.LVGS_SELECTED) && + ((newState & GroupState.LVGS_SELECTED) == GroupState.LVGS_SELECTED); + } + } + + /// + /// Gets whether the group was uncollapsed by this event + /// + public bool Uncollapsed { + get { + return ((oldState & GroupState.LVGS_COLLAPSED) == GroupState.LVGS_COLLAPSED) && + ((newState & GroupState.LVGS_COLLAPSED) != GroupState.LVGS_COLLAPSED); + } + } + + /// + /// Gets whether the group was unfocused by this event + /// + public bool Unfocused + { + get + { + return ((oldState & GroupState.LVGS_FOCUSED) == GroupState.LVGS_FOCUSED) && + ((newState & GroupState.LVGS_FOCUSED) != GroupState.LVGS_FOCUSED); + } + } + + /// + /// Gets whether the group was unselected by this event + /// + public bool Unselected + { + get + { + return ((oldState & GroupState.LVGS_SELECTED) == GroupState.LVGS_SELECTED) && + ((newState & GroupState.LVGS_SELECTED) != GroupState.LVGS_SELECTED); + } + } + + /// + /// Gets which group had its state changed + /// + public OLVGroup Group { + get { return this.group; } + } + + private readonly OLVGroup group; + + /// + /// Gets the previous state of the group + /// + public GroupState OldState { + get { return this.oldState; } + } + + private readonly GroupState oldState; + + + /// + /// Gets the new state of the group + /// + public GroupState NewState { + get { return this.newState; } + } + + private readonly GroupState newState; + } + + /// + /// This event argument block is used when a branch of a tree is about to be expanded + /// + public class TreeBranchExpandingEventArgs : CancellableEventArgs + { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchExpandingEventArgs(object model, OLVListItem item) + { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is about to expand. If null, all branches are going to be expanded. + /// + public object Model + { + get { return model; } + private set { model = value; } + } + private object model; + + /// + /// Gets the OLVListItem that is about to be expanded + /// + public OLVListItem Item + { + get { return item; } + private set { item = value; } + } + private OLVListItem item; + + } + + /// + /// This event argument block is used when a branch of a tree has just been expanded + /// + public class TreeBranchExpandedEventArgs : EventArgs + { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchExpandedEventArgs(object model, OLVListItem item) + { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is was expanded. If null, all branches were expanded. + /// + public object Model + { + get { return model; } + private set { model = value; } + } + private object model; + + /// + /// Gets the OLVListItem that was expanded + /// + public OLVListItem Item + { + get { return item; } + private set { item = value; } + } + private OLVListItem item; + + } + + /// + /// This event argument block is used when a branch of a tree is about to be collapsed + /// + public class TreeBranchCollapsingEventArgs : CancellableEventArgs + { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchCollapsingEventArgs(object model, OLVListItem item) + { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is about to collapse. If this is null, all models are going to collapse. + /// + public object Model + { + get { return model; } + private set { model = value; } + } + private object model; + + /// + /// Gets the OLVListItem that is about to be collapsed. Can be null + /// + public OLVListItem Item + { + get { return item; } + private set { item = value; } + } + private OLVListItem item; + } + + + /// + /// This event argument block is used when a branch of a tree has just been collapsed + /// + public class TreeBranchCollapsedEventArgs : EventArgs + { + /// + /// Create a new event args + /// + /// + /// + public TreeBranchCollapsedEventArgs(object model, OLVListItem item) + { + this.Model = model; + this.Item = item; + } + + /// + /// Gets the model that is was collapsed. If null, all branches were collapsed + /// + public object Model + { + get { return model; } + private set { model = value; } + } + private object model; + + /// + /// Gets the OLVListItem that was collapsed + /// + public OLVListItem Item + { + get { return item; } + private set { item = value; } + } + private OLVListItem item; + + } + + #endregion + + +} diff --git a/ObjectListView/Implementation/GroupingParameters.cs b/ObjectListView/Implementation/GroupingParameters.cs new file mode 100644 index 0000000..6469a43 --- /dev/null +++ b/ObjectListView/Implementation/GroupingParameters.cs @@ -0,0 +1,175 @@ +/* + * GroupingParameters - All the data that is used to create groups in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// This class contains all the settings used when groups are created + /// + public class GroupingParameters { + /// + /// Create a GroupingParameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public GroupingParameters(ObjectListView olv, OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn column, SortOrder order, OLVColumn secondaryColumn, SortOrder secondaryOrder, + string titleFormat, string titleSingularFormat, bool sortItemsByPrimaryColumn) { + this.ListView = olv; + this.GroupByColumn = groupByColumn; + this.GroupByOrder = groupByOrder; + this.PrimarySort = column; + this.PrimarySortOrder = order; + this.SecondarySort = secondaryColumn; + this.SecondarySortOrder = secondaryOrder; + this.SortItemsByPrimaryColumn = sortItemsByPrimaryColumn; + this.TitleFormat = titleFormat; + this.TitleSingularFormat = titleSingularFormat; + } + + /// + /// Gets or sets the ObjectListView being grouped + /// + public ObjectListView ListView { + get { return this.listView; } + set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the column used to create groups + /// + public OLVColumn GroupByColumn { + get { return this.groupByColumn; } + set { this.groupByColumn = value; } + } + private OLVColumn groupByColumn; + + /// + /// In what order will the groups themselves be sorted? + /// + public SortOrder GroupByOrder { + get { return this.groupByOrder; } + set { this.groupByOrder = value; } + } + private SortOrder groupByOrder; + + /// + /// If this is set, this comparer will be used to order the groups + /// + public IComparer GroupComparer { + get { return this.groupComparer; } + set { this.groupComparer = value; } + } + private IComparer groupComparer; + + /// + /// If this is set, this comparer will be used to order items within each group + /// + public IComparer ItemComparer { + get { return this.itemComparer; } + set { this.itemComparer = value; } + } + private IComparer itemComparer; + + /// + /// Gets or sets the column that will be the primary sort + /// + public OLVColumn PrimarySort { + get { return this.primarySort; } + set { this.primarySort = value; } + } + private OLVColumn primarySort; + + /// + /// Gets or sets the ordering for the primary sort + /// + public SortOrder PrimarySortOrder { + get { return this.primarySortOrder; } + set { this.primarySortOrder = value; } + } + private SortOrder primarySortOrder; + + /// + /// Gets or sets the column used for secondary sorting + /// + public OLVColumn SecondarySort { + get { return this.secondarySort; } + set { this.secondarySort = value; } + } + private OLVColumn secondarySort; + + /// + /// Gets or sets the ordering for the secondary sort + /// + public SortOrder SecondarySortOrder { + get { return this.secondarySortOrder; } + set { this.secondarySortOrder = value; } + } + private SortOrder secondarySortOrder; + + /// + /// Gets or sets the title format used for groups with zero or more than one element + /// + public string TitleFormat { + get { return this.titleFormat; } + set { this.titleFormat = value; } + } + private string titleFormat; + + /// + /// Gets or sets the title format used for groups with only one element + /// + public string TitleSingularFormat { + get { return this.titleSingularFormat; } + set { this.titleSingularFormat = value; } + } + private string titleSingularFormat; + + /// + /// Gets or sets whether the items should be sorted by the primary column + /// + public bool SortItemsByPrimaryColumn { + get { return this.sortItemsByPrimaryColumn; } + set { this.sortItemsByPrimaryColumn = value; } + } + private bool sortItemsByPrimaryColumn; + } +} diff --git a/ObjectListView/Implementation/Groups.cs b/ObjectListView/Implementation/Groups.cs new file mode 100644 index 0000000..91237e3 --- /dev/null +++ b/ObjectListView/Implementation/Groups.cs @@ -0,0 +1,747 @@ +/* + * Groups - Enhancements to the normal ListViewGroup + * + * Author: Phillip Piper + * Date: 22/08/2009 6:03PM + * + * Change log: + * v2.3 + * 2009-09-09 JPP - Added Collapsed and Collapsible properties + * 2009-09-01 JPP - Cleaned up code, added more docs + * - Works under VS2005 again + * 2009-08-22 JPP - Initial version + * + * To do: + * - Implement subseting + * - Implement footer items + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// These values indicate what is the state of the group. These values + /// are taken directly from the SDK and many are not used by ObjectListView. + /// + [Flags] + public enum GroupState + { + /// + /// Normal + /// + LVGS_NORMAL = 0x0, + + /// + /// Collapsed + /// + LVGS_COLLAPSED = 0x1, + + /// + /// Hidden + /// + LVGS_HIDDEN = 0x2, + + /// + /// NoHeader + /// + LVGS_NOHEADER = 0x4, + + /// + /// Can be collapsed + /// + LVGS_COLLAPSIBLE = 0x8, + + /// + /// Has focus + /// + LVGS_FOCUSED = 0x10, + + /// + /// Is Selected + /// + LVGS_SELECTED = 0x20, + + /// + /// Is subsetted + /// + LVGS_SUBSETED = 0x40, + + /// + /// Subset link has focus + /// + LVGS_SUBSETLINKFOCUSED = 0x80, + + /// + /// All styles + /// + LVGS_ALL = 0xFFFF + } + + /// + /// This mask indicates which members of a LVGROUP have valid data. These values + /// are taken directly from the SDK and many are not used by ObjectListView. + /// + [Flags] + public enum GroupMask + { + /// + /// No mask + /// + LVGF_NONE = 0, + + /// + /// Group has header + /// + LVGF_HEADER = 1, + + /// + /// Group has footer + /// + LVGF_FOOTER = 2, + + /// + /// Group has state + /// + LVGF_STATE = 4, + + /// + /// + /// + LVGF_ALIGN = 8, + + /// + /// + /// + LVGF_GROUPID = 0x10, + + /// + /// pszSubtitle is valid + /// + LVGF_SUBTITLE = 0x00100, + + /// + /// pszTask is valid + /// + LVGF_TASK = 0x00200, + + /// + /// pszDescriptionTop is valid + /// + LVGF_DESCRIPTIONTOP = 0x00400, + + /// + /// pszDescriptionBottom is valid + /// + LVGF_DESCRIPTIONBOTTOM = 0x00800, + + /// + /// iTitleImage is valid + /// + LVGF_TITLEIMAGE = 0x01000, + + /// + /// iExtendedImage is valid + /// + LVGF_EXTENDEDIMAGE = 0x02000, + + /// + /// iFirstItem and cItems are valid + /// + LVGF_ITEMS = 0x04000, + + /// + /// pszSubsetTitle is valid + /// + LVGF_SUBSET = 0x08000, + + /// + /// readonly, cItems holds count of items in visible subset, iFirstItem is valid + /// + LVGF_SUBSETITEMS = 0x10000 + } + + /// + /// This mask indicates which members of a GROUPMETRICS structure are valid + /// + [Flags] + public enum GroupMetricsMask + { + /// + /// + /// + LVGMF_NONE = 0, + + /// + /// + /// + LVGMF_BORDERSIZE = 1, + + /// + /// + /// + LVGMF_BORDERCOLOR = 2, + + /// + /// + /// + LVGMF_TEXTCOLOR = 4 + } + + /// + /// Instances of this class enhance the capabilities of a normal ListViewGroup, + /// enabling the functionality that was released in v6 of the common controls. + /// + /// + /// + /// In this implementation (2009-09), these objects are essentially passive. + /// Setting properties does not automatically change the associated group in + /// the listview. Collapsed and Collapsible are two exceptions to this and + /// give immediate results. + /// + /// + /// This really should be a subclass of ListViewGroup, but that class is + /// sealed (why is that?). So this class provides the same interface as a + /// ListViewGroup, plus many other new properties. + /// + /// + public class OLVGroup + { + #region Creation + + /// + /// Create an OLVGroup + /// + public OLVGroup() : this("Default group header") { + } + + /// + /// Create a group with the given title + /// + /// Title of the group + public OLVGroup(string header) { + this.Header = header; + this.Id = OLVGroup.nextId++; + this.TitleImage = -1; + this.ExtendedImage = -1; + } + private static int nextId; + + #endregion + + #region Public properties + + /// + /// Gets or sets the bottom description of the group + /// + /// + /// Descriptions only appear when group is centered and there is a title image + /// + public string BottomDescription { + get { return this.bottomDescription; } + set { this.bottomDescription = value; } + } + private string bottomDescription; + + /// + /// Gets or sets whether or not this group is collapsed + /// + public bool Collapsed { + get { return this.GetOneState(GroupState.LVGS_COLLAPSED); } + set { this.SetOneState(value, GroupState.LVGS_COLLAPSED); } + } + + /// + /// Gets or sets whether or not this group can be collapsed + /// + public bool Collapsible { + get { return this.GetOneState(GroupState.LVGS_COLLAPSIBLE); } + set { this.SetOneState(value, GroupState.LVGS_COLLAPSIBLE); } + } + + /// + /// Gets or sets some representation of the contents of this group + /// + /// This is user defined (like Tag) + public IList Contents { + get { return this.contents; } + set { this.contents = value; } + } + private IList contents; + + /// + /// Gets whether this group has been created. + /// + public bool Created { + get { return this.ListView != null; } + } + + /// + /// Gets or sets the int or string that will select the extended image to be shown against the title + /// + public object ExtendedImage { + get { return this.extendedImage; } + set { this.extendedImage = value; } + } + private object extendedImage; + + /// + /// Gets or sets the footer of the group + /// + public string Footer { + get { return this.footer; } + set { this.footer = value; } + } + private string footer; + + /// + /// Gets the internal id of our associated ListViewGroup. + /// + public int GroupId { + get { + if (this.ListViewGroup == null) + return this.Id; + + // Use reflection to get around the access control on the ID property + if (OLVGroup.groupIdPropInfo == null) { + OLVGroup.groupIdPropInfo = typeof(ListViewGroup).GetProperty("ID", + BindingFlags.NonPublic | BindingFlags.Instance); + System.Diagnostics.Debug.Assert(OLVGroup.groupIdPropInfo != null); + } + + int? groupId = OLVGroup.groupIdPropInfo.GetValue(this.ListViewGroup, null) as int?; + return groupId.HasValue ? groupId.Value : -1; + } + } + private static PropertyInfo groupIdPropInfo; + + /// + /// Gets or sets the header of the group + /// + public string Header { + get { return this.header; } + set { this.header = value; } + } + private string header; + + /// + /// Gets or sets the horizontal alignment of the group header + /// + public HorizontalAlignment HeaderAlignment { + get { return this.headerAlignment; } + set { this.headerAlignment = value; } + } + private HorizontalAlignment headerAlignment; + + /// + /// Gets or sets the internally created id of the group + /// + public int Id { + get { return this.id; } + set { this.id = value; } + } + private int id; + + /// + /// Gets or sets ListViewItems that are members of this group + /// + /// Listener of the BeforeCreatingGroups event can populate this collection. + /// It is only used on non-virtual lists. + public IList Items { + get { return this.items; } + set { this.items = value; } + } + private IList items = new List(); + + /// + /// Gets or sets the key that was used to partition objects into this group + /// + /// This is user defined (like Tag) + public object Key { + get { return this.key; } + set { this.key = value; } + } + private object key; + + /// + /// Gets the ObjectListView that this group belongs to + /// + /// If this is null, the group has not yet been created. + public ObjectListView ListView { + get { return this.listView; } + protected set { this.listView = value; } + } + private ObjectListView listView; + + /// + /// Gets or sets the name of the group + /// + /// As of 2009-09-01, this property is not used. + public string Name { + get { return this.name; } + set { this.name = value; } + } + private string name; + + /// + /// Gets or sets whether this group is focused + /// + public bool Focused + { + get { return this.GetOneState(GroupState.LVGS_FOCUSED); } + set { this.SetOneState(value, GroupState.LVGS_FOCUSED); } + } + + /// + /// Gets or sets whether this group is selected + /// + public bool Selected + { + get { return this.GetOneState(GroupState.LVGS_SELECTED); } + set { this.SetOneState(value, GroupState.LVGS_SELECTED); } + } + + /// + /// Gets or sets the text that will show that this group is subsetted + /// + /// + /// As of WinSDK v7.0, subsetting of group is officially unimplemented. + /// We can get around this using undocumented interfaces and may do so. + /// + public string SubsetTitle { + get { return this.subsetTitle; } + set { this.subsetTitle = value; } + } + private string subsetTitle; + + /// + /// Gets or set the subtitleof the task + /// + public string Subtitle { + get { return this.subtitle; } + set { this.subtitle = value; } + } + private string subtitle; + + /// + /// Gets or sets the value by which this group will be sorted. + /// + public IComparable SortValue { + get { return this.sortValue; } + set { this.sortValue = value; } + } + private IComparable sortValue; + + /// + /// Gets or sets the state of the group + /// + public GroupState State { + get { return this.state; } + set { this.state = value; } + } + private GroupState state; + + /// + /// Gets or sets which bits of State are valid + /// + public GroupState StateMask { + get { return this.stateMask; } + set { this.stateMask = value; } + } + private GroupState stateMask; + + /// + /// Gets or sets whether this group is showing only a subset of its elements + /// + /// + /// As of WinSDK v7.0, this property officially does nothing. + /// + public bool Subseted { + get { return this.GetOneState(GroupState.LVGS_SUBSETED); } + set { this.SetOneState(value, GroupState.LVGS_SUBSETED); } + } + + /// + /// Gets or sets the user-defined data attached to this group + /// + public object Tag { + get { return this.tag; } + set { this.tag = value; } + } + private object tag; + + /// + /// Gets or sets the task of this group + /// + /// This task is the clickable text that appears on the right margin + /// of the group header. + public string Task { + get { return this.task; } + set { this.task = value; } + } + private string task; + + /// + /// Gets or sets the int or string that will select the image to be shown against the title + /// + public object TitleImage { + get { return this.titleImage; } + set { this.titleImage = value; } + } + private object titleImage; + + /// + /// Gets or sets the top description of the group + /// + /// + /// Descriptions only appear when group is centered and there is a title image + /// + public string TopDescription { + get { return this.topDescription; } + set { this.topDescription = value; } + } + private string topDescription; + + /// + /// Gets or sets the number of items that are within this group. + /// + /// This should only be used for virtual groups. + public int VirtualItemCount { + get { return this.virtualItemCount; } + set { this.virtualItemCount = value; } + } + private int virtualItemCount; + + #endregion + + #region Protected properties + + /// + /// Gets or sets the ListViewGroup that is shadowed by this group. + /// + /// For virtual groups, this will always be null. + protected ListViewGroup ListViewGroup { + get { return this.listViewGroup; } + set { this.listViewGroup = value; } + } + private ListViewGroup listViewGroup; + #endregion + + #region Calculations/Conversions + + /// + /// Calculate the index into the group image list of the given image selector + /// + /// + /// + public int GetImageIndex(object imageSelector) { + if (imageSelector == null || this.ListView == null || this.ListView.GroupImageList == null) + return -1; + + if (imageSelector is Int32) + return (int)imageSelector; + + String imageSelectorAsString = imageSelector as String; + if (imageSelectorAsString != null) + return this.ListView.GroupImageList.Images.IndexOfKey(imageSelectorAsString); + + return -1; + } + + /// + /// Convert this object to a string representation + /// + /// + public override string ToString() { + return this.Header; + } + + #endregion + + #region Commands + + /// + /// Insert a native group into the underlying Windows control, + /// *without* using a ListViewGroup + /// + /// + /// This is used when creating virtual groups + public void InsertGroupNewStyle(ObjectListView olv) { + this.ListView = olv; + NativeMethods.InsertGroup(olv, this.AsNativeGroup(true)); + } + + /// + /// Insert a native group into the underlying control via a ListViewGroup + /// + /// + public void InsertGroupOldStyle(ObjectListView olv) { + this.ListView = olv; + + // Create/update the associated ListViewGroup + if (this.ListViewGroup == null) + this.ListViewGroup = new ListViewGroup(); + this.ListViewGroup.Header = this.Header; + this.ListViewGroup.HeaderAlignment = this.HeaderAlignment; + this.ListViewGroup.Name = this.Name; + + // Remember which OLVGroup created the ListViewGroup + this.ListViewGroup.Tag = this; + + // Add the group to the control + olv.Groups.Add(this.ListViewGroup); + + // Add any extra information + NativeMethods.SetGroupInfo(olv, this.GroupId, this.AsNativeGroup(false)); + } + + /// + /// Change the members of the group to match the current contents of Items, + /// using a ListViewGroup + /// + public void SetItemsOldStyle() { + List list = this.Items as List; + if (list == null) { + foreach (OLVListItem item in this.Items) { + this.ListViewGroup.Items.Add(item); + } + } else { + this.ListViewGroup.Items.AddRange(list.ToArray()); + } + } + + #endregion + + #region Implementation + + /// + /// Create a native LVGROUP structure that matches this group + /// + internal NativeMethods.LVGROUP2 AsNativeGroup(bool withId) { + + NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2(); + group.cbSize = (uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2)); + group.mask = (uint)(GroupMask.LVGF_HEADER ^ GroupMask.LVGF_ALIGN ^ GroupMask.LVGF_STATE); + group.pszHeader = this.Header; + group.uAlign = (uint)this.HeaderAlignment; + group.stateMask = (uint)this.StateMask; + group.state = (uint)this.State; + + if (withId) { + group.iGroupId = this.GroupId; + group.mask ^= (uint)GroupMask.LVGF_GROUPID; + } + + if (!String.IsNullOrEmpty(this.Footer)) { + group.pszFooter = this.Footer; + group.mask ^= (uint)GroupMask.LVGF_FOOTER; + } + + if (!String.IsNullOrEmpty(this.Subtitle)) { + group.pszSubtitle = this.Subtitle; + group.mask ^= (uint)GroupMask.LVGF_SUBTITLE; + } + + if (!String.IsNullOrEmpty(this.Task)) { + group.pszTask = this.Task; + group.mask ^= (uint)GroupMask.LVGF_TASK; + } + + if (!String.IsNullOrEmpty(this.TopDescription)) { + group.pszDescriptionTop = this.TopDescription; + group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONTOP; + } + + if (!String.IsNullOrEmpty(this.BottomDescription)) { + group.pszDescriptionBottom = this.BottomDescription; + group.mask ^= (uint)GroupMask.LVGF_DESCRIPTIONBOTTOM; + } + + int imageIndex = this.GetImageIndex(this.TitleImage); + if (imageIndex >= 0) { + group.iTitleImage = imageIndex; + group.mask ^= (uint)GroupMask.LVGF_TITLEIMAGE; + } + + imageIndex = this.GetImageIndex(this.ExtendedImage); + if (imageIndex >= 0) { + group.iExtendedImage = imageIndex; + group.mask ^= (uint)GroupMask.LVGF_EXTENDEDIMAGE; + } + + if (!String.IsNullOrEmpty(this.SubsetTitle)) { + group.pszSubsetTitle = this.SubsetTitle; + group.mask ^= (uint)GroupMask.LVGF_SUBSET; + } + + if (this.VirtualItemCount > 0) { + group.cItems = this.VirtualItemCount; + group.mask ^= (uint)GroupMask.LVGF_ITEMS; + } + + return group; + } + + private bool GetOneState(GroupState mask) { + if (this.Created) + this.State = this.GetState(); + return (this.State & mask) == mask; + } + + /// + /// Get the current state of this group from the underlying control + /// + protected GroupState GetState() { + return NativeMethods.GetGroupState(this.ListView, this.GroupId, GroupState.LVGS_ALL); + } + + /// + /// Get the current state of this group from the underlying control + /// + protected int SetState(GroupState newState, GroupState mask) { + NativeMethods.LVGROUP2 group = new NativeMethods.LVGROUP2(); + group.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUP2))); + group.mask = (uint)GroupMask.LVGF_STATE; + group.state = (uint)newState; + group.stateMask = (uint)mask; + return NativeMethods.SetGroupInfo(this.ListView, this.GroupId, group); + } + + private void SetOneState(bool value, GroupState mask) + { + this.StateMask ^= mask; + if (value) + this.State ^= mask; + else + this.State &= ~mask; + + if (this.Created) + this.SetState(this.State, mask); + } + + #endregion + + } +} diff --git a/ObjectListView/Implementation/Munger.cs b/ObjectListView/Implementation/Munger.cs new file mode 100644 index 0000000..57f635b --- /dev/null +++ b/ObjectListView/Implementation/Munger.cs @@ -0,0 +1,568 @@ +/* + * Munger - An Interface pattern on getting and setting values from object through Reflection + * + * Author: Phillip Piper + * Date: 28/11/2008 17:15 + * + * Change log: + * v2.5.1 + * 2012-05-01 JPP - Added IgnoreMissingAspects property + * v2.5 + * 2011-05-20 JPP - Accessing through an indexer when the target had both a integer and + * a string indexer didn't work reliably. + * v2.4.1 + * 2010-08-10 JPP - Refactored into Munger/SimpleMunger. 3x faster! + * v2.3 + * 2009-02-15 JPP - Made Munger a public class + * 2009-01-20 JPP - Made the Munger capable of handling indexed access. + * Incidentally, this removed the ugliness that the last change introduced. + * 2009-01-18 JPP - Handle target objects from a DataListView (normally DataRowViews) + * v2.0 + * 2008-11-28 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace BrightIdeasSoftware +{ + /// + /// An instance of Munger gets a value from or puts a value into a target object. The property + /// to be peeked (or poked) is determined from a string. The peeking or poking is done using reflection. + /// + /// + /// Name of the aspect to be peeked can be a field, property or parameterless method. The name of an + /// aspect to poke can be a field, writable property or single parameter method. + /// + /// Aspect names can be dotted to chain a series of references. + /// + /// Order.Customer.HomeAddress.State + /// + public class Munger + { + #region Life and death + + /// + /// Create a do nothing Munger + /// + public Munger() + { + } + + /// + /// Create a Munger that works on the given aspect name + /// + /// The name of the + public Munger(String aspectName) + { + this.AspectName = aspectName; + } + + #endregion + + #region Static utility methods + + /// + /// A helper method to put the given value into the given aspect of the given object. + /// + /// This method catches and silently ignores any errors that occur + /// while modifying the target object + /// The object to be modified + /// The name of the property/field to be modified + /// The value to be assigned + /// Did the modification work? + public static bool PutProperty(object target, string propertyName, object value) { + try { + Munger munger = new Munger(propertyName); + return munger.PutValue(target, value); + } + catch (MungerException) { + // Not a lot we can do about this. Something went wrong in the bowels + // of the property. Let's take the ostrich approach and just ignore it :-) + + // Normally, we would never just silently ignore an exception. + // However, in this case, this is a utility method that explicitly + // contracts to catch and ignore errors. If this is not acceptible, + // the programmer should not use this method. + } + + return false; + } + + /// + /// Gets or sets whether Mungers will silently ignore missing aspect errors. + /// + /// + /// + /// By default, if a Munger is asked to fetch a field/property/method + /// that does not exist from a model, it returns an error message, since that + /// condition is normally a programming error. There are some use cases where + /// this is not an error, and the munger should simply keep quiet. + /// + /// By default this is true during release builds. + /// + public static bool IgnoreMissingAspects { + get { return ignoreMissingAspects; } + set { ignoreMissingAspects = value; } + } + private static bool ignoreMissingAspects +#if !DEBUG + = true +#endif + ; + + #endregion + + #region Public properties + + /// + /// The name of the aspect that is to be peeked or poked. + /// + /// + /// + /// This name can be a field, property or parameter-less method. + /// + /// + /// The name can be dotted, which chains references. If any link in the chain returns + /// null, the entire chain is considered to return null. + /// + /// + /// "DateOfBirth" + /// "Owner.HomeAddress.Postcode" + public string AspectName + { + get { return aspectName; } + set { + aspectName = value; + + // Clear any cache + aspectParts = null; + } + } + private string aspectName; + + #endregion + + + #region Public interface + + /// + /// Extract the value indicated by our AspectName from the given target. + /// + /// If the aspect name is null or empty, this will return null. + /// The object that will be peeked + /// The value read from the target + public Object GetValue(Object target) { + if (this.Parts.Count == 0) + return null; + + try { + return this.EvaluateParts(target, this.Parts); + } catch (MungerException ex) { + if (Munger.IgnoreMissingAspects) + return null; + + return String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", + ex.Munger.AspectName, ex.Target.GetType()); + } + } + + /// + /// Extract the value indicated by our AspectName from the given target, raising exceptions + /// if the munger fails. + /// + /// If the aspect name is null or empty, this will return null. + /// The object that will be peeked + /// The value read from the target + public Object GetValueEx(Object target) { + if (this.Parts.Count == 0) + return null; + + return this.EvaluateParts(target, this.Parts); + } + + /// + /// Poke the given value into the given target indicated by our AspectName. + /// + /// + /// + /// If the AspectName is a dotted path, all the selectors bar the last + /// are used to find the object that should be updated, and the last + /// selector is used as the property to update on that object. + /// + /// + /// So, if 'target' is a Person and the AspectName is "HomeAddress.Postcode", + /// this method will first fetch "HomeAddress" property, and then try to set the + /// "Postcode" property on the home address object. + /// + /// + /// The object that will be poked + /// The value that will be poked into the target + /// bool indicating whether the put worked + public bool PutValue(Object target, Object value) + { + if (this.Parts.Count == 0) + return false; + + SimpleMunger lastPart = this.Parts[this.Parts.Count - 1]; + + if (this.Parts.Count > 1) { + List parts = new List(this.Parts); + parts.RemoveAt(parts.Count - 1); + try { + target = this.EvaluateParts(target, parts); + } catch (MungerException ex) { + this.ReportPutValueException(ex); + return false; + } + } + + if (target != null) { + try { + return lastPart.PutValue(target, value); + } catch (MungerException ex) { + this.ReportPutValueException(ex); + } + } + + return false; + } + + #endregion + + #region Implementation + + /// + /// Gets the list of SimpleMungers that match our AspectName + /// + private IList Parts { + get { + if (aspectParts == null) + aspectParts = BuildParts(this.AspectName); + return aspectParts; + } + } + private IList aspectParts; + + /// + /// Convert a possibly dotted AspectName into a list of SimpleMungers + /// + /// + /// + private IList BuildParts(string aspect) { + List parts = new List(); + if (!String.IsNullOrEmpty(aspect)) { + foreach (string part in aspect.Split('.')) { + parts.Add(new SimpleMunger(part.Trim())); + } + } + return parts; + } + + /// + /// Evaluate the given chain of SimpleMungers against an initial target. + /// + /// + /// + /// + private object EvaluateParts(object target, IList parts) { + foreach (SimpleMunger part in parts) { + if (target == null) + break; + target = part.GetValue(target); + } + return target; + } + + private void ReportPutValueException(MungerException ex) { + //TODO: How should we report this error? + System.Diagnostics.Debug.WriteLine("PutValue failed"); + System.Diagnostics.Debug.WriteLine(String.Format("- Culprit aspect: {0}", ex.Munger.AspectName)); + System.Diagnostics.Debug.WriteLine(String.Format("- Target: {0} of type {1}", ex.Target, ex.Target.GetType())); + System.Diagnostics.Debug.WriteLine(String.Format("- Inner exception: {0}", ex.InnerException)); + } + + #endregion + } + + /// + /// A SimpleMunger deals with a single property/field/method on its target. + /// + /// + /// Munger uses a chain of these resolve a dotted aspect name. + /// + public class SimpleMunger + { + #region Life and death + + /// + /// Create a SimpleMunger + /// + /// + public SimpleMunger(String aspectName) + { + this.aspectName = aspectName; + } + + #endregion + + #region Public properties + + /// + /// The name of the aspect that is to be peeked or poked. + /// + /// + /// + /// This name can be a field, property or method. + /// When using a method to get a value, the method must be parameter-less. + /// When using a method to set a value, the method must accept 1 parameter. + /// + /// + /// It cannot be a dotted name. + /// + /// + public string AspectName { + get { return aspectName; } + } + private readonly string aspectName; + + #endregion + + #region Public interface + + /// + /// Get a value from the given target + /// + /// + /// + public Object GetValue(Object target) { + if (target == null) + return null; + + this.ResolveName(target, this.AspectName, 0); + + try { + if (this.resolvedPropertyInfo != null) + return this.resolvedPropertyInfo.GetValue(target, null); + + if (this.resolvedMethodInfo != null) + return this.resolvedMethodInfo.Invoke(target, null); + + if (this.resolvedFieldInfo != null) + return this.resolvedFieldInfo.GetValue(target); + + // If that didn't work, try to use the indexer property. + // This covers things like dictionaries and DataRows. + if (this.indexerPropertyInfo != null) + return this.indexerPropertyInfo.GetValue(target, new object[] { this.AspectName }); + } catch (Exception ex) { + // Lots of things can do wrong in these invocations + throw new MungerException(this, target, ex); + } + + // If we get to here, we couldn't find a match for the aspect + throw new MungerException(this, target, new MissingMethodException()); + } + + /// + /// Poke the given value into the given target indicated by our AspectName. + /// + /// The object that will be poked + /// The value that will be poked into the target + /// bool indicating if the put worked + public bool PutValue(object target, object value) { + if (target == null) + return false; + + this.ResolveName(target, this.AspectName, 1); + + try { + if (this.resolvedPropertyInfo != null) { + this.resolvedPropertyInfo.SetValue(target, value, null); + return true; + } + + if (this.resolvedMethodInfo != null) { + this.resolvedMethodInfo.Invoke(target, new object[] { value }); + return true; + } + + if (this.resolvedFieldInfo != null) { + this.resolvedFieldInfo.SetValue(target, value); + return true; + } + + // If that didn't work, try to use the indexer property. + // This covers things like dictionaries and DataRows. + if (this.indexerPropertyInfo != null) { + this.indexerPropertyInfo.SetValue(target, value, new object[] { this.AspectName }); + return true; + } + } catch (Exception ex) { + // Lots of things can do wrong in these invocations + throw new MungerException(this, target, ex); + } + + return false; + } + + #endregion + + #region Implementation + + private void ResolveName(object target, string name, int numberMethodParameters) { + + if (cachedTargetType == target.GetType() && cachedName == name && cachedNumberParameters == numberMethodParameters) + return; + + cachedTargetType = target.GetType(); + cachedName = name; + cachedNumberParameters = numberMethodParameters; + + resolvedFieldInfo = null; + resolvedPropertyInfo = null; + resolvedMethodInfo = null; + indexerPropertyInfo = null; + + const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance /*| BindingFlags.NonPublic*/; + + foreach (PropertyInfo pinfo in target.GetType().GetProperties(flags)) { + if (pinfo.Name == name) { + resolvedPropertyInfo = pinfo; + return; + } + + // See if we can find an string indexer property while we are here. + // We also need to allow for old style keyed collections. + if (indexerPropertyInfo == null && pinfo.Name == "Item") { + ParameterInfo[] par = pinfo.GetGetMethod().GetParameters(); + if (par.Length > 0) { + Type parameterType = par[0].ParameterType; + if (parameterType == typeof(string) || parameterType == typeof(object)) + indexerPropertyInfo = pinfo; + } + } + } + + foreach (FieldInfo info in target.GetType().GetFields(flags)) { + if (info.Name == name) { + resolvedFieldInfo = info; + return; + } + } + + foreach (MethodInfo info in target.GetType().GetMethods(flags)) { + if (info.Name == name && info.GetParameters().Length == numberMethodParameters) { + resolvedMethodInfo = info; + return; + } + } + } + + private Type cachedTargetType; + private string cachedName; + private int cachedNumberParameters; + + private FieldInfo resolvedFieldInfo; + private PropertyInfo resolvedPropertyInfo; + private MethodInfo resolvedMethodInfo; + private PropertyInfo indexerPropertyInfo; + + #endregion + } + + /// + /// These exceptions are raised when a munger finds something it cannot process + /// + public class MungerException : ApplicationException + { + /// + /// Create a MungerException + /// + /// + /// + /// + public MungerException(SimpleMunger munger, object target, Exception ex) + : base("Munger failed", ex) { + this.munger = munger; + this.target = target; + } + + /// + /// Get the munger that raised the exception + /// + public SimpleMunger Munger { + get { return munger; } + } + private readonly SimpleMunger munger; + + /// + /// Gets the target that threw the exception + /// + public object Target { + get { return target; } + } + private readonly object target; + } + + /* + * We don't currently need this + * 2010-08-06 + * + + internal class SimpleBinder : Binder + { + public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, System.Globalization.CultureInfo culture) { + //return Type.DefaultBinder.BindToField( + throw new NotImplementedException(); + } + + public override object ChangeType(object value, Type type, System.Globalization.CultureInfo culture) { + throw new NotImplementedException(); + } + + public override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] names, out object state) { + throw new NotImplementedException(); + } + + public override void ReorderArgumentArray(ref object[] args, object state) { + throw new NotImplementedException(); + } + + public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) { + throw new NotImplementedException(); + } + + public override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers) { + if (match == null) + throw new ArgumentNullException("match"); + + if (match.Length == 0) + return null; + + return match[0]; + } + } + */ + +} diff --git a/ObjectListView/Implementation/NativeMethods.cs b/ObjectListView/Implementation/NativeMethods.cs new file mode 100644 index 0000000..3540521 --- /dev/null +++ b/ObjectListView/Implementation/NativeMethods.cs @@ -0,0 +1,1226 @@ +/* + * NativeMethods - All the Windows SDK structures and imports + * + * Author: Phillip Piper + * Date: 10/10/2006 + * + * Change log: + * v2.8.0 + * 2014-05-21 JPP - Added DeselectOneItem + * - Added new imagelist drawing + * v2.3 + * 2006-10-10 JPP - Initial version + * + * To do: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// Wrapper for all native method calls on ListView controls + /// + internal static class NativeMethods + { + #region Constants + + private const int LVM_FIRST = 0x1000; + private const int LVM_GETCOLUMN = LVM_FIRST + 95; + private const int LVM_GETCOUNTPERPAGE = LVM_FIRST + 40; + private const int LVM_GETGROUPINFO = LVM_FIRST + 149; + private const int LVM_GETGROUPSTATE = LVM_FIRST + 92; + private const int LVM_GETHEADER = LVM_FIRST + 31; + private const int LVM_GETTOOLTIPS = LVM_FIRST + 78; + private const int LVM_GETTOPINDEX = LVM_FIRST + 39; + private const int LVM_HITTEST = LVM_FIRST + 18; + private const int LVM_INSERTGROUP = LVM_FIRST + 145; + private const int LVM_REMOVEALLGROUPS = LVM_FIRST + 160; + private const int LVM_SCROLL = LVM_FIRST + 20; + private const int LVM_SETBKIMAGE = LVM_FIRST + 0x8A; + private const int LVM_SETCOLUMN = LVM_FIRST + 96; + private const int LVM_SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54; + private const int LVM_SETGROUPINFO = LVM_FIRST + 147; + private const int LVM_SETGROUPMETRICS = LVM_FIRST + 155; + private const int LVM_SETIMAGELIST = LVM_FIRST + 3; + private const int LVM_SETITEM = LVM_FIRST + 76; + private const int LVM_SETITEMCOUNT = LVM_FIRST + 47; + private const int LVM_SETITEMSTATE = LVM_FIRST + 43; + private const int LVM_SETSELECTEDCOLUMN = LVM_FIRST + 140; + private const int LVM_SETTOOLTIPS = LVM_FIRST + 74; + private const int LVM_SUBITEMHITTEST = LVM_FIRST + 57; + private const int LVS_EX_SUBITEMIMAGES = 0x0002; + + private const int LVIF_TEXT = 0x0001; + private const int LVIF_IMAGE = 0x0002; + private const int LVIF_PARAM = 0x0004; + private const int LVIF_STATE = 0x0008; + private const int LVIF_INDENT = 0x0010; + private const int LVIF_NORECOMPUTE = 0x0800; + + private const int LVIS_SELECTED = 2; + + private const int LVCF_FMT = 0x0001; + private const int LVCF_WIDTH = 0x0002; + private const int LVCF_TEXT = 0x0004; + private const int LVCF_SUBITEM = 0x0008; + private const int LVCF_IMAGE = 0x0010; + private const int LVCF_ORDER = 0x0020; + private const int LVCFMT_LEFT = 0x0000; + private const int LVCFMT_RIGHT = 0x0001; + private const int LVCFMT_CENTER = 0x0002; + private const int LVCFMT_JUSTIFYMASK = 0x0003; + + private const int LVCFMT_IMAGE = 0x0800; + private const int LVCFMT_BITMAP_ON_RIGHT = 0x1000; + private const int LVCFMT_COL_HAS_IMAGES = 0x8000; + + private const int LVBKIF_SOURCE_NONE = 0x0; + private const int LVBKIF_SOURCE_HBITMAP = 0x1; + private const int LVBKIF_SOURCE_URL = 0x2; + private const int LVBKIF_SOURCE_MASK = 0x3; + private const int LVBKIF_STYLE_NORMAL = 0x0; + private const int LVBKIF_STYLE_TILE = 0x10; + private const int LVBKIF_STYLE_MASK = 0x10; + private const int LVBKIF_FLAG_TILEOFFSET = 0x100; + private const int LVBKIF_TYPE_WATERMARK = 0x10000000; + private const int LVBKIF_FLAG_ALPHABLEND = 0x20000000; + + private const int LVSICF_NOINVALIDATEALL = 1; + private const int LVSICF_NOSCROLL = 2; + + private const int HDM_FIRST = 0x1200; + private const int HDM_HITTEST = HDM_FIRST + 6; + private const int HDM_GETITEMRECT = HDM_FIRST + 7; + private const int HDM_GETITEM = HDM_FIRST + 11; + private const int HDM_SETITEM = HDM_FIRST + 12; + + private const int HDI_WIDTH = 0x0001; + private const int HDI_TEXT = 0x0002; + private const int HDI_FORMAT = 0x0004; + private const int HDI_BITMAP = 0x0010; + private const int HDI_IMAGE = 0x0020; + + private const int HDF_LEFT = 0x0000; + private const int HDF_RIGHT = 0x0001; + private const int HDF_CENTER = 0x0002; + private const int HDF_JUSTIFYMASK = 0x0003; + private const int HDF_RTLREADING = 0x0004; + private const int HDF_STRING = 0x4000; + private const int HDF_BITMAP = 0x2000; + private const int HDF_BITMAP_ON_RIGHT = 0x1000; + private const int HDF_IMAGE = 0x0800; + private const int HDF_SORTUP = 0x0400; + private const int HDF_SORTDOWN = 0x0200; + + private const int SB_HORZ = 0; + private const int SB_VERT = 1; + private const int SB_CTL = 2; + private const int SB_BOTH = 3; + + private const int SIF_RANGE = 0x0001; + private const int SIF_PAGE = 0x0002; + private const int SIF_POS = 0x0004; + private const int SIF_DISABLENOSCROLL = 0x0008; + private const int SIF_TRACKPOS = 0x0010; + private const int SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS); + + private const int ILD_NORMAL = 0x0; + private const int ILD_TRANSPARENT = 0x1; + private const int ILD_MASK = 0x10; + private const int ILD_IMAGE = 0x20; + private const int ILD_BLEND25 = 0x2; + private const int ILD_BLEND50 = 0x4; + + const int SWP_NOSIZE = 1; + const int SWP_NOMOVE = 2; + const int SWP_NOZORDER = 4; + const int SWP_NOREDRAW = 8; + const int SWP_NOACTIVATE = 16; + public const int SWP_FRAMECHANGED = 32; + + const int SWP_ZORDERONLY = SWP_NOSIZE | SWP_NOMOVE | SWP_NOREDRAW | SWP_NOACTIVATE; + const int SWP_SIZEONLY = SWP_NOMOVE | SWP_NOREDRAW | SWP_NOZORDER | SWP_NOACTIVATE; + const int SWP_UPDATE_FRAME = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED; + + #endregion + + #region Structures + + [StructLayout(LayoutKind.Sequential)] + public struct HDITEM + { + public int mask; + public int cxy; + public IntPtr pszText; + public IntPtr hbm; + public int cchTextMax; + public int fmt; + public IntPtr lParam; + public int iImage; + public int iOrder; + //if (_WIN32_IE >= 0x0500) + public int type; + public IntPtr pvFilter; + } + + [StructLayout(LayoutKind.Sequential)] + public class HDHITTESTINFO + { + public int pt_x; + public int pt_y; + public int flags; + public int iItem; + } + + [StructLayout(LayoutKind.Sequential)] + public class HDLAYOUT + { + public IntPtr prc; + public IntPtr pwpos; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGELISTDRAWPARAMS + { + public int cbSize; + public IntPtr himl; + public int i; + public IntPtr hdcDst; + public int x; + public int y; + public int cx; + public int cy; + public int xBitmap; + public int yBitmap; + public uint rgbBk; + public uint rgbFg; + public uint fStyle; + public uint dwRop; + public uint fState; + public uint Frame; + public uint crEffect; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVBKIMAGE + { + public int ulFlags; + public IntPtr hBmp; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszImage; + public int cchImageMax; + public int xOffset; + public int yOffset; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVCOLUMN + { + public int mask; + public int fmt; + public int cx; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszText; + public int cchTextMax; + public int iSubItem; + // These are available in Common Controls >= 0x0300 + public int iImage; + public int iOrder; + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVFINDINFO + { + public int flags; + public string psz; + public IntPtr lParam; + public int ptX; + public int ptY; + public int vkDirection; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUP + { + public uint cbSize; + public uint mask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszHeader; + public int cchHeader; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszFooter; + public int cchFooter; + public int iGroupId; + public uint stateMask; + public uint state; + public uint uAlign; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUP2 + { + public uint cbSize; + public uint mask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszHeader; + public uint cchHeader; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszFooter; + public int cchFooter; + public int iGroupId; + public uint stateMask; + public uint state; + public uint uAlign; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszSubtitle; + public uint cchSubtitle; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszTask; + public uint cchTask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszDescriptionTop; + public uint cchDescriptionTop; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszDescriptionBottom; + public uint cchDescriptionBottom; + public int iTitleImage; + public int iExtendedImage; + public int iFirstItem; // Read only + public int cItems; // Read only + [MarshalAs(UnmanagedType.LPTStr)] + public string pszSubsetTitle; // NULL if group is not subset + public uint cchSubsetTitle; + } + + [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] + public struct LVGROUPMETRICS + { + public uint cbSize; + public uint mask; + public uint Left; + public uint Top; + public uint Right; + public uint Bottom; + public int crLeft; + public int crTop; + public int crRight; + public int crBottom; + public int crHeader; + public int crFooter; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVHITTESTINFO + { + public int pt_x; + public int pt_y; + public int flags; + public int iItem; + public int iSubItem; + public int iGroup; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct LVITEM + { + public int mask; + public int iItem; + public int iSubItem; + public int state; + public int stateMask; + [MarshalAs(UnmanagedType.LPTStr)] + public string pszText; + public int cchTextMax; + public int iImage; + public IntPtr lParam; + // These are available in Common Controls >= 0x0300 + public int iIndent; + // These are available in Common Controls >= 0x056 + public int iGroupId; + public int cColumns; + public IntPtr puColumns; + }; + + [StructLayout(LayoutKind.Sequential)] + public struct NMHDR + { + public IntPtr hwndFrom; + public IntPtr idFrom; + public int code; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMCUSTOMDRAW + { + public NativeMethods.NMHDR nmcd; + public int dwDrawStage; + public IntPtr hdc; + public NativeMethods.RECT rc; + public IntPtr dwItemSpec; + public int uItemState; + public IntPtr lItemlParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMHEADER + { + public NMHDR nhdr; + public int iItem; + public int iButton; + public IntPtr pHDITEM; + } + + const int MAX_LINKID_TEXT = 48; + const int L_MAX_URL_LENGTH = 2048 + 32 + 4; + //#define L_MAX_URL_LENGTH (2048 + 32 + sizeof("://")) + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct LITEM + { + public uint mask; + public int iLink; + public uint state; + public uint stateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_LINKID_TEXT)] + public string szID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = L_MAX_URL_LENGTH)] + public string szUrl; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLISTVIEW + { + public NativeMethods.NMHDR hdr; + public int iItem; + public int iSubItem; + public int uNewState; + public int uOldState; + public int uChanged; + public IntPtr lParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVCUSTOMDRAW + { + public NativeMethods.NMCUSTOMDRAW nmcd; + public int clrText; + public int clrTextBk; + public int iSubItem; + public int dwItemType; + public int clrFace; + public int iIconEffect; + public int iIconPhase; + public int iPartId; + public int iStateId; + public NativeMethods.RECT rcText; + public uint uAlign; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVFINDITEM + { + public NativeMethods.NMHDR hdr; + public int iStart; + public NativeMethods.LVFINDINFO lvfi; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVGETINFOTIP + { + public NativeMethods.NMHDR hdr; + public int dwFlags; + public string pszText; + public int cchTextMax; + public int iItem; + public int iSubItem; + public IntPtr lParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVGROUP + { + public NMHDR hdr; + public int iGroupId; // which group is changing + public uint uNewState; // LVGS_xxx flags + public uint uOldState; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVLINK + { + public NMHDR hdr; + public LITEM link; + public int iItem; + public int iSubItem; + } + + [StructLayout(LayoutKind.Sequential)] + public struct NMLVSCROLL + { + public NativeMethods.NMHDR hdr; + public int dx; + public int dy; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct NMTTDISPINFO + { + public NativeMethods.NMHDR hdr; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszText; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] + public string szText; + public IntPtr hinst; + public int uFlags; + public IntPtr lParam; + //public int hbmp; This is documented but doesn't work + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential)] + public class SCROLLINFO + { + public int cbSize = Marshal.SizeOf(typeof(NativeMethods.SCROLLINFO)); + public int fMask; + public int nMin; + public int nMax; + public int nPage; + public int nPos; + public int nTrackPos; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class TOOLINFO + { + public int cbSize = Marshal.SizeOf(typeof(NativeMethods.TOOLINFO)); + public int uFlags; + public IntPtr hwnd; + public IntPtr uId; + public NativeMethods.RECT rect; + public IntPtr hinst = IntPtr.Zero; + public IntPtr lpszText; + public IntPtr lParam = IntPtr.Zero; + } + + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWPOS + { + public IntPtr hwnd; + public IntPtr hwndInsertAfter; + public int x; + public int y; + public int cx; + public int cy; + public int flags; + } + + #endregion + + #region Entry points + + // Various flavours of SendMessage: plain vanilla, and passing references to various structures + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageLVItem(IntPtr hWnd, int msg, int wParam, ref LVITEM lvi); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO ht); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageRECT(IntPtr hWnd, int msg, int wParam, ref RECT r); + //[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + //private static extern IntPtr SendMessageLVColumn(IntPtr hWnd, int m, int wParam, ref LVCOLUMN lvc); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessageHDItem(IntPtr hWnd, int msg, int wParam, ref HDITEM hdi); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageHDHITTESTINFO(IntPtr hWnd, int Msg, IntPtr wParam, [In, Out] HDHITTESTINFO lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageTOOLINFO(IntPtr hWnd, int Msg, int wParam, NativeMethods.TOOLINFO lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageLVBKIMAGE(IntPtr hWnd, int Msg, int wParam, ref NativeMethods.LVBKIMAGE lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageString(IntPtr hWnd, int Msg, int wParam, string lParam); + [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageIUnknown(IntPtr hWnd, int msg, [MarshalAs(UnmanagedType.IUnknown)] object wParam, int lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUP2 lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, ref LVGROUPMETRICS lParam); + + [DllImport("gdi32.dll")] + public static extern bool DeleteObject(IntPtr objectHandle); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool GetClientRect(IntPtr hWnd, ref Rectangle r); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool GetScrollInfo(IntPtr hWnd, int fnBar, SCROLLINFO scrollInfo); + + [DllImport("user32.dll", EntryPoint = "GetUpdateRect", CharSet = CharSet.Auto)] + private static extern bool GetUpdateRectInternal(IntPtr hWnd, ref Rectangle r, bool eraseBackground); + + [DllImport("comctl32.dll", CharSet = CharSet.Auto)] + private static extern bool ImageList_Draw(IntPtr himl, int i, IntPtr hdcDst, int x, int y, int fStyle); + + [DllImport("comctl32.dll", CharSet = CharSet.Auto)] + private static extern bool ImageList_DrawIndirect(ref IMAGELISTDRAWPARAMS parms); + + [DllImport("user32.dll")] + public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool GetWindowRect(IntPtr hWnd, ref Rectangle r); + + [DllImport("user32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto)] + public static extern IntPtr GetWindowLong32(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", CharSet = CharSet.Auto)] + public static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)] + public static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)] + public static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", EntryPoint = "ValidateRect", CharSet = CharSet.Auto)] + private static extern IntPtr ValidatedRectInternal(IntPtr hWnd, ref Rectangle r); + + #endregion + + //[DllImport("user32.dll", EntryPoint = "LockWindowUpdate", CharSet = CharSet.Auto)] + //private static extern int LockWindowUpdateInternal(IntPtr hWnd); + + //public static void LockWindowUpdate(IWin32Window window) { + // if (window == null) + // NativeMethods.LockWindowUpdateInternal(IntPtr.Zero); + // else + // NativeMethods.LockWindowUpdateInternal(window.Handle); + //} + + /// + /// Put an image under the ListView. + /// + /// + /// + /// The ListView must have its handle created before calling this. + /// + /// + /// This doesn't work very well. Specifically, it doesn't play well with owner drawn, + /// and grid lines are drawn over it. + /// + /// + /// + /// The image to be used as the background. If this is null, any existing background image will be cleared. + /// If this is true, the image is pinned to the bottom right and does not scroll. The other parameters are ignored + /// If this is true, the image will be tiled to fill the whole control background. The offset parameters will be ignored. + /// If both watermark and tiled are false, this indicates the horizontal percentage where the image will be placed. 0 is absolute left, 100 is absolute right. + /// If both watermark and tiled are false, this indicates the vertical percentage where the image will be placed. + /// + public static bool SetBackgroundImage(ListView lv, Image image, bool isWatermark, bool isTiled, int xOffset, int yOffset) { + + LVBKIMAGE lvbkimage = new LVBKIMAGE(); + + // We have to clear any pre-existing background image, otherwise the attempt to set the image will fail. + // We don't know which type may already have been set, so we just clear both the watermark and the image. + lvbkimage.ulFlags = LVBKIF_TYPE_WATERMARK; + IntPtr result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + lvbkimage.ulFlags = LVBKIF_SOURCE_HBITMAP; + result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + + Bitmap bm = image as Bitmap; + if (bm != null) { + lvbkimage.hBmp = bm.GetHbitmap(); + lvbkimage.ulFlags = isWatermark ? LVBKIF_TYPE_WATERMARK : (isTiled ? LVBKIF_SOURCE_HBITMAP | LVBKIF_STYLE_TILE : LVBKIF_SOURCE_HBITMAP); + lvbkimage.xOffset = xOffset; + lvbkimage.yOffset = yOffset; + result = NativeMethods.SendMessageLVBKIMAGE(lv.Handle, LVM_SETBKIMAGE, 0, ref lvbkimage); + } + + return (result != IntPtr.Zero); + } + + public static bool DrawImageList(Graphics g, ImageList il, int index, int x, int y, bool isSelected, bool isDisabled) { + ImageListDrawItemConstants flags = (isSelected ? ImageListDrawItemConstants.ILD_SELECTED : ImageListDrawItemConstants.ILD_NORMAL) | ImageListDrawItemConstants.ILD_TRANSPARENT; + ImageListDrawStateConstants state = isDisabled ? ImageListDrawStateConstants.ILS_SATURATE : ImageListDrawStateConstants.ILS_NORMAL; + try { + IntPtr hdc = g.GetHdc(); + return DrawImage(il, hdc, index, x, y, flags, 0, 0, state); + } + finally { + g.ReleaseHdc(); + } + } + + /// + /// Flags controlling how the Image List item is + /// drawn + /// + [Flags] + public enum ImageListDrawItemConstants + { + /// + /// Draw item normally. + /// + ILD_NORMAL = 0x0, + /// + /// Draw item transparently. + /// + ILD_TRANSPARENT = 0x1, + /// + /// Draw item blended with 25% of the specified foreground colour + /// or the Highlight colour if no foreground colour specified. + /// + ILD_BLEND25 = 0x2, + /// + /// Draw item blended with 50% of the specified foreground colour + /// or the Highlight colour if no foreground colour specified. + /// + ILD_SELECTED = 0x4, + /// + /// Draw the icon's mask + /// + ILD_MASK = 0x10, + /// + /// Draw the icon image without using the mask + /// + ILD_IMAGE = 0x20, + /// + /// Draw the icon using the ROP specified. + /// + ILD_ROP = 0x40, + /// + /// Preserves the alpha channel in dest. XP only. + /// + ILD_PRESERVEALPHA = 0x1000, + /// + /// Scale the image to cx, cy instead of clipping it. XP only. + /// + ILD_SCALE = 0x2000, + /// + /// Scale the image to the current DPI of the display. XP only. + /// + ILD_DPISCALE = 0x4000 + } + + /// + /// Enumeration containing XP ImageList Draw State options + /// + [Flags] + public enum ImageListDrawStateConstants + { + /// + /// The image state is not modified. + /// + ILS_NORMAL = (0x00000000), + /// + /// Adds a glow effect to the icon, which causes the icon to appear to glow + /// with a given color around the edges. (Note: does not appear to be implemented) + /// + ILS_GLOW = (0x00000001), //The color for the glow effect is passed to the IImageList::Draw method in the crEffect member of IMAGELISTDRAWPARAMS. + /// + /// Adds a drop shadow effect to the icon. (Note: does not appear to be implemented) + /// + ILS_SHADOW = (0x00000002), //The color for the drop shadow effect is passed to the IImageList::Draw method in the crEffect member of IMAGELISTDRAWPARAMS. + /// + /// Saturates the icon by increasing each color component + /// of the RGB triplet for each pixel in the icon. (Note: only ever appears to result in a completely unsaturated icon) + /// + ILS_SATURATE = (0x00000004), // The amount to increase is indicated by the frame member in the IMAGELISTDRAWPARAMS method. + /// + /// Alpha blends the icon. Alpha blending controls the transparency + /// level of an icon, according to the value of its alpha channel. + /// (Note: does not appear to be implemented). + /// + ILS_ALPHA = (0x00000008) //The value of the alpha channel is indicated by the frame member in the IMAGELISTDRAWPARAMS method. The alpha channel can be from 0 to 255, with 0 being completely transparent, and 255 being completely opaque. + } + + private const uint CLR_DEFAULT = 0xFF000000; + + /// + /// Draws an image using the specified flags and state on XP systems. + /// + /// The image list from which an item will be drawn + /// Device context to draw to + /// Index of image to draw + /// X Position to draw at + /// Y Position to draw at + /// Drawing flags + /// Width to draw + /// Height to draw + /// State flags + public static bool DrawImage(ImageList il, IntPtr hdc, int index, int x, int y, ImageListDrawItemConstants flags, int cx, int cy, ImageListDrawStateConstants stateFlags) { + IMAGELISTDRAWPARAMS pimldp = new IMAGELISTDRAWPARAMS(); + pimldp.hdcDst = hdc; + pimldp.cbSize = Marshal.SizeOf(pimldp.GetType()); + pimldp.i = index; + pimldp.x = x; + pimldp.y = y; + pimldp.cx = cx; + pimldp.cy = cy; + pimldp.rgbFg = CLR_DEFAULT; + pimldp.fStyle = (uint) flags; + pimldp.fState = (uint) stateFlags; + pimldp.himl = il.Handle; + return ImageList_DrawIndirect(ref pimldp); + } + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + /// The listview to send a m to + public static void ForceSubItemImagesExStyle(ListView list) { + SendMessage(list.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_SUBITEMIMAGES, LVS_EX_SUBITEMIMAGES); + } + + /// + /// Change the virtual list size of the given ListView (which must be in virtual mode) + /// + /// This will not change the scroll position + /// The listview to send a message to + /// How many rows should the list have? + public static void SetItemCount(ListView list, int count) { + SendMessage(list.Handle, LVM_SETITEMCOUNT, count, LVSICF_NOSCROLL); + } + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + /// The listview to send a m to + /// + /// + public static void SetExtendedStyle(ListView list, int style, int styleMask) { + SendMessage(list.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE, styleMask, style); + } + + /// + /// Calculates the number of items that can fit vertically in the visible area of a list-view (which + /// must be in details or list view. + /// + /// The listView + /// Number of visible items per page + public static int GetCountPerPage(ListView list) { + return (int)SendMessage(list.Handle, LVM_GETCOUNTPERPAGE, 0, 0); + } + /// + /// For the given item and subitem, make it display the given image + /// + /// The listview to send a m to + /// row number (0 based) + /// subitem (0 is the item itself) + /// index into the image list + public static void SetSubItemImage(ListView list, int itemIndex, int subItemIndex, int imageIndex) { + LVITEM lvItem = new LVITEM(); + lvItem.mask = LVIF_IMAGE; + lvItem.iItem = itemIndex; + lvItem.iSubItem = subItemIndex; + lvItem.iImage = imageIndex; + SendMessageLVItem(list.Handle, LVM_SETITEM, 0, ref lvItem); + } + + /// + /// Setup the given column of the listview to show the given image to the right of the text. + /// If the image index is -1, any previous image is cleared + /// + /// The listview to send a m to + /// Index of the column to modifiy + /// + /// Index into the small image list + public static void SetColumnImage(ListView list, int columnIndex, SortOrder order, int imageIndex) { + IntPtr hdrCntl = NativeMethods.GetHeaderControl(list); + if (hdrCntl.ToInt32() == 0) + return; + + HDITEM item = new HDITEM(); + item.mask = HDI_FORMAT; + IntPtr result = SendMessageHDItem(hdrCntl, HDM_GETITEM, columnIndex, ref item); + + item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN | HDF_IMAGE | HDF_BITMAP_ON_RIGHT); + + if (NativeMethods.HasBuiltinSortIndicators()) { + if (order == SortOrder.Ascending) + item.fmt |= HDF_SORTUP; + if (order == SortOrder.Descending) + item.fmt |= HDF_SORTDOWN; + } else { + item.mask |= HDI_IMAGE; + item.fmt |= (HDF_IMAGE | HDF_BITMAP_ON_RIGHT); + item.iImage = imageIndex; + } + + result = SendMessageHDItem(hdrCntl, HDM_SETITEM, columnIndex, ref item); + } + + /// + /// Does this version of the operating system have builtin sort indicators? + /// + /// Are there builtin sort indicators + /// XP and later have these + public static bool HasBuiltinSortIndicators() { + return OSFeature.Feature.GetVersionPresent(OSFeature.Themes) != null; + } + + /// + /// Return the bounds of the update region on the given control. + /// + /// The BeginPaint() system call validates the update region, effectively wiping out this information. + /// So this call has to be made before the BeginPaint() call. + /// The control whose update region is be calculated + /// A rectangle + public static Rectangle GetUpdateRect(Control cntl) { + Rectangle r = new Rectangle(); + GetUpdateRectInternal(cntl.Handle, ref r, false); + return r; + } + + /// + /// Validate an area of the given control. A validated area will not be repainted at the next redraw. + /// + /// The control to be validated + /// The area of the control to be validated + public static void ValidateRect(Control cntl, Rectangle r) { + ValidatedRectInternal(cntl.Handle, ref r); + } + + /// + /// Select all rows on the given listview + /// + /// The listview whose items are to be selected + public static void SelectAllItems(ListView list) { + NativeMethods.SetItemState(list, -1, LVIS_SELECTED, LVIS_SELECTED); + } + + /// + /// Deselect all rows on the given listview + /// + /// The listview whose items are to be deselected + public static void DeselectAllItems(ListView list) { + NativeMethods.SetItemState(list, -1, LVIS_SELECTED, 0); + } + + /// + /// Deselect a single row + /// + /// + /// + public static void DeselectOneItem(ListView list, int index) { + NativeMethods.SetItemState(list, index, LVIS_SELECTED, 0); + } + + /// + /// Set the item state on the given item + /// + /// The listview whose item's state is to be changed + /// The index of the item to be changed + /// Which bits of the value are to be set? + /// The value to be set + public static void SetItemState(ListView list, int itemIndex, int mask, int value) { + LVITEM lvItem = new LVITEM(); + lvItem.stateMask = mask; + lvItem.state = value; + SendMessageLVItem(list.Handle, LVM_SETITEMSTATE, itemIndex, ref lvItem); + } + + /// + /// Scroll the given listview by the given deltas + /// + /// + /// + /// + /// true if the scroll succeeded + public static bool Scroll(ListView list, int dx, int dy) { + return SendMessage(list.Handle, LVM_SCROLL, dx, dy) != IntPtr.Zero; + } + + /// + /// Return the handle to the header control on the given list + /// + /// The listview whose header control is to be returned + /// The handle to the header control + public static IntPtr GetHeaderControl(ListView list) { + return SendMessage(list.Handle, LVM_GETHEADER, 0, 0); + } + + /// + /// Return the edges of the given column. + /// + /// + /// + /// A Point holding the left and right co-ords of the column. + /// -1 means that the sides could not be retrieved. + public static Point GetColumnSides(ObjectListView lv, int columnIndex) { + Point sides = new Point(-1, -1); + IntPtr hdr = NativeMethods.GetHeaderControl(lv); + if (hdr == IntPtr.Zero) + return new Point(-1, -1); + + RECT r = new RECT(); + NativeMethods.SendMessageRECT(hdr, HDM_GETITEMRECT, columnIndex, ref r); + return new Point(r.left, r.right); + } + + /// + /// Return the edges of the given column. + /// + /// + /// + /// A Point holding the left and right co-ords of the column. + /// -1 means that the sides could not be retrieved. + public static Point GetScrolledColumnSides(ListView lv, int columnIndex) { + IntPtr hdr = NativeMethods.GetHeaderControl(lv); + if (hdr == IntPtr.Zero) + return new Point(-1, -1); + + RECT r = new RECT(); + IntPtr result = NativeMethods.SendMessageRECT(hdr, HDM_GETITEMRECT, columnIndex, ref r); + int scrollH = NativeMethods.GetScrollPosition(lv, true); + return new Point(r.left - scrollH, r.right - scrollH); + } + + /// + /// Return the index of the column of the header that is under the given point. + /// Return -1 if no column is under the pt + /// + /// The list we are interested in + /// The client co-ords + /// The index of the column under the point, or -1 if no column header is under that point + public static int GetColumnUnderPoint(IntPtr handle, Point pt) { + const int HHT_ONHEADER = 2; + const int HHT_ONDIVIDER = 4; + return NativeMethods.HeaderControlHitTest(handle, pt, HHT_ONHEADER | HHT_ONDIVIDER); + } + + private static int HeaderControlHitTest(IntPtr handle, Point pt, int flag) { + HDHITTESTINFO testInfo = new HDHITTESTINFO(); + testInfo.pt_x = pt.X; + testInfo.pt_y = pt.Y; + IntPtr result = NativeMethods.SendMessageHDHITTESTINFO(handle, HDM_HITTEST, IntPtr.Zero, testInfo); + if ((testInfo.flags & flag) != 0) + return testInfo.iItem; + else + return -1; + } + + /// + /// Return the index of the divider under the given point. Return -1 if no divider is under the pt + /// + /// The list we are interested in + /// The client co-ords + /// The index of the divider under the point, or -1 if no divider is under that point + public static int GetDividerUnderPoint(IntPtr handle, Point pt) { + const int HHT_ONDIVIDER = 4; + return NativeMethods.HeaderControlHitTest(handle, pt, HHT_ONDIVIDER); + } + + /// + /// Get the scroll position of the given scroll bar + /// + /// + /// + /// + public static int GetScrollPosition(ListView lv, bool horizontalBar) { + int fnBar = (horizontalBar ? SB_HORZ : SB_VERT); + + SCROLLINFO scrollInfo = new SCROLLINFO(); + scrollInfo.fMask = SIF_POS; + if (GetScrollInfo(lv.Handle, fnBar, scrollInfo)) + return scrollInfo.nPos; + else + return -1; + } + + /// + /// Change the z-order to the window 'toBeMoved' so it appear directly on top of 'reference' + /// + /// + /// + /// + public static bool ChangeZOrder(IWin32Window toBeMoved, IWin32Window reference) { + return NativeMethods.SetWindowPos(toBeMoved.Handle, reference.Handle, 0, 0, 0, 0, SWP_ZORDERONLY); + } + + /// + /// Make the given control/window a topmost window + /// + /// + /// + public static bool MakeTopMost(IWin32Window toBeMoved) { + IntPtr HWND_TOPMOST = (IntPtr)(-1); + return NativeMethods.SetWindowPos(toBeMoved.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_ZORDERONLY); + } + + /// + /// Change the size of the window without affecting any other attributes + /// + /// + /// + /// + /// + public static bool ChangeSize(IWin32Window toBeMoved, int width, int height) { + return NativeMethods.SetWindowPos(toBeMoved.Handle, IntPtr.Zero, 0, 0, width, height, SWP_SIZEONLY); + } + + /// + /// Show the given window without activating it + /// + /// The window to show + static public void ShowWithoutActivate(IWin32Window win) { + const int SW_SHOWNA = 8; + NativeMethods.ShowWindow(win.Handle, SW_SHOWNA); + } + + /// + /// Mark the given column as being selected. + /// + /// + /// The OLVColumn or null to clear + /// + /// This method works, but it prevents subitems in the given column from having + /// back colors. + /// + static public void SetSelectedColumn(ListView objectListView, ColumnHeader value) { + NativeMethods.SendMessage(objectListView.Handle, + LVM_SETSELECTEDCOLUMN, (value == null) ? -1 : value.Index, 0); + } + + static public int GetTopIndex(ListView lv) { + return (int)SendMessage(lv.Handle, LVM_GETTOPINDEX, 0, 0); + } + + static public IntPtr GetTooltipControl(ListView lv) { + return SendMessage(lv.Handle, LVM_GETTOOLTIPS, 0, 0); + } + + static public IntPtr SetTooltipControl(ListView lv, ToolTipControl tooltip) { + return SendMessage(lv.Handle, LVM_SETTOOLTIPS, 0, tooltip.Handle); + } + + static public bool HasHorizontalScrollBar(ListView lv) { + const int GWL_STYLE = -16; + const int WS_HSCROLL = 0x00100000; + + return (NativeMethods.GetWindowLong(lv.Handle, GWL_STYLE) & WS_HSCROLL) != 0; + } + + public static int GetWindowLong(IntPtr hWnd, int nIndex) { + if (IntPtr.Size == 4) + return (int)GetWindowLong32(hWnd, nIndex); + else + return (int)(long)GetWindowLongPtr64(hWnd, nIndex); + } + + public static int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong) { + if (IntPtr.Size == 4) + return (int)SetWindowLongPtr32(hWnd, nIndex, dwNewLong); + else + return (int)(long)SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + } + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetBkColor(IntPtr hDC, int clr); + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetTextColor(IntPtr hDC, int crColor); + + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); + + [DllImport("uxtheme.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + public static extern IntPtr SetWindowTheme(IntPtr hWnd, string subApp, string subIdList); + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern bool InvalidateRect(IntPtr hWnd, int ignored, bool erase); + + [StructLayout(LayoutKind.Sequential)] + public struct LVITEMINDEX + { + public int iItem; + public int iGroup; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + public static int GetGroupInfo(ObjectListView olv, int groupId, ref LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_GETGROUPINFO, groupId, ref group); + } + + public static GroupState GetGroupState(ObjectListView olv, int groupId, GroupState mask) { + return (GroupState)NativeMethods.SendMessage(olv.Handle, LVM_GETGROUPSTATE, groupId, (int)mask); + } + + public static int InsertGroup(ObjectListView olv, LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_INSERTGROUP, -1, ref group); + } + + public static int SetGroupInfo(ObjectListView olv, int groupId, LVGROUP2 group) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_SETGROUPINFO, groupId, ref group); + } + + public static int SetGroupMetrics(ObjectListView olv, LVGROUPMETRICS metrics) { + return (int)NativeMethods.SendMessage(olv.Handle, LVM_SETGROUPMETRICS, 0, ref metrics); + } + + public static void ClearGroups(VirtualObjectListView virtualObjectListView) { + NativeMethods.SendMessage(virtualObjectListView.Handle, LVM_REMOVEALLGROUPS, 0, 0); + } + + public static void SetGroupImageList(ObjectListView olv, ImageList il) { + const int LVSIL_GROUPHEADER = 3; + IntPtr handle = IntPtr.Zero; + if (il != null) + handle = il.Handle; + NativeMethods.SendMessage(olv.Handle, LVM_SETIMAGELIST, LVSIL_GROUPHEADER, handle); + } + + public static int HitTest(ObjectListView olv, ref LVHITTESTINFO hittest) + { + return (int)NativeMethods.SendMessage(olv.Handle, olv.View == View.Details ? LVM_SUBITEMHITTEST : LVM_HITTEST, -1, ref hittest); + } + } +} diff --git a/ObjectListView/Implementation/NullableDictionary.cs b/ObjectListView/Implementation/NullableDictionary.cs new file mode 100644 index 0000000..4d8d4d3 --- /dev/null +++ b/ObjectListView/Implementation/NullableDictionary.cs @@ -0,0 +1,87 @@ +/* + * NullableDictionary - A simple Dictionary that can handle null as a key + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; + +namespace BrightIdeasSoftware { + + /// + /// A simple-minded implementation of a Dictionary that can handle null as a key. + /// + /// The type of the dictionary key + /// The type of the values to be stored + /// This is not a full implementation and is only meant to handle + /// collecting groups by their keys, since groups can have null as a key value. + internal class NullableDictionary : Dictionary { + private bool hasNullKey; + private TValue nullValue; + + new public TValue this[TKey key] { + get { + if (key != null) + return base[key]; + + if (this.hasNullKey) + return this.nullValue; + + throw new KeyNotFoundException(); + } + set { + if (key == null) { + this.hasNullKey = true; + this.nullValue = value; + } else + base[key] = value; + } + } + + new public bool ContainsKey(TKey key) { + return key == null ? this.hasNullKey : base.ContainsKey(key); + } + + new public IList Keys { + get { + ArrayList list = new ArrayList(base.Keys); + if (this.hasNullKey) + list.Add(null); + return list; + } + } + + new public IList Values { + get { + List list = new List(base.Values); + if (this.hasNullKey) + list.Add(this.nullValue); + return list; + } + } + } +} diff --git a/ObjectListView/Implementation/OLVListItem.cs b/ObjectListView/Implementation/OLVListItem.cs new file mode 100644 index 0000000..d3373be --- /dev/null +++ b/ObjectListView/Implementation/OLVListItem.cs @@ -0,0 +1,272 @@ +/* + * OLVListItem - A row in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * v2.8 + * 2014-09-27 JPP - Remove faulty caching of CheckState + * 2014-05-06 JPP - Added OLVListItem.Enabled flag + * vOld + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// OLVListItems are specialized ListViewItems that know which row object they came from, + /// and the row index at which they are displayed, even when in group view mode. They + /// also know the image they should draw against themselves + /// + public class OLVListItem : ListViewItem { + #region Constructors + + /// + /// Create a OLVListItem for the given row object + /// + public OLVListItem(object rowObject) { + this.rowObject = rowObject; + } + + /// + /// Create a OLVListItem for the given row object, represented by the given string and image + /// + public OLVListItem(object rowObject, string text, Object image) + : base(text, -1) { + this.rowObject = rowObject; + this.imageSelector = image; + } + + #endregion + + #region Properties + + /// + /// Gets the bounding rectangle of the item, including all subitems + /// + new public Rectangle Bounds { + get { + try { + return base.Bounds; + } + catch (System.ArgumentException) { + // If the item is part of a collapsed group, Bounds will throw an exception + return Rectangle.Empty; + } + } + } + + /// + /// Gets or sets how many pixels will be left blank around each cell of this item + /// + /// This setting only takes effect when the control is owner drawn. + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how the cells of this item will be vertically aligned + /// + /// This setting only takes effect when the control is owner drawn. + public StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets the checkedness of this item. + /// + /// + /// Virtual lists don't handle checkboxes well, so we have to intercept attempts to change them + /// through the items, and change them into something that will work. + /// Unfortuneately, this won't work if this property is set through the base class, since + /// the property is not declared as virtual. + /// + new public bool Checked { + get { + return base.Checked; + } + set { + if (this.Checked != value) { + if (value) + ((ObjectListView)this.ListView).CheckObject(this.RowObject); + else + ((ObjectListView)this.ListView).UncheckObject(this.RowObject); + } + } + } + + /// + /// Enable tri-state checkbox. + /// + /// .NET's Checked property was not built to handle tri-state checkboxes, + /// and will return True for both Checked and Indeterminate states. + public CheckState CheckState { + get { + switch (this.StateImageIndex) { + case 0: + return System.Windows.Forms.CheckState.Unchecked; + case 1: + return System.Windows.Forms.CheckState.Checked; + case 2: + return System.Windows.Forms.CheckState.Indeterminate; + default: + return System.Windows.Forms.CheckState.Unchecked; + } + } + set { + switch (value) { + case System.Windows.Forms.CheckState.Unchecked: + this.StateImageIndex = 0; + break; + case System.Windows.Forms.CheckState.Checked: + this.StateImageIndex = 1; + break; + case System.Windows.Forms.CheckState.Indeterminate: + this.StateImageIndex = 2; + break; + } + } + } + + /// + /// Gets if this item has any decorations set for it. + /// + public bool HasDecoration { + get { + return this.decorations != null && this.decorations.Count > 0; + } + } + + /// + /// Gets or sets the decoration that will be drawn over this item + /// + /// Setting this replaces all other decorations + public IDecoration Decoration { + get { + if (this.HasDecoration) + return this.Decorations[0]; + else + return null; + } + set { + this.Decorations.Clear(); + if (value != null) + this.Decorations.Add(value); + } + } + + /// + /// Gets the collection of decorations that will be drawn over this item + /// + public IList Decorations { + get { + if (this.decorations == null) + this.decorations = new List(); + return this.decorations; + } + } + private IList decorations; + + /// + /// Gets whether or not this row can be selected and activated + /// + public bool Enabled + { + get { return this.enabled; } + internal set { this.enabled = value; } + } + private bool enabled; + + /// + /// Get or set the image that should be shown against this item + /// + /// This can be an Image, a string or an int. A string or an int will + /// be used as an index into the small image list. + public Object ImageSelector { + get { return imageSelector; } + set { + imageSelector = value; + if (value is Int32) + this.ImageIndex = (Int32)value; + else if (value is String) + this.ImageKey = (String)value; + else + this.ImageIndex = -1; + } + } + private Object imageSelector; + + /// + /// Gets or sets the the model object that is source of the data for this list item. + /// + public object RowObject { + get { return rowObject; } + set { rowObject = value; } + } + private object rowObject; + + #endregion + + #region Accessing + + /// + /// Return the sub item at the given index + /// + /// Index of the subitem to be returned + /// An OLVListSubItem + public virtual OLVListSubItem GetSubItem(int index) { + if (index >= 0 && index < this.SubItems.Count) + return (OLVListSubItem)this.SubItems[index]; + + return null; + } + + + /// + /// Return bounds of the given subitem + /// + /// This correctly calculates the bounds even for column 0. + public virtual Rectangle GetSubItemBounds(int subItemIndex) { + if (subItemIndex == 0) { + Rectangle r = this.Bounds; + Point sides = NativeMethods.GetScrolledColumnSides(this.ListView, subItemIndex); + r.X = sides.X + 1; + r.Width = sides.Y - sides.X; + return r; + } + + OLVListSubItem subItem = this.GetSubItem(subItemIndex); + return subItem == null ? new Rectangle() : subItem.Bounds; + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/OLVListSubItem.cs b/ObjectListView/Implementation/OLVListSubItem.cs new file mode 100644 index 0000000..986046d --- /dev/null +++ b/ObjectListView/Implementation/OLVListSubItem.cs @@ -0,0 +1,162 @@ +/* + * OLVListSubItem - A single cell in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using System.ComponentModel; + +namespace BrightIdeasSoftware { + + /// + /// A ListViewSubItem that knows which image should be drawn against it. + /// + [Browsable(false)] + public class OLVListSubItem : ListViewItem.ListViewSubItem { + #region Constructors + + /// + /// Create a OLVListSubItem + /// + public OLVListSubItem() { + } + + /// + /// Create a OLVListSubItem that shows the given string and image + /// + public OLVListSubItem(object modelValue, string text, Object image) { + this.ModelValue = modelValue; + this.Text = text; + this.ImageSelector = image; + } + + #endregion + + #region Properties + + /// + /// Gets or sets how many pixels will be left blank around this cell + /// + /// This setting only takes effect when the control is owner drawn. + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how this cell will be vertically aligned + /// + /// This setting only takes effect when the control is owner drawn. + public StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets the model value is being displayed by this subitem. + /// + public object ModelValue + { + get { return modelValue; } + private set { modelValue = value; } + } + private object modelValue; + + /// + /// Gets if this subitem has any decorations set for it. + /// + public bool HasDecoration { + get { + return this.decorations != null && this.decorations.Count > 0; + } + } + + /// + /// Gets or sets the decoration that will be drawn over this item + /// + /// Setting this replaces all other decorations + public IDecoration Decoration { + get { + return this.HasDecoration ? this.Decorations[0] : null; + } + set { + this.Decorations.Clear(); + if (value != null) + this.Decorations.Add(value); + } + } + + /// + /// Gets the collection of decorations that will be drawn over this item + /// + public IList Decorations { + get { + if (this.decorations == null) + this.decorations = new List(); + return this.decorations; + } + } + private IList decorations; + + /// + /// Get or set the image that should be shown against this item + /// + /// This can be an Image, a string or an int. A string or an int will + /// be used as an index into the small image list. + public Object ImageSelector { + get { return imageSelector; } + set { imageSelector = value; } + } + private Object imageSelector; + + /// + /// Gets or sets the url that should be invoked when this subitem is clicked + /// + public string Url { + get { return this.url; } + set { this.url = value; } + } + private string url; + + #endregion + + #region Implementation Properties + + /// + /// Return the state of the animatation of the image on this subitem. + /// Null means there is either no image, or it is not an animation + /// + internal ImageRenderer.AnimationState AnimationState; + + #endregion + } + +} diff --git a/ObjectListView/Implementation/OlvListViewHitTestInfo.cs b/ObjectListView/Implementation/OlvListViewHitTestInfo.cs new file mode 100644 index 0000000..78e4ec2 --- /dev/null +++ b/ObjectListView/Implementation/OlvListViewHitTestInfo.cs @@ -0,0 +1,383 @@ +/* + * OlvListViewHitTestInfo - All information gathered during a OlvHitTest() operation + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware { + + /// + /// An indication of where a hit was within ObjectListView cell + /// + public enum HitTestLocation { + /// + /// Nowhere + /// + Nothing, + + /// + /// On the text + /// + Text, + + /// + /// On the image + /// + Image, + + /// + /// On the checkbox + /// + CheckBox, + + /// + /// On the expand button (TreeListView) + /// + ExpandButton, + + /// + /// in the cell but not in any more specific location + /// + InCell, + + /// + /// UserDefined location1 (used for custom renderers) + /// + UserDefined, + + /// + /// On the expand/collapse widget of the group + /// + GroupExpander, + + /// + /// Somewhere on a group + /// + Group, + + /// + /// Somewhere in a column header + /// + Header, + + /// + /// Somewhere in a column header checkbox + /// + HeaderCheckBox, + + /// + /// Somewhere in a header divider + /// + HeaderDivider, + } + + /// + /// A collection of ListViewHitTest constants + /// + [Flags] + public enum HitTestLocationEx { + /// + /// + /// + LVHT_NOWHERE = 0x00000001, + /// + /// + /// + LVHT_ONITEMICON = 0x00000002, + /// + /// + /// + LVHT_ONITEMLABEL = 0x00000004, + /// + /// + /// + LVHT_ONITEMSTATEICON = 0x00000008, + /// + /// + /// + LVHT_ONITEM = (LVHT_ONITEMICON | LVHT_ONITEMLABEL | LVHT_ONITEMSTATEICON), + + /// + /// + /// + LVHT_ABOVE = 0x00000008, + /// + /// + /// + LVHT_BELOW = 0x00000010, + /// + /// + /// + LVHT_TORIGHT = 0x00000020, + /// + /// + /// + LVHT_TOLEFT = 0x00000040, + + /// + /// + /// + LVHT_EX_GROUP_HEADER = 0x10000000, + /// + /// + /// + LVHT_EX_GROUP_FOOTER = 0x20000000, + /// + /// + /// + LVHT_EX_GROUP_COLLAPSE = 0x40000000, + /// + /// + /// + LVHT_EX_GROUP_BACKGROUND = -2147483648, // 0x80000000 + /// + /// + /// + LVHT_EX_GROUP_STATEICON = 0x01000000, + /// + /// + /// + LVHT_EX_GROUP_SUBSETLINK = 0x02000000, + /// + /// + /// + LVHT_EX_GROUP = (LVHT_EX_GROUP_BACKGROUND | LVHT_EX_GROUP_COLLAPSE | LVHT_EX_GROUP_FOOTER | LVHT_EX_GROUP_HEADER | LVHT_EX_GROUP_STATEICON | LVHT_EX_GROUP_SUBSETLINK), + /// + /// + /// + LVHT_EX_GROUP_MINUS_FOOTER_AND_BKGRD = (LVHT_EX_GROUP_COLLAPSE | LVHT_EX_GROUP_HEADER | LVHT_EX_GROUP_STATEICON | LVHT_EX_GROUP_SUBSETLINK), + /// + /// + /// + LVHT_EX_ONCONTENTS = 0x04000000, // On item AND not on the background + /// + /// + /// + LVHT_EX_FOOTER = 0x08000000, + } + + /// + /// Instances of this class encapsulate the information gathered during a OlvHitTest() + /// operation. + /// + /// Custom renderers can use HitTestLocation.UserDefined and the UserData + /// object to store more specific locations for use during event handlers. + public class OlvListViewHitTestInfo { + + /// + /// Create a OlvListViewHitTestInfo + /// + public OlvListViewHitTestInfo(OLVListItem olvListItem, OLVListSubItem subItem, int flags, OLVGroup group, int iColumn) + { + this.item = olvListItem; + this.subItem = subItem; + this.location = ConvertNativeFlagsToDotNetLocation(olvListItem, flags); + this.HitTestLocationEx = (HitTestLocationEx)flags; + this.Group = group; + this.ColumnIndex = iColumn; + this.ListView = olvListItem == null ? null : (ObjectListView)olvListItem.ListView; + + switch (location) { + case ListViewHitTestLocations.StateImage: + this.HitTestLocation = HitTestLocation.CheckBox; + break; + case ListViewHitTestLocations.Image: + this.HitTestLocation = HitTestLocation.Image; + break; + case ListViewHitTestLocations.Label: + this.HitTestLocation = HitTestLocation.Text; + break; + default: + if ((this.HitTestLocationEx & HitTestLocationEx.LVHT_EX_GROUP_COLLAPSE) == HitTestLocationEx.LVHT_EX_GROUP_COLLAPSE) + this.HitTestLocation = HitTestLocation.GroupExpander; + else if ((this.HitTestLocationEx & HitTestLocationEx.LVHT_EX_GROUP_MINUS_FOOTER_AND_BKGRD) != 0) + this.HitTestLocation = HitTestLocation.Group; + else + this.HitTestLocation = HitTestLocation.Nothing; + break; + } + } + + /// + /// Create a OlvListViewHitTestInfo when the header was hit + /// + public OlvListViewHitTestInfo(ObjectListView olv, int iColumn, bool isOverCheckBox, int iDivider) { + this.ListView = olv; + this.ColumnIndex = iColumn; + this.HeaderDividerIndex = iDivider; + this.HitTestLocation = isOverCheckBox ? HitTestLocation.HeaderCheckBox : (iDivider < 0 ? HitTestLocation.Header : HitTestLocation.HeaderDivider); + } + + private static ListViewHitTestLocations ConvertNativeFlagsToDotNetLocation(OLVListItem hitItem, int flags) + { + // Untangle base .NET behaviour. + + // In Windows SDK, the value 8 can have two meanings here: LVHT_ONITEMSTATEICON or LVHT_ABOVE. + // .NET changes these to be: + // - LVHT_ABOVE becomes ListViewHitTestLocations.AboveClientArea (which is 0x100). + // - LVHT_ONITEMSTATEICON becomes ListViewHitTestLocations.StateImage (which is 0x200). + // So, if we see the 8 bit set in flags, we change that to either a state image hit + // (if we hit an item) or to AboveClientAream if nothing was hit. + + if ((8 & flags) == 8) + return (ListViewHitTestLocations)(0xf7 & flags | (hitItem == null ? 0x100 : 0x200)); + + // Mask off the LVHT_EX_XXXX values since ListViewHitTestLocations doesn't have them + return (ListViewHitTestLocations)(flags & 0xffff); + } + + #region Public fields + + /// + /// Where is the hit location? + /// + public HitTestLocation HitTestLocation; + + /// + /// Where is the hit location? + /// + public HitTestLocationEx HitTestLocationEx; + + /// + /// Which group was hit? + /// + public OLVGroup Group; + + /// + /// Custom renderers can use this information to supply more details about the hit location + /// + public Object UserData; + + #endregion + + #region Public read-only properties + + /// + /// Gets the item that was hit + /// + public OLVListItem Item { + get { return item; } + internal set { item = value; } + } + private OLVListItem item; + + /// + /// Gets the subitem that was hit + /// + public OLVListSubItem SubItem { + get { return subItem; } + internal set { subItem = value; } + } + private OLVListSubItem subItem; + + /// + /// Gets the part of the subitem that was hit + /// + public ListViewHitTestLocations Location { + get { return location; } + internal set { location = value; } + } + private ListViewHitTestLocations location; + + /// + /// Gets the ObjectListView that was tested + /// + public ObjectListView ListView { + get { return listView; } + internal set { listView = value; } + } + private ObjectListView listView; + + /// + /// Gets the model object that was hit + /// + public Object RowObject { + get { + return this.Item == null ? null : this.Item.RowObject; + } + } + + /// + /// Gets the index of the row under the hit point or -1 + /// + public int RowIndex { + get { return this.Item == null ? -1 : this.Item.Index; } + } + + /// + /// Gets the index of the column under the hit point + /// + public int ColumnIndex { + get { return columnIndex; } + internal set { columnIndex = value; } + } + private int columnIndex; + + /// + /// Gets the index of the header divider + /// + public int HeaderDividerIndex { + get { return headerDividerIndex; } + internal set { headerDividerIndex = value; } + } + private int headerDividerIndex = -1; + + /// + /// Gets the column that was hit + /// + public OLVColumn Column { + get { + int index = this.ColumnIndex; + return index < 0 || this.ListView == null ? null : this.ListView.GetColumn(index); + } + } + + #endregion + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() + { + return string.Format("HitTestLocation: {0}, HitTestLocationEx: {1}, Item: {2}, SubItem: {3}, Location: {4}, Group: {5}, ColumnIndex: {6}", + this.HitTestLocation, this.HitTestLocationEx, this.item, this.subItem, this.location, this.Group, this.ColumnIndex); + } + + internal class HeaderHitTestInfo + { + public int ColumnIndex; + public bool IsOverCheckBox; + public int OverDividerIndex; + } + } +} diff --git a/ObjectListView/Implementation/TreeDataSourceAdapter.cs b/ObjectListView/Implementation/TreeDataSourceAdapter.cs new file mode 100644 index 0000000..4b56d71 --- /dev/null +++ b/ObjectListView/Implementation/TreeDataSourceAdapter.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; + +namespace BrightIdeasSoftware +{ + /// + /// A TreeDataSourceAdapter knows how to build a tree structure from a binding list. + /// + /// To build a tree + public class TreeDataSourceAdapter : DataSourceAdapter + { + #region Life and death + + /// + /// Create a data source adaptor that knows how to build a tree structure + /// + /// + public TreeDataSourceAdapter(DataTreeListView tlv) + : base(tlv) { + this.treeListView = tlv; + this.treeListView.CanExpandGetter = delegate(object model) { return this.CalculateHasChildren(model); }; + this.treeListView.ChildrenGetter = delegate(object model) { return this.CalculateChildren(model); }; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the name of the property/column that uniquely identifies each row. + /// + /// + /// + /// The value contained by this column must be unique across all rows + /// in the data source. Odd and unpredictable things will happen if two + /// rows have the same id. + /// + /// Null cannot be a valid key value. + /// + public virtual string KeyAspectName { + get { return keyAspectName; } + set { + if (keyAspectName == value) + return; + keyAspectName = value; + this.keyMunger = new Munger(this.KeyAspectName); + this.InitializeDataSource(); + } + } + private string keyAspectName; + + /// + /// Gets or sets the name of the property/column that contains the key of + /// the parent of a row. + /// + /// + /// + /// The test condition for deciding if one row is the parent of another is functionally + /// equivilent to this: + /// + /// Object.Equals(candidateParentRow[this.KeyAspectName], row[this.ParentKeyAspectName]) + /// + /// + /// Unlike key value, parent keys can be null but a null parent key can only be used + /// to identify root objects. + /// + public virtual string ParentKeyAspectName { + get { return parentKeyAspectName; } + set { + if (parentKeyAspectName == value) + return; + parentKeyAspectName = value; + this.parentKeyMunger = new Munger(this.ParentKeyAspectName); + this.InitializeDataSource(); + } + } + private string parentKeyAspectName; + + /// + /// Gets or sets the value that identifies a row as a root object. + /// When the ParentKey of a row equals the RootKeyValue, that row will + /// be treated as root of the TreeListView. + /// + /// + /// + /// The test condition for deciding a root object is functionally + /// equivilent to this: + /// + /// Object.Equals(candidateRow[this.ParentKeyAspectName], this.RootKeyValue) + /// + /// + /// The RootKeyValue can be null. + /// + public virtual object RootKeyValue { + get { return rootKeyValue; } + set { + if (Equals(rootKeyValue, value)) + return; + rootKeyValue = value; + this.InitializeDataSource(); + } + } + private object rootKeyValue; + + /// + /// Gets or sets whether or not the key columns (id and parent id) should + /// be shown to the user. + /// + /// This must be set before the DataSource is set. It has no effect + /// afterwards. + public virtual bool ShowKeyColumns { + get { return showKeyColumns; } + set { showKeyColumns = value; } + } + private bool showKeyColumns = true; + + + #endregion + + #region Implementation properties + + /// + /// Gets the DataTreeListView that is being managed + /// + protected DataTreeListView TreeListView { + get { return treeListView; } + } + private readonly DataTreeListView treeListView; + + #endregion + + #region Implementation + + /// + /// + /// + protected override void InitializeDataSource() { + base.InitializeDataSource(); + this.TreeListView.RebuildAll(true); + } + + /// + /// + /// + protected override void SetListContents() { + this.TreeListView.Roots = this.CalculateRoots(); + } + + /// + /// + /// + /// + /// + protected override bool ShouldCreateColumn(PropertyDescriptor property) { + // If the property is a key column, and we aren't supposed to show keys, don't show it + if (!this.ShowKeyColumns && (property.Name == this.KeyAspectName || property.Name == this.ParentKeyAspectName)) + return false; + + return base.ShouldCreateColumn(property); + } + + /// + /// + /// + /// + protected override void HandleListChangedItemChanged(System.ComponentModel.ListChangedEventArgs e) { + // If the id or the parent id of a row changes, we just rebuild everything. + // We can't do anything more specific. We don't know what the previous values, so we can't + // tell the previous parent to refresh itself. If the id itself has changed, things that used + // to be children will no longer be children. Just rebuild everything. + // It seems PropertyDescriptor is only filled in .NET 4 :( + if (e.PropertyDescriptor != null && + (e.PropertyDescriptor.Name == this.KeyAspectName || + e.PropertyDescriptor.Name == this.ParentKeyAspectName)) + this.InitializeDataSource(); + else + base.HandleListChangedItemChanged(e); + } + + /// + /// + /// + /// + protected override void ChangePosition(int index) { + // We can't use our base method directly, since the normal position management + // doesn't know about our tree structure. They treat our dataset as a flat list + // but we have a collapsable structure. This means that the 5'th row to them + // may not even be visible to us + + // To display the n'th row, we have to make sure that all its ancestors + // are expanded. Then we will be able to select it. + object model = this.CurrencyManager.List[index]; + object parent = this.CalculateParent(model); + while (parent != null && !this.TreeListView.IsExpanded(parent)) { + this.TreeListView.Expand(parent); + parent = this.CalculateParent(parent); + } + + base.ChangePosition(index); + } + + private IEnumerable CalculateRoots() { + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(this.RootKeyValue, parentKey)) + yield return x; + } + } + + private bool CalculateHasChildren(object model) { + object keyValue = this.GetKeyValue(model); + if (keyValue == null) + return false; + + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(keyValue, parentKey)) + return true; + } + return false; + } + + private IEnumerable CalculateChildren(object model) { + object keyValue = this.GetKeyValue(model); + if (keyValue != null) { + foreach (object x in this.CurrencyManager.List) { + object parentKey = this.GetParentValue(x); + if (Object.Equals(keyValue, parentKey)) + yield return x; + } + } + } + + private object CalculateParent(object model) { + object parentValue = this.GetParentValue(model); + if (parentValue == null) + return null; + + foreach (object x in this.CurrencyManager.List) { + object key = this.GetKeyValue(x); + if (Object.Equals(parentValue, key)) + return x; + } + return null; + } + + private object GetKeyValue(object model) { + return this.keyMunger == null ? null : this.keyMunger.GetValue(model); + } + + private object GetParentValue(object model) { + return this.parentKeyMunger == null ? null : this.parentKeyMunger.GetValue(model); + } + + #endregion + + private Munger keyMunger; + private Munger parentKeyMunger; + } +} \ No newline at end of file diff --git a/ObjectListView/Implementation/VirtualGroups.cs b/ObjectListView/Implementation/VirtualGroups.cs new file mode 100644 index 0000000..51ca0dd --- /dev/null +++ b/ObjectListView/Implementation/VirtualGroups.cs @@ -0,0 +1,355 @@ +/* + * Virtual groups - Classes and interfaces needed to implement virtual groups + * + * Author: Phillip Piper + * Date: 28/08/2009 11:10am + * + * Change log: + * 2011-02-21 JPP - Correctly honor group comparer and collapsible groups settings + * v2.3 + * 2009-08-28 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// A IVirtualGroups is the interface that a virtual list must implement to support virtual groups + /// + public interface IVirtualGroups + { + /// + /// Return the list of groups that should be shown according to the given parameters + /// + /// + /// + IList GetGroups(GroupingParameters parameters); + + /// + /// Return the index of the item that appears at the given position within the given group. + /// + /// + /// + /// + int GetGroupMember(OLVGroup group, int indexWithinGroup); + + /// + /// Return the index of the group to which the given item belongs + /// + /// + /// + int GetGroup(int itemIndex); + + /// + /// Return the index at which the given item is shown in the given group + /// + /// + /// + /// + int GetIndexWithinGroup(OLVGroup group, int itemIndex); + + /// + /// A hint that the given range of items are going to be required + /// + /// + /// + /// + /// + void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex); + } + + /// + /// This is a safe, do nothing implementation of a grouping strategy + /// + public class AbstractVirtualGroups : IVirtualGroups + { + /// + /// Return the list of groups that should be shown according to the given parameters + /// + /// + /// + public virtual IList GetGroups(GroupingParameters parameters) { + return new List(); + } + + /// + /// Return the index of the item that appears at the given position within the given group. + /// + /// + /// + /// + public virtual int GetGroupMember(OLVGroup group, int indexWithinGroup) { + return -1; + } + + /// + /// Return the index of the group to which the given item belongs + /// + /// + /// + public virtual int GetGroup(int itemIndex) { + return -1; + } + + /// + /// Return the index at which the given item is shown in the given group + /// + /// + /// + /// + public virtual int GetIndexWithinGroup(OLVGroup group, int itemIndex) { + return -1; + } + + /// + /// A hint that the given range of items are going to be required + /// + /// + /// + /// + /// + public virtual void CacheHint(int fromGroupIndex, int fromIndex, int toGroupIndex, int toIndex) { + } + } + + + /// + /// Provides grouping functionality to a FastObjectListView + /// + public class FastListGroupingStrategy : AbstractVirtualGroups + { + /// + /// Create groups for FastListView + /// + /// + /// + public override IList GetGroups(GroupingParameters parmameters) { + + // There is a lot of overlap between this method and ObjectListView.MakeGroups() + // Any changes made here may need to be reflected there + + // This strategy can only be used on FastObjectListViews + FastObjectListView folv = (FastObjectListView)parmameters.ListView; + + // Separate the list view items into groups, using the group key as the descrimanent + int objectCount = 0; + NullableDictionary> map = new NullableDictionary>(); + foreach (object model in folv.FilteredObjects) { + object key = parmameters.GroupByColumn.GetGroupKey(model); + if (!map.ContainsKey(key)) + map[key] = new List(); + map[key].Add(model); + objectCount++; + } + + // Sort the items within each group + // TODO: Give parameters a ModelComparer property + OLVColumn primarySortColumn = parmameters.SortItemsByPrimaryColumn ? parmameters.ListView.GetColumn(0) : parmameters.PrimarySort; + ModelObjectComparer sorter = new ModelObjectComparer(primarySortColumn, parmameters.PrimarySortOrder, + parmameters.SecondarySort, parmameters.SecondarySortOrder); + foreach (object key in map.Keys) { + map[key].Sort(sorter); + } + + // Make a list of the required groups + List groups = new List(); + foreach (object key in map.Keys) { + string title = parmameters.GroupByColumn.ConvertGroupKeyToTitle(key); + if (!String.IsNullOrEmpty(parmameters.TitleFormat)) { + int count = map[key].Count; + string format = (count == 1 ? parmameters.TitleSingularFormat : parmameters.TitleFormat); + try { + title = String.Format(format, title, count); + } catch (FormatException) { + title = "Invalid group format: " + format; + } + } + OLVGroup lvg = new OLVGroup(title); + lvg.Collapsible = folv.HasCollapsibleGroups; + lvg.Key = key; + lvg.SortValue = key as IComparable; + lvg.Contents = map[key].ConvertAll(delegate(object x) { return folv.IndexOf(x); }); + lvg.VirtualItemCount = map[key].Count; + if (parmameters.GroupByColumn.GroupFormatter != null) + parmameters.GroupByColumn.GroupFormatter(lvg, parmameters); + groups.Add(lvg); + } + + // Sort the groups + if (parmameters.GroupByOrder != SortOrder.None) + groups.Sort(parmameters.GroupComparer ?? new OLVGroupComparer(parmameters.GroupByOrder)); + + // Build an array that remembers which group each item belongs to. + this.indexToGroupMap = new List(objectCount); + this.indexToGroupMap.AddRange(new int[objectCount]); + + for (int i = 0; i < groups.Count; i++) { + OLVGroup group = groups[i]; + List members = (List)group.Contents; + foreach (int j in members) + this.indexToGroupMap[j] = i; + } + + return groups; + } + private List indexToGroupMap; + + /// + /// + /// + /// + /// + /// + public override int GetGroupMember(OLVGroup group, int indexWithinGroup) { + return (int)group.Contents[indexWithinGroup]; + } + + /// + /// + /// + /// + /// + public override int GetGroup(int itemIndex) { + return this.indexToGroupMap[itemIndex]; + } + + /// + /// + /// + /// + /// + /// + public override int GetIndexWithinGroup(OLVGroup group, int itemIndex) { + return group.Contents.IndexOf(itemIndex); + } + } + + + /// + /// This is the COM interface that a ListView must be given in order for groups in virtual lists to work. + /// + /// + /// This interface is NOT documented by MS. It was found on Greg Chapell's site. This means that there is + /// no guarantee that it will work on future versions of Windows, nor continue to work on current ones. + /// + [ComImport(), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown), + Guid("44C09D56-8D3B-419D-A462-7B956B105B47")] + internal interface IOwnerDataCallback + { + /// + /// Not sure what this does + /// + /// + /// + void GetItemPosition(int i, out NativeMethods.POINT pt); + + /// + /// Not sure what this does + /// + /// + /// + void SetItemPosition(int t, NativeMethods.POINT pt); + + /// + /// Get the index of the item that occurs at the n'th position of the indicated group. + /// + /// Index of the group + /// Index within the group + /// Index of the item within the whole list + void GetItemInGroup(int groupIndex, int n, out int itemIndex); + + /// + /// Get the index of the group to which the given item belongs + /// + /// Index of the item within the whole list + /// Which occurences of the item is wanted + /// Index of the group + void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex); + + /// + /// Get the number of groups that contain the given item + /// + /// Index of the item within the whole list + /// How many groups does it occur within + void GetItemGroupCount(int itemIndex, out int occurrenceCount); + + /// + /// A hint to prepare any cache for the given range of requests + /// + /// + /// + void OnCacheHint(NativeMethods.LVITEMINDEX i, NativeMethods.LVITEMINDEX j); + } + + /// + /// A default implementation of the IOwnerDataCallback interface + /// + [Guid("6FC61F50-80E8-49b4-B200-3F38D3865ABD")] + internal class OwnerDataCallbackImpl : IOwnerDataCallback + { + public OwnerDataCallbackImpl(VirtualObjectListView olv) { + this.olv = olv; + } + VirtualObjectListView olv; + + #region IOwnerDataCallback Members + + public void GetItemPosition(int i, out NativeMethods.POINT pt) { + //System.Diagnostics.Debug.WriteLine("GetItemPosition"); + throw new NotSupportedException(); + } + + public void SetItemPosition(int t, NativeMethods.POINT pt) { + //System.Diagnostics.Debug.WriteLine("SetItemPosition"); + throw new NotSupportedException(); + } + + public void GetItemInGroup(int groupIndex, int n, out int itemIndex) { + //System.Diagnostics.Debug.WriteLine(String.Format("-> GetItemInGroup({0}, {1})", groupIndex, n)); + itemIndex = this.olv.GroupingStrategy.GetGroupMember(this.olv.OLVGroups[groupIndex], n); + //System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", itemIndex)); + } + + public void GetItemGroup(int itemIndex, int occurrenceCount, out int groupIndex) { + //System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroup({0}, {1})", itemIndex, occurrenceCount)); + groupIndex = this.olv.GroupingStrategy.GetGroup(itemIndex); + //System.Diagnostics.Debug.WriteLine(String.Format("<- {0}", groupIndex)); + } + + public void GetItemGroupCount(int itemIndex, out int occurrenceCount) { + //System.Diagnostics.Debug.WriteLine(String.Format("GetItemGroupCount({0})", itemIndex)); + occurrenceCount = 1; + } + + public void OnCacheHint(NativeMethods.LVITEMINDEX from, NativeMethods.LVITEMINDEX to) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnCacheHint({0}, {1}, {2}, {3})", from.iGroup, from.iItem, to.iGroup, to.iItem)); + this.olv.GroupingStrategy.CacheHint(from.iGroup, from.iItem, to.iGroup, to.iItem); + } + + #endregion + } +} diff --git a/ObjectListView/Implementation/VirtualListDataSource.cs b/ObjectListView/Implementation/VirtualListDataSource.cs new file mode 100644 index 0000000..9c4436f --- /dev/null +++ b/ObjectListView/Implementation/VirtualListDataSource.cs @@ -0,0 +1,334 @@ +/* + * VirtualListDataSource - Encapsulate how data is provided to a virtual list + * + * Author: Phillip Piper + * Date: 28/08/2009 11:10am + * + * Change log: + * v2.4 + * 2010-04-01 JPP - Added IFilterableDataSource + * v2.3 + * 2009-08-28 JPP - Initial version (Separated from VirtualObjectListView.cs) + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A VirtualListDataSource is a complete manner to provide functionality to a virtual list. + /// An object that implements this interface provides a VirtualObjectListView with all the + /// information it needs to be fully functional. + /// + /// Implementors must provide functioning implementations of at least GetObjectCount() + /// and GetNthObject(), otherwise nothing will appear in the list. + public interface IVirtualListDataSource + { + /// + /// Return the object that should be displayed at the n'th row. + /// + /// The index of the row whose object is to be returned. + /// The model object at the n'th row, or null if the fetching was unsuccessful. + Object GetNthObject(int n); + + /// + /// Return the number of rows that should be visible in the virtual list + /// + /// The number of rows the list view should have. + int GetObjectCount(); + + /// + /// Get the index of the row that is showing the given model object + /// + /// The model object sought + /// The index of the row showing the model, or -1 if the object could not be found. + int GetObjectIndex(Object model); + + /// + /// The ListView is about to request the given range of items. Do + /// whatever caching seems appropriate. + /// + /// + /// + void PrepareCache(int first, int last); + + /// + /// Find the first row that "matches" the given text in the given range. + /// + /// The text typed by the user + /// Start searching from this index. This may be greater than the 'to' parameter, + /// in which case the search should descend + /// Do not search beyond this index. This may be less than the 'from' parameter. + /// The column that should be considered when looking for a match. + /// Return the index of row that was matched, or -1 if no match was found + int SearchText(string value, int first, int last, OLVColumn column); + + /// + /// Sort the model objects in the data source. + /// + /// + /// + void Sort(OLVColumn column, SortOrder order); + + //----------------------------------------------------------------------------------- + // Modification commands + // THINK: Should we split these four into a separate interface? + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + void AddObjects(ICollection modelObjects); + + /// + /// Remove all of the given objects from the control + /// + /// Collection of objects to be removed + void RemoveObjects(ICollection modelObjects); + + /// + /// Set the collection of objects that this control will show. + /// + /// + void SetObjects(IEnumerable collection); + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + void UpdateObject(int index, object modelObject); + } + + /// + /// This extension allow virtual lists to filter their contents + /// + public interface IFilterableDataSource + { + /// + /// All subsequent retrievals on this data source should be filtered + /// through the given filters. null means no filtering of that kind. + /// + /// + /// + void ApplyFilters(IModelFilter modelFilter, IListFilter listFilter); + } + + /// + /// A do-nothing implementation of the VirtualListDataSource interface. + /// + public class AbstractVirtualListDataSource : IVirtualListDataSource, IFilterableDataSource + { + /// + /// Creates an AbstractVirtualListDataSource + /// + /// + public AbstractVirtualListDataSource(VirtualObjectListView listView) { + this.listView = listView; + } + + /// + /// The list view that this data source is giving information to. + /// + protected VirtualObjectListView listView; + + /// + /// + /// + /// + /// + public virtual object GetNthObject(int n) { + return null; + } + + /// + /// + /// + /// + public virtual int GetObjectCount() { + return -1; + } + + /// + /// + /// + /// + /// + public virtual int GetObjectIndex(object model) { + return -1; + } + + /// + /// + /// + /// + /// + public virtual void PrepareCache(int from, int to) { + } + + /// + /// + /// + /// + /// + /// + /// + /// + public virtual int SearchText(string value, int first, int last, OLVColumn column) { + return -1; + } + + /// + /// + /// + /// + /// + public virtual void Sort(OLVColumn column, SortOrder order) { + } + + /// + /// + /// + /// + public virtual void AddObjects(ICollection modelObjects) { + } + + /// + /// + /// + /// + public virtual void RemoveObjects(ICollection modelObjects) { + } + + /// + /// + /// + /// + public virtual void SetObjects(IEnumerable collection) { + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public virtual void UpdateObject(int index, object modelObject) { + } + + /// + /// This is a useful default implementation of SearchText method, intended to be called + /// by implementors of IVirtualListDataSource. + /// + /// + /// + /// + /// + /// + /// + static public int DefaultSearchText(string value, int first, int last, OLVColumn column, IVirtualListDataSource source) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(source.GetNthObject(i)); + if (data.StartsWith(value, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(source.GetNthObject(i)); + if (data.StartsWith(value, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + #region IFilterableDataSource Members + + /// + /// + /// + /// + /// + virtual public void ApplyFilters(IModelFilter modelFilter, IListFilter listFilter) { + } + + #endregion + } + + /// + /// This class mimics the behavior of VirtualObjectListView v1.x. + /// + public class VirtualListVersion1DataSource : AbstractVirtualListDataSource + { + /// + /// Creates a VirtualListVersion1DataSource + /// + /// + public VirtualListVersion1DataSource(VirtualObjectListView listView) + : base(listView) { + } + + #region Public properties + + /// + /// How will the n'th object of the data source be fetched? + /// + public RowGetterDelegate RowGetter { + get { return rowGetter; } + set { rowGetter = value; } + } + private RowGetterDelegate rowGetter; + + #endregion + + #region IVirtualListDataSource implementation + + /// + /// + /// + /// + /// + public override object GetNthObject(int n) { + if (this.RowGetter == null) + return null; + else + return this.RowGetter(n); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public override int SearchText(string value, int first, int last, OLVColumn column) { + return DefaultSearchText(value, first, last, column, this); + } + + #endregion + } +} diff --git a/ObjectListView/OLVColumn.cs b/ObjectListView/OLVColumn.cs new file mode 100644 index 0000000..96e7e37 --- /dev/null +++ b/ObjectListView/OLVColumn.cs @@ -0,0 +1,1683 @@ +/* + * OLVColumn - A column in an ObjectListView + * + * Author: Phillip Piper + * Date: 31-March-2011 5:53 pm + * + * Change log: + * 2014-09-07 JPP - Added ability to have checkboxes in headers + * + * 2011-05-27 JPP - Added Sortable, Hideable, Groupable, Searchable, ShowTextInHeader properties + * 2011-04-12 JPP - Added HasFilterIndicator + * 2011-03-31 JPP - Split into its own file + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.ComponentModel; +using System.Windows.Forms; +using System.Drawing; +using System.Collections; +using System.Diagnostics; + +namespace BrightIdeasSoftware { + + // TODO + //[TypeConverter(typeof(ExpandableObjectConverter))] + //public class CheckBoxSettings + //{ + // private bool useSettings; + // private Image checkedImage; + + // public bool UseSettings { + // get { return useSettings; } + // set { useSettings = value; } + // } + + // public Image CheckedImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + + // public Image UncheckedImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + + // public Image IndeterminateImage { + // get { return checkedImage; } + // set { checkedImage = value; } + // } + //} + + /// + /// An OLVColumn knows which aspect of an object it should present. + /// + /// + /// The column knows how to: + /// + /// extract its aspect from the row object + /// convert an aspect to a string + /// calculate the image for the row object + /// extract a group "key" from the row object + /// convert a group "key" into a title for the group + /// + /// For sorting to work correctly, aspects from the same column + /// must be of the same type, that is, the same aspect cannot sometimes + /// return strings and other times integers. + /// + [Browsable(false)] + public partial class OLVColumn : ColumnHeader { + + #region Life and death + + /// + /// Create an OLVColumn + /// + public OLVColumn() { + } + + /// + /// Initialize a column to have the given title, and show the given aspect + /// + /// The title of the column + /// The aspect to be shown in the column + public OLVColumn(string title, string aspect) + : this() { + this.Text = title; + this.AspectName = aspect; + } + + #endregion + + #region Public Properties + + /// + /// This delegate will be used to extract a value to be displayed in this column. + /// + /// + /// If this is set, AspectName is ignored. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectGetterDelegate AspectGetter { + get { return aspectGetter; } + set { aspectGetter = value; } + } + private AspectGetterDelegate aspectGetter; + + /// + /// Remember if this aspect getter for this column was generated internally, and can therefore + /// be regenerated at will + /// + [Obsolete("This property is no longer maintained", true), + Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool AspectGetterAutoGenerated { + get { return aspectGetterAutoGenerated; } + set { aspectGetterAutoGenerated = value; } + } + private bool aspectGetterAutoGenerated; + + /// + /// The name of the property or method that should be called to get the value to display in this column. + /// This is only used if a ValueGetterDelegate has not been given. + /// + /// This name can be dotted to chain references to properties or parameter-less methods. + /// "DateOfBirth" + /// "Owner.HomeAddress.Postcode" + [Category("ObjectListView"), + Description("The name of the property or method that should be called to get the aspect to display in this column"), + DefaultValue(null)] + public string AspectName { + get { return aspectName; } + set { + aspectName = value; + this.aspectMunger = null; + } + } + private string aspectName; + /// + /// Format that will be used when copying data to other text-based programs + /// + [Category("ObjectListView"), + Description("Format that will be used when copying data to other text-based programs"), + DefaultValue(null)] + public string TextCopyFormat { + get { return textCopyFormat; } + set { textCopyFormat = value; } + } + private string textCopyFormat; + + /// + /// This delegate will be used to put an edited value back into the model object. + /// + /// + /// This does nothing if IsEditable == false. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectPutterDelegate AspectPutter { + get { return aspectPutter; } + set { aspectPutter = value; } + } + private AspectPutterDelegate aspectPutter; + + /// + /// The delegate that will be used to translate the aspect to display in this column into a string. + /// + /// If this value is set, AspectToStringFormat will be ignored. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public AspectToStringConverterDelegate AspectToStringConverter { + get { return aspectToStringConverter; } + set { aspectToStringConverter = value; } + } + private AspectToStringConverterDelegate aspectToStringConverter; + + /// + /// This format string will be used to convert an aspect to its string representation. + /// + /// + /// This string is passed as the first parameter to the String.Format() method. + /// This is only used if AspectToStringConverter has not been set. + /// "{0:C}" to convert a number to currency + [Category("ObjectListView"), + Description("The format string that will be used to convert an aspect to its string representation"), + DefaultValue(null)] + public string AspectToStringFormat { + get { return aspectToStringFormat; } + set { aspectToStringFormat = value; } + } + private string aspectToStringFormat; + + /// + /// Gets or sets whether the cell editor should use AutoComplete + /// + [Category("ObjectListView"), + Description("Should the editor for cells of this column use AutoComplete"), + DefaultValue(true)] + public bool AutoCompleteEditor { + get { return this.AutoCompleteEditorMode != AutoCompleteMode.None; } + set { + if (value) { + if (this.AutoCompleteEditorMode == AutoCompleteMode.None) + this.AutoCompleteEditorMode = AutoCompleteMode.Append; + } else + this.AutoCompleteEditorMode = AutoCompleteMode.None; + } + } + + /// + /// Gets or sets whether the cell editor should use AutoComplete + /// + [Category("ObjectListView"), + Description("Should the editor for cells of this column use AutoComplete"), + DefaultValue(AutoCompleteMode.Append)] + public AutoCompleteMode AutoCompleteEditorMode { + get { return autoCompleteEditorMode; } + set { autoCompleteEditorMode = value; } + } + private AutoCompleteMode autoCompleteEditorMode = AutoCompleteMode.Append; + + /// + /// Gets whether this column can be hidden by user actions + /// + /// This take into account both the Hideable property and whether this column + /// is the primary column of the listview (column 0). + [Browsable(false)] + public bool CanBeHidden { + get { + return this.Hideable && (this.Index != 0); + } + } + + /// + /// Gets or sets how many pixels will be left blank around this cells in this column + /// + /// This setting only takes effect when the control is owner drawn. + [Category("ObjectListView"), + Description("How many pixels will be left blank around the cells in this column?"), + DefaultValue(null)] + public Rectangle? CellPadding + { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how cells in this column will be vertically aligned. + /// + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// + /// If this is not set, the value from the control itself will be used. + /// + /// + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(null)] + public virtual StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets or sets whether this column will show a checkbox. + /// + /// + /// Setting this on column 0 has no effect. Column 0 check box is controlled + /// by the CheckBoxes property on the ObjectListView itself. + /// + [Category("ObjectListView"), + Description("Should values in this column be treated as a checkbox, rather than a string?"), + DefaultValue(false)] + public virtual bool CheckBoxes { + get { return checkBoxes; } + set { + if (this.checkBoxes == value) + return; + + this.checkBoxes = value; + if (this.checkBoxes) { + if (this.Renderer == null) + this.Renderer = new CheckStateRenderer(); + } else { + if (this.Renderer is CheckStateRenderer) + this.Renderer = null; + } + } + } + private bool checkBoxes; + + /// + /// Gets or sets the clustering strategy used for this column. + /// + /// + /// + /// The clustering strategy is used to build a Filtering menu for this item. + /// If this is null, a useful default will be chosen. + /// + /// + /// To disable filtering on this colummn, set UseFiltering to false. + /// + /// + /// Cluster strategies belong to a particular column. The same instance + /// cannot be shared between multiple columns. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IClusteringStrategy ClusteringStrategy { + get { + if (this.clusteringStrategy == null) + this.ClusteringStrategy = this.DecideDefaultClusteringStrategy(); + return clusteringStrategy; + } + set { + this.clusteringStrategy = value; + if (this.clusteringStrategy != null) + this.clusteringStrategy.Column = this; + } + } + private IClusteringStrategy clusteringStrategy; + + /// + /// Should this column resize to fill the free space in the listview? + /// + /// + /// + /// If you want two (or more) columns to equally share the available free space, set this property to True. + /// If you want this column to have a larger or smaller share of the free space, you must + /// set the FreeSpaceProportion property explicitly. + /// + /// + /// Space filling columns are still governed by the MinimumWidth and MaximumWidth properties. + /// + /// /// + [Category("ObjectListView"), + Description("Will this column resize to fill unoccupied horizontal space in the listview?"), + DefaultValue(false)] + public bool FillsFreeSpace { + get { return this.FreeSpaceProportion > 0; } + set { this.FreeSpaceProportion = value ? 1 : 0; } + } + + /// + /// What proportion of the unoccupied horizontal space in the control should be given to this column? + /// + /// + /// + /// There are situations where it would be nice if a column (normally the rightmost one) would expand as + /// the list view expands, so that as much of the column was visible as possible without having to scroll + /// horizontally (you should never, ever make your users have to scroll anything horizontally!). + /// + /// + /// A space filling column is resized to occupy a proportion of the unoccupied width of the listview (the + /// unoccupied width is the width left over once all the the non-filling columns have been given their space). + /// This property indicates the relative proportion of that unoccupied space that will be given to this column. + /// The actual value of this property is not important -- only its value relative to the value in other columns. + /// For example: + /// + /// + /// If there is only one space filling column, it will be given all the free space, regardless of the value in FreeSpaceProportion. + /// + /// + /// If there are two or more space filling columns and they all have the same value for FreeSpaceProportion, + /// they will share the free space equally. + /// + /// + /// If there are three space filling columns with values of 3, 2, and 1 + /// for FreeSpaceProportion, then the first column with occupy half the free space, the second will + /// occupy one-third of the free space, and the third column one-sixth of the free space. + /// + /// + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int FreeSpaceProportion { + get { return freeSpaceProportion; } + set { freeSpaceProportion = Math.Max(0, value); } + } + private int freeSpaceProportion; + + /// + /// Gets or sets whether groups will be rebuild on this columns values when this column's header is clicked. + /// + /// + /// This setting is only used when ShowGroups is true. + /// + /// If this is false, clicking the header will not rebuild groups. It will not provide + /// any feedback as to why the list is not being regrouped. It is the programmers responsibility to + /// provide appropriate feedback. + /// + /// When this is false, BeforeCreatingGroups events are still fired, which can be used to allow grouping + /// or give feedback, on a case by case basis. + /// + [Category("ObjectListView"), + Description("Will the list create groups when this header is clicked?"), + DefaultValue(true)] + public bool Groupable { + get { return groupable; } + set { groupable = value; } + } + private bool groupable = true; + + /// + /// This delegate is called when a group has been created but not yet made + /// into a real ListViewGroup. The user can take this opportunity to fill + /// in lots of other details about the group. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupFormatterDelegate GroupFormatter { + get { return groupFormatter; } + set { groupFormatter = value; } + } + private GroupFormatterDelegate groupFormatter; + + /// + /// This delegate is called to get the object that is the key for the group + /// to which the given row belongs. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupKeyGetterDelegate GroupKeyGetter { + get { return groupKeyGetter; } + set { groupKeyGetter = value; } + } + private GroupKeyGetterDelegate groupKeyGetter; + + /// + /// This delegate is called to convert a group key into a title for that group. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public GroupKeyToTitleConverterDelegate GroupKeyToTitleConverter { + get { return groupKeyToTitleConverter; } + set { groupKeyToTitleConverter = value; } + } + private GroupKeyToTitleConverterDelegate groupKeyToTitleConverter; + + /// + /// When the listview is grouped by this column and group title has an item count, + /// how should the lable be formatted? + /// + /// + /// The given format string can/should have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group + /// + /// + /// "{0} [{1} items]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// Gets this.GroupWithItemCountFormat or a reasonable default + /// + /// + /// If GroupWithItemCountFormat is not set, its value will be taken from the ObjectListView if possible. + /// + [Browsable(false)] + public string GroupWithItemCountFormatOrDefault { + get { + if (!String.IsNullOrEmpty(this.GroupWithItemCountFormat)) + return this.GroupWithItemCountFormat; + + if (this.ListView != null) { + cachedGroupWithItemCountFormat = ((ObjectListView)this.ListView).GroupWithItemCountFormatOrDefault; + return cachedGroupWithItemCountFormat; + } + + // There is one rare but pathelogically possible case where the ListView can + // be null (if the column is grouping a ListView, but is not one of the columns + // for that ListView) so we have to provide a workable default for that rare case. + return cachedGroupWithItemCountFormat ?? "{0} [{1} items]"; + } + } + private string cachedGroupWithItemCountFormat; + + /// + /// When the listview is grouped by this column and a group title has an item count, + /// how should the lable be formatted if there is only one item in the group? + /// + /// + /// The given format string can/should have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group (always 1) + /// + /// + /// "{0} [{1} item]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// Get this.GroupWithItemCountSingularFormat or a reasonable default + /// + /// + /// If this value is not set, the values from the list view will be used + /// + [Browsable(false)] + public string GroupWithItemCountSingularFormatOrDefault { + get { + if (!String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat)) + return this.GroupWithItemCountSingularFormat; + + if (this.ListView != null) { + cachedGroupWithItemCountSingularFormat = ((ObjectListView)this.ListView).GroupWithItemCountSingularFormatOrDefault; + return cachedGroupWithItemCountSingularFormat; + } + + // There is one rare but pathelogically possible case where the ListView can + // be null (if the column is grouping a ListView, but is not one of the columns + // for that ListView) so we have to provide a workable default for that rare case. + return cachedGroupWithItemCountSingularFormat ?? "{0} [{1} item]"; + } + } + private string cachedGroupWithItemCountSingularFormat; + + /// + /// Gets whether this column should be drawn with a filter indicator in the column header. + /// + [Browsable(false)] + public bool HasFilterIndicator { + get { + return this.UseFiltering && this.ValuesChosenForFiltering != null && this.ValuesChosenForFiltering.Count > 0; + } + } + + /// + /// Gets or sets a delegate that will be used to own draw header column. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public HeaderDrawingDelegate HeaderDrawing { + get { return headerDrawing; } + set { headerDrawing = value; } + } + private HeaderDrawingDelegate headerDrawing; + + /// + /// Gets or sets the style that will be used to draw the header for this column + /// + /// This is only uses when the owning ObjectListView has HeaderUsesThemes set to false. + [Category("ObjectListView"), + Description("What style will be used to draw the header of this column"), + DefaultValue(null)] + public HeaderFormatStyle HeaderFormatStyle { + get { return this.headerFormatStyle; } + set { this.headerFormatStyle = value; } + } + private HeaderFormatStyle headerFormatStyle; + + /// + /// Gets or sets the font in which the header for this column will be drawn + /// + /// You should probably use a HeaderFormatStyle instead of this property + /// This is only uses when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("How will the header text be aligned?"), + DefaultValue(null)] + public Font HeaderFont { + get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } + set { + if (value == null && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetFont(value); + } + } + + /// + /// Gets or sets the color in which the text of the header for this column will be drawn + /// + /// You should probably use a HeaderFormatStyle instead of this property + /// This is only uses when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("How will the header text be aligned?"), + DefaultValue(typeof(Color), "")] + public Color HeaderForeColor { + get { return this.HeaderFormatStyle == null ? Color.Empty : this.HeaderFormatStyle.Normal.ForeColor; } + set { + if (value.IsEmpty && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetForeColor(value); + } + } + + /// + /// Gets or sets whether the text values in this column will act like hyperlinks + /// + /// This is only taken into account when HeaderUsesThemes is false. + [Category("ObjectListView"), + Description("Name of the image that will be shown in the column header."), + DefaultValue(null), + TypeConverter(typeof(ImageKeyConverter)), + Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(System.Drawing.Design.UITypeEditor)), + RefreshProperties(RefreshProperties.Repaint)] + public string HeaderImageKey { + get { return headerImageKey; } + set { headerImageKey = value; } + } + private string headerImageKey; + + + /// + /// Gets or sets how the text of the header will be drawn? + /// + [Category("ObjectListView"), + Description("How will the header text be aligned?"), + DefaultValue(HorizontalAlignment.Left)] + public HorizontalAlignment HeaderTextAlign { + get { return headerTextAlign.HasValue ? headerTextAlign.Value : this.TextAlign; } + set { headerTextAlign = value; } + } + private HorizontalAlignment? headerTextAlign; + + /// + /// Gets the header alignment converted to a StringAlignment + /// + [Browsable(false)] + public StringAlignment HeaderTextAlignAsStringAlignment { + get { + switch (this.HeaderTextAlign) { + case HorizontalAlignment.Left: return StringAlignment.Near; + case HorizontalAlignment.Center: return StringAlignment.Center; + case HorizontalAlignment.Right: return StringAlignment.Far; + default: return StringAlignment.Near; + } + } + } + + /// + /// Gets whether or not this column has an image in the header + /// + [Browsable(false)] + public bool HasHeaderImage { + get { + return (this.ListView != null && + this.ListView.SmallImageList != null && + this.ListView.SmallImageList.Images.ContainsKey(this.HeaderImageKey)); + } + } + + /// + /// Gets or sets whether this header will place a checkbox in the header + /// + [Category("ObjectListView"), + Description("Draw a checkbox in the header of this column"), + DefaultValue(false)] + public bool HeaderCheckBox + { + get { return headerCheckBox; } + set { headerCheckBox = value; } + } + private bool headerCheckBox; + + /// + /// Gets or sets whether this header will place a tri-state checkbox in the header + /// + [Category("ObjectListView"), + Description("Draw a tri-state checkbox in the header of this column"), + DefaultValue(false)] + public bool HeaderTriStateCheckBox + { + get { return headerTriStateCheckBox; } + set { headerTriStateCheckBox = value; } + } + private bool headerTriStateCheckBox; + + /// + /// Gets or sets the checkedness of the checkbox in the header of this column + /// + [Category("ObjectListView"), + Description("Checkedness of the header checkbox"), + DefaultValue(CheckState.Unchecked)] + public CheckState HeaderCheckState + { + get { return headerCheckState; } + set { headerCheckState = value; } + } + private CheckState headerCheckState = CheckState.Unchecked; + + /// + /// Gets or sets whether the + /// checking/unchecking the value of the header's checkbox will result in the + /// checkboxes for all cells in this column being set to the same checked/unchecked. + /// Defaults to true. + /// + /// + /// + /// There is no reverse of this function that automatically updates the header when the + /// checkedness of a cell changes. + /// + /// + /// This property's behaviour on a TreeListView is probably best describes as undefined + /// and should be avoided. + /// + /// + /// The performance of this action (checking/unchecking all rows) is O(n) where n is the + /// number of rows. It will work on large virtual lists, but it may take some time. + /// + /// + [Category("ObjectListView"), + Description("Update row checkboxs when the header checkbox is clicked by the user"), + DefaultValue(true)] + public bool HeaderCheckBoxUpdatesRowCheckBoxes { + get { return headerCheckBoxUpdatesRowCheckBoxes; } + set { headerCheckBoxUpdatesRowCheckBoxes = value; } + } + private bool headerCheckBoxUpdatesRowCheckBoxes = true; + + /// + /// Gets or sets whether the checkbox in the header is disabled + /// + /// + /// Clicking on a disabled checkbox does not change its value, though it does raise + /// a HeaderCheckBoxChanging event, which allows the programmer the opportunity to do + /// something appropriate. + [Category("ObjectListView"), + Description("Is the checkbox in the header of this column disabled"), + DefaultValue(false)] + public bool HeaderCheckBoxDisabled + { + get { return headerCheckBoxDisabled; } + set { headerCheckBoxDisabled = value; } + } + private bool headerCheckBoxDisabled; + + /// + /// Gets or sets whether this column can be hidden by the user. + /// + /// + /// Column 0 can never be hidden, regardless of this setting. + /// + [Category("ObjectListView"), + Description("Will the user be able to choose to hide this column?"), + DefaultValue(true)] + public bool Hideable { + get { return hideable; } + set { hideable = value; } + } + private bool hideable = true; + + /// + /// Gets or sets whether the text values in this column will act like hyperlinks + /// + [Category("ObjectListView"), + Description("Will the text values in the cells of this column act like hyperlinks?"), + DefaultValue(false)] + public bool Hyperlink { + get { return hyperlink; } + set { hyperlink = value; } + } + private bool hyperlink; + + /// + /// This is the name of property that will be invoked to get the image selector of the + /// image that should be shown in this column. + /// It can return an int, string, Image or null. + /// + /// + /// This is ignored if ImageGetter is not null. + /// The property can use these return value to identify the image: + /// + /// null or -1 -- indicates no image + /// an int -- the int value will be used as an index into the image list + /// a String -- the string value will be used as a key into the image list + /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) + /// + /// + [Category("ObjectListView"), + Description("The name of the property that holds the image selector"), + DefaultValue(null)] + public string ImageAspectName { + get { return imageAspectName; } + set { imageAspectName = value; } + } + private string imageAspectName; + + /// + /// This delegate is called to get the image selector of the image that should be shown in this column. + /// It can return an int, string, Image or null. + /// + /// This delegate can use these return value to identify the image: + /// + /// null or -1 -- indicates no image + /// an int -- the int value will be used as an index into the image list + /// a String -- the string value will be used as a key into the image list + /// an Image -- the Image will be drawn directly (only in OwnerDrawn mode) + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ImageGetterDelegate ImageGetter { + get { return imageGetter; } + set { imageGetter = value; } + } + private ImageGetterDelegate imageGetter; + + /// + /// Can the values shown in this column be edited? + /// + /// This defaults to true, since the primary means to control the editability of a listview + /// is on the listview itself. Once a listview is editable, all the columns are too, unless the + /// programmer explicitly marks them as not editable + [Category("ObjectListView"), + Description("Can the value in this column be edited?"), + DefaultValue(true)] + public bool IsEditable { + get { return isEditable; } + set { isEditable = value; } + } + private bool isEditable = true; + + /// + /// Is this column a fixed width column? + /// + [Browsable(false)] + public bool IsFixedWidth { + get { + return (this.MinimumWidth != -1 && this.MaximumWidth != -1 && this.MinimumWidth >= this.MaximumWidth); + } + } + + /// + /// Get/set whether this column should be used when the view is switched to tile view. + /// + /// Column 0 is always included in tileview regardless of this setting. + /// Tile views do not work well with many "columns" of information. + /// Two or three works best. + [Category("ObjectListView"), + Description("Will this column be used when the view is switched to tile view"), + DefaultValue(false)] + public bool IsTileViewColumn { + get { return isTileViewColumn; } + set { isTileViewColumn = value; } + } + private bool isTileViewColumn; + + /// + /// Gets or sets whether the text of this header should be rendered vertically. + /// + /// + /// If this is true, it is a good idea to set ToolTipText to the name of the column so it's easy to read. + /// Vertical headers are text only. They do not draw their image. + /// + [Category("ObjectListView"), + Description("Will the header for this column be drawn vertically?"), + DefaultValue(false)] + public bool IsHeaderVertical { + get { return isHeaderVertical; } + set { isHeaderVertical = value; } + } + private bool isHeaderVertical; + + /// + /// Can this column be seen by the user? + /// + /// After changing this value, you must call RebuildColumns() before the changes will take effect. + [Category("ObjectListView"), + Description("Can this column be seen by the user?"), + DefaultValue(true)] + public bool IsVisible { + get { return isVisible; } + set + { + if (isVisible == value) + return; + + isVisible = value; + OnVisibilityChanged(EventArgs.Empty); + } + } + private bool isVisible = true; + + /// + /// Where was this column last positioned within the Detail view columns + /// + /// DisplayIndex is volatile. Once a column is removed from the control, + /// there is no way to discover where it was in the display order. This property + /// guards that information even when the column is not in the listview's active columns. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int LastDisplayIndex { + get { return this.lastDisplayIndex; } + set { this.lastDisplayIndex = value; } + } + private int lastDisplayIndex = -1; + + /// + /// What is the maximum width that the user can give to this column? + /// + /// -1 means there is no maximum width. Give this the same value as MinimumWidth to make a fixed width column. + [Category("ObjectListView"), + Description("What is the maximum width to which the user can resize this column?"), + DefaultValue(-1)] + public int MaximumWidth { + get { return maxWidth; } + set { + maxWidth = value; + if (maxWidth != -1 && this.Width > maxWidth) + this.Width = maxWidth; + } + } + private int maxWidth = -1; + + /// + /// What is the minimum width that the user can give to this column? + /// + /// -1 means there is no minimum width. Give this the same value as MaximumWidth to make a fixed width column. + [Category("ObjectListView"), + Description("What is the minimum width to which the user can resize this column?"), + DefaultValue(-1)] + public int MinimumWidth { + get { return minWidth; } + set { + minWidth = value; + if (this.Width < minWidth) + this.Width = minWidth; + } + } + private int minWidth = -1; + + /// + /// Get/set the renderer that will be invoked when a cell needs to be redrawn + /// + [Category("ObjectListView"), + Description("The renderer will draw this column when the ListView is owner drawn"), + DefaultValue(null)] + public IRenderer Renderer { + get { return renderer; } + set { renderer = value; } + } + private IRenderer renderer; + + /// + /// This delegate is called when a cell needs to be drawn in OwnerDrawn mode. + /// + /// This method is kept primarily for backwards compatibility. + /// New code should implement an IRenderer, though this property will be maintained. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public RenderDelegate RendererDelegate { + get { + Version1Renderer version1Renderer = this.Renderer as Version1Renderer; + return version1Renderer != null ? version1Renderer.RenderDelegate : null; + } + set { + this.Renderer = value == null ? null : new Version1Renderer(value); + } + } + + /// + /// Gets or sets whether the text in this column's cell will be used when doing text searching. + /// + /// + /// + /// If this is false, text filters will not trying searching this columns cells when looking for matches. + /// + /// + [Category("ObjectListView"), + Description("Will the text of the cells in this column be considered when searching?"), + DefaultValue(true)] + public bool Searchable { + get { return searchable; } + set { searchable = value; } + } + private bool searchable = true; + + /// + /// Gets or sets whether the header for this column will include the column's Text. + /// + /// + /// + /// If this is false, the only thing rendered in the column header will be the image from . + /// + /// This setting is only considered when is false on the owning ObjectListView. + /// + [Category("ObjectListView"), + Description("Will the header for this column include text?"), + DefaultValue(true)] + public bool ShowTextInHeader { + get { return showTextInHeader; } + set { showTextInHeader = value; } + } + private bool showTextInHeader = true; + + /// + /// Gets or sets whether the contents of the list will be resorted when the user clicks the + /// header of this column. + /// + /// + /// + /// If this is false, clicking the header will not sort the list, but will not provide + /// any feedback as to why the list is not being sorted. It is the programmers responsibility to + /// provide appropriate feedback. + /// + /// When this is false, BeforeSorting events are still fired, which can be used to allow sorting + /// or give feedback, on a case by case basis. + /// + [Category("ObjectListView"), + Description("Will clicking this columns header resort the list?"), + DefaultValue(true)] + public bool Sortable { + get { return sortable; } + set { sortable = value; } + } + private bool sortable = true; + + /// + /// Gets or sets the horizontal alignment of the contents of the column. + /// + /// .NET will not allow column 0 to have any alignment except + /// to the left. We can't change the basic behaviour of the listview, + /// but when owner drawn, column 0 can now have other alignments. + new public HorizontalAlignment TextAlign { + get { + return this.textAlign.HasValue ? this.textAlign.Value : base.TextAlign; + } + set { + this.textAlign = value; + base.TextAlign = value; + } + } + private HorizontalAlignment? textAlign; + + /// + /// Gets the StringAlignment equivilent of the column text alignment + /// + [Browsable(false)] + public StringAlignment TextStringAlign { + get { + switch (this.TextAlign) { + case HorizontalAlignment.Center: + return StringAlignment.Center; + case HorizontalAlignment.Left: + return StringAlignment.Near; + case HorizontalAlignment.Right: + return StringAlignment.Far; + default: + return StringAlignment.Near; + } + } + } + + /// + /// What string should be displayed when the mouse is hovered over the header of this column? + /// + /// If a HeaderToolTipGetter is installed on the owning ObjectListView, this + /// value will be ignored. + [Category("ObjectListView"), + Description("The tooltip to show when the mouse is hovered over the header of this column"), + DefaultValue((String)null), + Localizable(true)] + public String ToolTipText { + get { return toolTipText; } + set { toolTipText = value; } + } + private String toolTipText; + + /// + /// Should this column have a tri-state checkbox? + /// + /// + /// If this is true, the user can choose the third state (normally Indeterminate). + /// + [Category("ObjectListView"), + Description("Should values in this column be treated as a tri-state checkbox?"), + DefaultValue(false)] + public virtual bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + if (value && !this.CheckBoxes) + this.CheckBoxes = true; + } + } + private bool triStateCheckBoxes; + + /// + /// Group objects by the initial letter of the aspect of the column + /// + /// + /// One common pattern is to group column by the initial letter of the value for that group. + /// The aspect must be a string (obviously). + /// + [Category("ObjectListView"), + Description("The name of the property or method that should be called to get the aspect to display in this column"), + DefaultValue(false)] + public bool UseInitialLetterForGroup { + get { return useInitialLetterForGroup; } + set { useInitialLetterForGroup = value; } + } + private bool useInitialLetterForGroup; + + /// + /// Gets or sets whether or not this column should be user filterable + /// + [Category("ObjectListView"), + Description("Does this column want to show a Filter menu item when its header is right clicked"), + DefaultValue(true)] + public bool UseFiltering { + get { return useFiltering; } + set { useFiltering = value; } + } + private bool useFiltering = true; + + /// + /// Gets or sets a filter that will only include models where the model's value + /// for this column is one of the values in ValuesChosenForFiltering + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IModelFilter ValueBasedFilter { + get { + if (!this.UseFiltering) + return null; + + if (valueBasedFilter != null) + return valueBasedFilter; + + if (this.ClusteringStrategy == null) + return null; + + if (this.ValuesChosenForFiltering == null || this.ValuesChosenForFiltering.Count == 0) + return null; + + return this.ClusteringStrategy.CreateFilter(this.ValuesChosenForFiltering); + } + set { valueBasedFilter = value; } + } + private IModelFilter valueBasedFilter; + + /// + /// Gets or sets the values that will be used to generate a filter for this + /// column. For a model to be included by the generated filter, its value for this column + /// must be in this list. If the list is null or empty, this column will + /// not be used for filtering. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList ValuesChosenForFiltering { + get { return this.valuesChosenForFiltering; } + set { this.valuesChosenForFiltering = value; } + } + private IList valuesChosenForFiltering = new ArrayList(); + + /// + /// What is the width of this column? + /// + [Category("ObjectListView"), + Description("The width in pixels of this column"), + DefaultValue(60)] + new public int Width { + get { return base.Width; } + set { + if (this.MaximumWidth != -1 && value > this.MaximumWidth) + base.Width = this.MaximumWidth; + else + base.Width = Math.Max(this.MinimumWidth, value); + } + } + + /// + /// Gets or set whether the contents of this column's cells should be word wrapped + /// + /// If this column uses a custom IRenderer (that is, one that is not descended + /// from BaseRenderer), then that renderer is responsible for implementing word wrapping. + [Category("ObjectListView"), + Description("Draw this column cell's word wrapped"), + DefaultValue(false)] + public bool WordWrap { + get { return wordWrap; } + set { + wordWrap = value; + + // If there isn't a renderer and they are turning word wrap off, we don't need to do anything + if (this.Renderer == null && !wordWrap) + return; + + // All other cases require a renderer of some sort + if (this.Renderer == null) + this.Renderer = new HighlightTextRenderer(); + + BaseRenderer baseRenderer = this.Renderer as BaseRenderer; + + // If there is a custom renderer (not descended from BaseRenderer), + // we leave it up to them to implement wrapping + if (baseRenderer == null) + return; + + baseRenderer.CanWrap = wordWrap; + } + } + private bool wordWrap; + + #endregion + + #region Object commands + + /// + /// For a given group value, return the string that should be used as the groups title. + /// + /// The group key that is being converted to a title + /// string + public string ConvertGroupKeyToTitle(object value) { + if (this.groupKeyToTitleConverter != null) + return this.groupKeyToTitleConverter(value); + + return value == null ? ObjectListView.GroupTitleDefault : this.ValueToString(value); + } + + /// + /// Get the checkedness of the given object for this column + /// + /// The row object that is being displayed + /// The checkedness of the object + public CheckState GetCheckState(object rowObject) { + if (!this.CheckBoxes) + return CheckState.Unchecked; + + bool? aspectAsBool = this.GetValue(rowObject) as bool?; + if (aspectAsBool.HasValue) { + if (aspectAsBool.Value) + return CheckState.Checked; + else + return CheckState.Unchecked; + } else + return CheckState.Indeterminate; + } + + /// + /// Put the checkedness of the given object for this column + /// + /// The row object that is being displayed + /// + /// The checkedness of the object + public void PutCheckState(object rowObject, CheckState newState) { + if (newState == CheckState.Checked) + this.PutValue(rowObject, true); + else + if (newState == CheckState.Unchecked) + this.PutValue(rowObject, false); + else + this.PutValue(rowObject, null); + } + + /// + /// For a given row object, extract the value indicated by the AspectName property of this column. + /// + /// The row object that is being displayed + /// An object, which is the aspect named by AspectName + public object GetAspectByName(object rowObject) { + if (this.aspectMunger == null) + this.aspectMunger = new Munger(this.AspectName); + + return this.aspectMunger.GetValue(rowObject); + } + private Munger aspectMunger; + + /// + /// For a given row object, return the object that is the key of the group that this row belongs to. + /// + /// The row object that is being displayed + /// Group key object + public object GetGroupKey(object rowObject) { + if (this.groupKeyGetter != null) + return this.groupKeyGetter(rowObject); + + object key = this.GetValue(rowObject); + + if (this.UseInitialLetterForGroup) { + String keyAsString = key as String; + if (!string.IsNullOrEmpty(keyAsString)) + return keyAsString.Substring(0, 1).ToUpper(); + } + + return key; + } + + /// + /// For a given row object, return the image selector of the image that should displayed in this column. + /// + /// The row object that is being displayed + /// int or string or Image. int or string will be used as index into image list. null or -1 means no image + public Object GetImage(object rowObject) { + if (this.CheckBoxes) + return this.GetCheckStateImage(rowObject); + + if (this.ImageGetter != null) + return this.ImageGetter(rowObject); + + if (!String.IsNullOrEmpty(this.ImageAspectName)) { + if (this.imageAspectMunger == null) + this.imageAspectMunger = new Munger(this.ImageAspectName); + + return this.imageAspectMunger.GetValue(rowObject); + } + + // I think this is wrong. ImageKey is meant for the image in the header, not in the rows + if (!String.IsNullOrEmpty(this.ImageKey)) + return this.ImageKey; + + return this.ImageIndex; + } + private Munger imageAspectMunger; + + /// + /// Return the image that represents the check box for the given model + /// + /// + /// + public string GetCheckStateImage(Object rowObject) { + CheckState checkState = this.GetCheckState(rowObject); + + if (checkState == CheckState.Checked) + return ObjectListView.CHECKED_KEY; + + if (checkState == CheckState.Unchecked) + return ObjectListView.UNCHECKED_KEY; + + return ObjectListView.INDETERMINATE_KEY; + } + + /// + /// For a given row object, return the string representation of the value shown in this column. + /// + /// + /// For aspects that are string (e.g. aPerson.Name), the aspect and its string representation are the same. + /// For non-strings (e.g. aPerson.DateOfBirth), the string representation is very different. + /// + /// + /// + public string GetStringValue(object rowObject) { + return this.ValueToString(this.GetValue(rowObject)); + } + + /// + /// For a given row object, return the object that is to be displayed in this column. + /// + /// The row object that is being displayed + /// An object, which is the aspect to be displayed + public object GetValue(object rowObject) { + if (this.AspectGetter == null) + return this.GetAspectByName(rowObject); + else + return this.AspectGetter(rowObject); + } + + /// + /// Update the given model object with the given value using the column's + /// AspectName. + /// + /// The model object to be updated + /// The value to be put into the model + public void PutAspectByName(Object rowObject, Object newValue) { + if (this.aspectMunger == null) + this.aspectMunger = new Munger(this.AspectName); + + this.aspectMunger.PutValue(rowObject, newValue); + } + + /// + /// Update the given model object with the given value + /// + /// The model object to be updated + /// The value to be put into the model + public void PutValue(Object rowObject, Object newValue) { + if (this.aspectPutter == null) + this.PutAspectByName(rowObject, newValue); + else + this.aspectPutter(rowObject, newValue); + } + + /// + /// Convert the aspect object to its string representation. + /// + /// + /// If the column has been given a AspectToStringConverter, that will be used to do + /// the conversion, otherwise just use ToString(). + /// The returned value will not be null. Nulls are always converted + /// to empty strings. + /// + /// The value of the aspect that should be displayed + /// A string representation of the aspect + public string ValueToString(object value) { + // Give the installed converter a chance to work (even if the value is null) + if (this.AspectToStringConverter != null) + return this.AspectToStringConverter(value) ?? String.Empty; + + // Without a converter, nulls become simple empty strings + if (value == null) + return String.Empty; + + string fmt = this.AspectToStringFormat; + if (String.IsNullOrEmpty(fmt)) + return value.ToString(); + else + return String.Format(fmt, value); + } + + #endregion + + #region Utilities + + /// + /// Decide the clustering strategy that will be used for this column + /// + /// + private IClusteringStrategy DecideDefaultClusteringStrategy() { + if (!this.UseFiltering) + return null; + + if (this.DataType == typeof(DateTime)) + return new DateTimeClusteringStrategy(); + + return new ClustersFromGroupsStrategy(); + } + + /// + /// Gets or sets the type of data shown in this column. + /// + /// If this is not set, it will try to get the type + /// by looking through the rows of the listview. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Type DataType { + get { + if (this.dataType == null) { + ObjectListView olv = this.ListView as ObjectListView; + if (olv != null) { + object value = olv.GetFirstNonNullValue(this); + if (value != null) + return value.GetType(); // THINK: Should we cache this? + } + } + return this.dataType; + } + set { + this.dataType = value; + } + } + private Type dataType; + + #region Events + + /// + /// This event is triggered when the visibility of this column changes. + /// + [Category("ObjectListView"), + Description("This event is triggered when the visibility of the column changes.")] + public event EventHandler VisibilityChanged; + + /// + /// Tell the world when visibility of a column changes. + /// + public virtual void OnVisibilityChanged(EventArgs e) + { + if (this.VisibilityChanged != null) + this.VisibilityChanged(this, e); + } + + #endregion + + /// + /// Create groupies + /// This is an untyped version to help with Generator and OLVColumn attributes + /// + /// + /// + public void MakeGroupies(object[] values, string[] descriptions) { + this.MakeGroupies(values, descriptions, null, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions) { + this.MakeGroupies(values, descriptions, null, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images) { + this.MakeGroupies(values, descriptions, images, null, null); + } + + /// + /// Create groupies + /// + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles) { + this.MakeGroupies(values, descriptions, images, subtitles, null); + } + + /// + /// Create groupies. + /// Install delegates that will group the columns aspects into progressive partitions. + /// If an aspect is less than value[n], it will be grouped with description[n]. + /// If an aspect has a value greater than the last element in "values", it will be grouped + /// with the last element in "descriptions". + /// + /// Array of values. Values must be able to be + /// compared to the aspect (using IComparable) + /// The description for the matching value. The last element is the default description. + /// If there are n values, there must be n+1 descriptions. + /// + /// this.salaryColumn.MakeGroupies( + /// new UInt32[] { 20000, 100000 }, + /// new string[] { "Lowly worker", "Middle management", "Rarified elevation"}); + /// + /// + /// + /// + /// + public void MakeGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { + // Sanity checks + if (values == null) + throw new ArgumentNullException("values"); + if (descriptions == null) + throw new ArgumentNullException("descriptions"); + if (values.Length + 1 != descriptions.Length) + throw new ArgumentException("descriptions must have one more element than values."); + + // Install a delegate that returns the index of the description to be shown + this.GroupKeyGetter = delegate(object row) { + Object aspect = this.GetValue(row); + if (aspect == null || aspect == System.DBNull.Value) + return -1; + IComparable comparable = (IComparable)aspect; + for (int i = 0; i < values.Length; i++) { + if (comparable.CompareTo(values[i]) < 0) + return i; + } + + // Display the last element in the array + return descriptions.Length - 1; + }; + + // Install a delegate that simply looks up the given index in the descriptions. + this.GroupKeyToTitleConverter = delegate(object key) { + if ((int)key < 0) + return ""; + + return descriptions[(int)key]; + }; + + // Install one delegate that does all the other formatting + this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { + int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter + + if (key >= 0) { + if (images != null && key < images.Length) + group.TitleImage = images[key]; + + if (subtitles != null && key < subtitles.Length) + group.Subtitle = subtitles[key]; + + if (tasks != null && key < tasks.Length) + group.Task = tasks[key]; + } + }; + } + /// + /// Create groupies based on exact value matches. + /// + /// + /// Install delegates that will group rows into partitions based on equality of this columns aspects. + /// If an aspect is equal to value[n], it will be grouped with description[n]. + /// If an aspect is not equal to any value, it will be grouped with "[other]". + /// + /// Array of values. Values must be able to be + /// equated to the aspect + /// The description for the matching value. + /// + /// this.marriedColumn.MakeEqualGroupies( + /// new MaritalStatus[] { MaritalStatus.Single, MaritalStatus.Married, MaritalStatus.Divorced, MaritalStatus.Partnered }, + /// new string[] { "Looking", "Content", "Looking again", "Mostly content" }); + /// + /// + /// + /// + /// + public void MakeEqualGroupies(T[] values, string[] descriptions, object[] images, string[] subtitles, string[] tasks) { + // Sanity checks + if (values == null) + throw new ArgumentNullException("values"); + if (descriptions == null) + throw new ArgumentNullException("descriptions"); + if (values.Length != descriptions.Length) + throw new ArgumentException("descriptions must have the same number of elements as values."); + + ArrayList valuesArray = new ArrayList(values); + + // Install a delegate that returns the index of the description to be shown + this.GroupKeyGetter = delegate(object row) { + return valuesArray.IndexOf(this.GetValue(row)); + }; + + // Install a delegate that simply looks up the given index in the descriptions. + this.GroupKeyToTitleConverter = delegate(object key) { + int intKey = (int)key; // we know this is an int since we created it in GroupKeyGetter + return (intKey < 0) ? "[other]" : descriptions[intKey]; + }; + + // Install one delegate that does all the other formatting + this.GroupFormatter = delegate(OLVGroup group, GroupingParameters parms) { + int key = (int)group.Key; // we know this is an int since we created it in GroupKeyGetter + + if (key >= 0) { + if (images != null && key < images.Length) + group.TitleImage = images[key]; + + if (subtitles != null && key < subtitles.Length) + group.Subtitle = subtitles[key]; + + if (tasks != null && key < tasks.Length) + group.Task = tasks[key]; + } + }; + } + + #endregion + + } +} diff --git a/ObjectListView/ObjectListView.DesignTime.cs b/ObjectListView/ObjectListView.DesignTime.cs new file mode 100644 index 0000000..c3f2406 --- /dev/null +++ b/ObjectListView/ObjectListView.DesignTime.cs @@ -0,0 +1,550 @@ +/* + * DesignSupport - Design time support for the various classes within ObjectListView + * + * Author: Phillip Piper + * Date: 12/08/2009 8:36 PM + * + * Change log: + * 2012-08-27 JPP - Fall back to more specific type name for the ListViewDesigner if + * the first GetType() fails. + * v2.5.1 + * 2012-04-26 JPP - Filter group events from TreeListView since it can't have groups + * 2011-06-06 JPP - Vastly improved ObjectListViewDesigner, based off information in + * "'Inheriting' from an Internal WinForms Designer" on CodeProject. + * v2.3 + * 2009-08-12 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.Windows.Forms.Design; + +namespace BrightIdeasSoftware.Design +{ + + /// + /// Designer for and its subclasses. + /// + /// + /// + /// This designer removes properties and events that are available on ListView but that are not + /// useful on ObjectListView. + /// + /// + /// We can't inherit from System.Windows.Forms.Design.ListViewDesigner, since it is marked internal. + /// So, this class uses reflection to create a ListViewDesigner and then forwards messages to that designer. + /// + /// + public class ObjectListViewDesigner : ControlDesigner + { + + #region Initialize & Dispose + + /// + /// Initializes the designer with the specified component. + /// + /// The to associate the designer with. This component must always be an instance of, or derive from, . + public override void Initialize(IComponent component) { + // Debug.WriteLine("ObjectListViewDesigner.Initialize"); + + // Use reflection to bypass the "internal" marker on ListViewDesigner + // If we can't get the unversioned designer, look specifically for .NET 4.0 version of it. + Type tListViewDesigner = Type.GetType("System.Windows.Forms.Design.ListViewDesigner, System.Design") ?? + Type.GetType("System.Windows.Forms.Design.ListViewDesigner, System.Design, " + + "Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + if (tListViewDesigner == null) throw new ArgumentException("Could not load ListViewDesigner"); + + this.listViewDesigner = (ControlDesigner)Activator.CreateInstance(tListViewDesigner, BindingFlags.Instance | BindingFlags.Public, null, null, null); + this.designerFilter = this.listViewDesigner; + + // Fetch the methods from the ListViewDesigner that we know we want to use + this.listViewDesignGetHitTest = tListViewDesigner.GetMethod("GetHitTest", BindingFlags.Instance | BindingFlags.NonPublic); + this.listViewDesignWndProc = tListViewDesigner.GetMethod("WndProc", BindingFlags.Instance | BindingFlags.NonPublic); + + Debug.Assert(this.listViewDesignGetHitTest != null, "Required method (GetHitTest) not found on ListViewDesigner"); + Debug.Assert(this.listViewDesignWndProc != null, "Required method (WndProc) not found on ListViewDesigner"); + + // Tell the Designer to use properties of default designer as well as the properties of this class (do before base.Initialize) + TypeDescriptor.CreateAssociation(component, this.listViewDesigner); + + IServiceContainer site = (IServiceContainer)component.Site; + if (site != null && GetService(typeof(DesignerCommandSet)) == null) { + site.AddService(typeof(DesignerCommandSet), new CDDesignerCommandSet(this)); + } else { + Debug.Fail("site != null && GetService(typeof (DesignerCommandSet)) == null"); + } + + this.listViewDesigner.Initialize(component); + base.Initialize(component); + + RemoveDuplicateDockingActionList(); + } + + /// + /// Initializes a newly created component. + /// + /// A name/value dictionary of default values to apply to properties. May be null if no default values are specified. + public override void InitializeNewComponent(IDictionary defaultValues) { + // Debug.WriteLine("ObjectListViewDesigner.InitializeNewComponent"); + base.InitializeNewComponent(defaultValues); + this.listViewDesigner.InitializeNewComponent(defaultValues); + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) { + // Debug.WriteLine("ObjectListViewDesigner.Dispose"); + if (disposing) { + if (this.listViewDesigner != null) { + this.listViewDesigner.Dispose(); + // Normally we would now null out the designer, but this designer + // still has methods called AFTER it is disposed. + } + } + + base.Dispose(disposing); + } + + /// + /// Removes the duplicate DockingActionList added by this designer to the . + /// + /// + /// adds an internal DockingActionList : 'Dock/Undock in Parent Container'. + /// But the default designer has already added that action list. So we need to remove one. + /// + private void RemoveDuplicateDockingActionList() { + // This is a true hack -- in a class that is basically a huge hack itself. + // Reach into the bowel of our base class, get a private field, and use that fields value to + // remove an action from the designer. + // In ControlDesigner, there is "private DockingActionList dockingAction;" + // Don't you just love Reflector?! + FieldInfo fi = typeof(ControlDesigner).GetField("dockingAction", BindingFlags.Instance | BindingFlags.NonPublic); + if (fi != null) { + DesignerActionList dockingAction = (DesignerActionList)fi.GetValue(this); + if (dockingAction != null) { + DesignerActionService service = (DesignerActionService)GetService(typeof(DesignerActionService)); + if (service != null) { + service.Remove(this.Control, dockingAction); + } + } + } + } + + #endregion + + #region IDesignerFilter overrides + + /// + /// Adjusts the set of properties the component exposes through a . + /// + /// An containing the properties for the class of the component. + protected override void PreFilterProperties(IDictionary properties) { + // Debug.WriteLine("ObjectListViewDesigner.PreFilterProperties"); + + // Always call the base PreFilterProperties implementation + // before you modify the properties collection. + base.PreFilterProperties(properties); + + // Give the listviewdesigner a chance to filter the properties + // (though we already know it's not going to do anything) + this.designerFilter.PreFilterProperties(properties); + + // I'd like to just remove the redundant properties, but that would + // break backward compatibility. The deserialiser that handles the XXX.Designer.cs file + // works off the designer, so even if the property exists in the class, the deserialiser will + // throw an error if the associated designer actually removes that property. + // So we shadow the unwanted properties, and give the replacement properties + // non-browsable attributes so that they are hidden from the user + + List unwantedProperties = new List(new string[] { + "BackgroundImage", "BackgroundImageTiled", "HotTracking", "HoverSelection", + "LabelEdit", "VirtualListSize", "VirtualMode" }); + + // Also hid Tooltip properties, since giving a tooltip to the control through the IDE + // messes up the tooltip handling + foreach (string propertyName in properties.Keys) { + if (propertyName.StartsWith("ToolTip")) { + unwantedProperties.Add(propertyName); + } + } + + // If we are looking at a TreeListView, remove group related properties + // since TreeListViews can't show groups + if (this.Control is TreeListView) { + unwantedProperties.AddRange(new string[] { + "GroupImageList", "GroupWithItemCountFormat", "GroupWithItemCountSingularFormat", "HasCollapsibleGroups", + "SpaceBetweenGroups", "ShowGroups", "SortGroupItemsByPrimaryColumn", "ShowItemCountOnGroups" + }); + } + + // Shadow the unwanted properties, and give the replacement properties + // non-browsable attributes so that they are hidden from the user + foreach (string unwantedProperty in unwantedProperties) { + PropertyDescriptor propertyDesc = TypeDescriptor.CreateProperty( + typeof(ObjectListView), + (PropertyDescriptor)properties[unwantedProperty], + new BrowsableAttribute(false)); + properties[unwantedProperty] = propertyDesc; + } + } + + /// + /// Allows a designer to add to the set of events that it exposes through a . + /// + /// The events for the class of the component. + protected override void PreFilterEvents(IDictionary events) { + // Debug.WriteLine("ObjectListViewDesigner.PreFilterEvents"); + base.PreFilterEvents(events); + this.designerFilter.PreFilterEvents(events); + + // Remove the events that don't make sense for an ObjectListView. + // See PreFilterProperties() for why we do this dance rather than just remove the event. + List unwanted = new List(new string[] { + "AfterLabelEdit", + "BeforeLabelEdit", + "DrawColumnHeader", + "DrawItem", + "DrawSubItem", + "RetrieveVirtualItem", + "SearchForVirtualItem", + "VirtualItemsSelectionRangeChanged" + }); + + // If we are looking at a TreeListView, remove group related events + // since TreeListViews can't show groups + if (this.Control is TreeListView) { + unwanted.AddRange(new string[] { + "AboutToCreateGroups", + "AfterCreatingGroups", + "BeforeCreatingGroups", + "GroupTaskClicked", + "GroupExpandingCollapsing", + "GroupStateChanged" + }); + } + + foreach (string unwantedEvent in unwanted) { + EventDescriptor eventDesc = TypeDescriptor.CreateEvent( + typeof(ObjectListView), + (EventDescriptor)events[unwantedEvent], + new BrowsableAttribute(false)); + events[unwantedEvent] = eventDesc; + } + } + + /// + /// Allows a designer to change or remove items from the set of attributes that it exposes through a . + /// + /// The attributes for the class of the component. + protected override void PostFilterAttributes(IDictionary attributes) { + // Debug.WriteLine("ObjectListViewDesigner.PostFilterAttributes"); + this.designerFilter.PostFilterAttributes(attributes); + base.PostFilterAttributes(attributes); + } + + /// + /// Allows a designer to change or remove items from the set of events that it exposes through a . + /// + /// The events for the class of the component. + protected override void PostFilterEvents(IDictionary events) { + // Debug.WriteLine("ObjectListViewDesigner.PostFilterEvents"); + this.designerFilter.PostFilterEvents(events); + base.PostFilterEvents(events); + } + + #endregion + + #region Overrides + + /// + /// Gets the design-time action lists supported by the component associated with the designer. + /// + /// + /// The design-time action lists supported by the component associated with the designer. + /// + public override DesignerActionListCollection ActionLists { + get { + // We want to change the first action list so it only has the commands we want + DesignerActionListCollection actionLists = this.listViewDesigner.ActionLists; + if (actionLists.Count > 0 && !(actionLists[0] is ListViewActionListAdapter)) { + actionLists[0] = new ListViewActionListAdapter(this, actionLists[0]); + } + return actionLists; + } + } + + /// + /// Gets the collection of components associated with the component managed by the designer. + /// + /// + /// The components that are associated with the component managed by the designer. + /// + public override ICollection AssociatedComponents { + get { + ArrayList components = new ArrayList(base.AssociatedComponents); + components.AddRange(this.listViewDesigner.AssociatedComponents); + return components; + } + } + + /// + /// Indicates whether a mouse click at the specified point should be handled by the control. + /// + /// + /// true if a click at the specified point is to be handled by the control; otherwise, false. + /// + /// A indicating the position at which the mouse was clicked, in screen coordinates. + protected override bool GetHitTest(Point point) { + // The ListViewDesigner wants to allow column dividers to be resized + return (bool)this.listViewDesignGetHitTest.Invoke(listViewDesigner, new object[] { point }); + } + + /// + /// Processes Windows messages and optionally routes them to the control. + /// + /// The to process. + protected override void WndProc(ref Message m) { + switch (m.Msg) { + case 0x4e: + case 0x204e: + // The listview designer is interested in HDN_ENDTRACK notifications + this.listViewDesignWndProc.Invoke(listViewDesigner, new object[] { m }); + break; + default: + base.WndProc(ref m); + break; + } + } + + #endregion + + #region Implementation variables + + private ControlDesigner listViewDesigner; + private IDesignerFilter designerFilter; + private MethodInfo listViewDesignGetHitTest; + private MethodInfo listViewDesignWndProc; + + #endregion + + #region Custom action list + + /// + /// This class modifies a ListViewActionList, by removing the "Edit Items" and "Edit Groups" actions. + /// + /// + /// + /// That class is internal, so we cannot simply subclass it, which would be simplier. + /// + /// + /// Action lists use reflection to determine if that action can be executed, so we not + /// only have to modify the returned collection of actions, but we have to implement + /// the properties and commands that the returned actions use. + /// + private class ListViewActionListAdapter : DesignerActionList + { + public ListViewActionListAdapter(ObjectListViewDesigner designer, DesignerActionList wrappedList) + : base(wrappedList.Component) { + this.designer = designer; + this.wrappedList = wrappedList; + } + + public override DesignerActionItemCollection GetSortedActionItems() { + DesignerActionItemCollection items = wrappedList.GetSortedActionItems(); + items.RemoveAt(2); // remove Edit Groups + items.RemoveAt(0); // remove Edit Items + return items; + } + + private void EditValue(ComponentDesigner componentDesigner, IComponent iComponent, string propertyName) { + // One more complication. The ListViewActionList classes uses an internal class, EditorServiceContext, to + // edit the items/columns/groups collections. So, we use reflection to bypass the data hiding. + Type tEditorServiceContext = Type.GetType("System.Windows.Forms.Design.EditorServiceContext, System.Design"); + tEditorServiceContext.InvokeMember("EditValue", BindingFlags.InvokeMethod | BindingFlags.Static, null, null, new object[] { componentDesigner, iComponent, propertyName }); + } + + private void SetValue(object target, string propertyName, object value) { + TypeDescriptor.GetProperties(target)[propertyName].SetValue(target, value); + } + + public void InvokeColumnsDialog() { + EditValue(this.designer, base.Component, "Columns"); + } + + // Don't need these since we removed their corresponding actions from the list. + // Keep the methods just in case. + + //public void InvokeGroupsDialog() { + // EditValue(this.designer, base.Component, "Groups"); + //} + + //public void InvokeItemsDialog() { + // EditValue(this.designer, base.Component, "Items"); + //} + + public ImageList LargeImageList { + get { return ((ListView)base.Component).LargeImageList; } + set { SetValue(base.Component, "LargeImageList", value); } + } + + public ImageList SmallImageList { + get { return ((ListView)base.Component).SmallImageList; } + set { SetValue(base.Component, "SmallImageList", value); } + } + + public View View { + get { return ((ListView)base.Component).View; } + set { SetValue(base.Component, "View", value); } + } + + ObjectListViewDesigner designer; + DesignerActionList wrappedList; + } + + #endregion + + #region DesignerCommandSet + + private class CDDesignerCommandSet : DesignerCommandSet + { + + public CDDesignerCommandSet(ComponentDesigner componentDesigner) { + this.componentDesigner = componentDesigner; + } + + public override ICollection GetCommands(string name) { + // Debug.WriteLine("CDDesignerCommandSet.GetCommands:" + name); + if (componentDesigner != null) { + if (name.Equals("Verbs")) { + return componentDesigner.Verbs; + } + if (name.Equals("ActionLists")) { + return componentDesigner.ActionLists; + } + } + return base.GetCommands(name); + } + + private readonly ComponentDesigner componentDesigner; + } + + #endregion + } + + /// + /// This class works in conjunction with the OLVColumns property to allow OLVColumns + /// to be added to the ObjectListView. + /// + public class OLVColumnCollectionEditor : System.ComponentModel.Design.CollectionEditor + { + /// + /// Create a OLVColumnCollectionEditor + /// + /// + public OLVColumnCollectionEditor(Type t) + : base(t) { + } + + /// + /// What type of object does this editor create? + /// + /// + protected override Type CreateCollectionItemType() { + return typeof(OLVColumn); + } + + /// + /// Edit a given value + /// + /// + /// + /// + /// + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { + if (context == null) + throw new ArgumentNullException("context"); + if (provider == null) + throw new ArgumentNullException("provider"); + + // Figure out which ObjectListView we are working on. This should be the Instance of the context. + ObjectListView olv = context.Instance as ObjectListView; + Debug.Assert(olv != null, "Instance must be an ObjectListView"); + + // Edit all the columns, not just the ones that are visible + base.EditValue(context, provider, olv.AllColumns); + + // Set the columns on the ListView to just the visible columns + List newColumns = olv.GetFilteredColumns(View.Details); + olv.Columns.Clear(); + olv.Columns.AddRange(newColumns.ToArray()); + + return olv.Columns; + } + + /// + /// What text should be shown in the list for the given object? + /// + /// + /// + protected override string GetDisplayText(object value) { + OLVColumn col = value as OLVColumn; + if (col == null || String.IsNullOrEmpty(col.AspectName)) + return base.GetDisplayText(value); + + return String.Format("{0} ({1})", base.GetDisplayText(value), col.AspectName); + } + } + + /// + /// Control how the overlay is presented in the IDE + /// + internal class OverlayConverter : ExpandableObjectConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { + return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); + } + + public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { + if (destinationType == typeof(string)) { + ImageOverlay imageOverlay = value as ImageOverlay; + if (imageOverlay != null) { + return imageOverlay.Image == null ? "(none)" : "(set)"; + } + TextOverlay textOverlay = value as TextOverlay; + if (textOverlay != null) { + return String.IsNullOrEmpty(textOverlay.Text) ? "(none)" : "(set)"; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} diff --git a/ObjectListView/ObjectListView.FxCop b/ObjectListView/ObjectListView.FxCop new file mode 100644 index 0000000..b5653dd --- /dev/null +++ b/ObjectListView/ObjectListView.FxCop @@ -0,0 +1,3521 @@ + + + + True + c:\program files\microsoft fxcop 1.36\Xml\FxCopReport.xsl + + + + + + True + True + True + 10 + 1 + + False + + False + 120 + True + 2.0 + + + + $(ProjectDir)/trunk/ObjectListView/bin/Debug/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'ObjectListView.dll' + + + + + + + + + + + 'BarRenderer' + 'Pen' + + + + + + + + + + + + + + 'BarRenderer.BarRenderer(Pen, Brush)' + 'BarRenderer.UseStandardBar' + 'bool' + false + + + + + + + + + + + + + + 'BarRenderer.BarRenderer(int, int, Pen, Brush)' + 'BarRenderer.UseStandardBar' + 'bool' + false + + + + + + + + + + + + + + 'BarRenderer.BackgroundColor' + 'BaseRenderer.GetBackgroundColor()' + + + + + + + + + + + + + + + + + + 'BaseRenderer.GetBackgroundColor()' + + + + + + + + + + + + + + 'BaseRenderer.GetForegroundColor()' + + + + + + + + + + + + + + 'BaseRenderer.GetImageSelector()' + + + + + + + + + 'BaseRenderer.GetText()' + + + + + + + + + + + + + + 'BaseRenderer.GetTextBackgroundColor()' + + + + + + + + + + + + + + + + 'BorderDecoration' + 'SolidBrush' + + + + + + + + + 'CellEditEventHandler' + + + + + + + + + + + + + + + + 'g' + 'CheckStateRenderer.CalculateCheckBoxBounds(Graphics, Rectangle)' + + + + + + + + + + + 'ColumnRightClickEventHandler' + + + + + + + + + + + + + + + + + + 'ComboBoxItem.Key.get()' + + + + + + + + + + + + + + + 'DataListView.currencyManager_ListChanged(object, ListChangedEventArgs)' + + + + + + + + + 'DataListView.currencyManager_MetaDataChanged(object, EventArgs)' + + + + + + + + + 'DataListView.currencyManager_PositionChanged(object, EventArgs)' + + + + + + + + + + + + + 'DescribedTaskRenderer.GetDescription()' + + + + + + + + + + + 'DropTargetLocation' + + + + + + + + + + + 'collection' + 'ICollection' + 'FastObjectListDataSource.EnumerableToArray(IEnumerable)' + castclass + + + + + + + + + + + 'FastObjectListDataSource.FilteredObjectList.get()' + + + + + + + + + + + + + Flag + 'FlagRenderer' + + + + + + + + + + + + + 'FloatCellEditor.Value.get()' + + + + + + + + + + + + + + 'FloatCellEditor.Value.set(double)' + + + + + + + + + + + + + + + + + + + + + + 'GlassPanelForm.CreateParams.get()' + 'Form.CreateParams.get()' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + + + + + + + 'GlassPanelForm.WndProc(ref Message)' + 'Form.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + + + + + + + 'GroupMetricsMask' + 'GroupMetricsMask.LVGMF_NONE' + + + + + + + + + + 'GroupMetricsMask' + + + + + + + + + + + + + + 'GroupState' + 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000 + + + + + 'GroupState' + 'GroupState.LVGS_NORMAL' + + + + + 'GroupState' + + + + + + + + + + + 'HeaderControl.HeaderControl(ObjectListView)' + 'NativeWindow.AssignHandle(IntPtr)' + ->'HeaderControl.HeaderControl(ObjectListView)' ->'HeaderControl.HeaderControl(ObjectListView)' + + + 'HeaderControl.HeaderControl(ObjectListView)' + 'NativeWindow.NativeWindow()' + ->'HeaderControl.HeaderControl(ObjectListView)' ->'HeaderControl.HeaderControl(ObjectListView)' + + + + + + + + + + + + + + 'g' + 'HeaderControl.CalculateHeight(Graphics)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawHeaderText(Graphics, Rectangle, OLVColumn, HeaderStateStyle)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawThemedBackground(Graphics, Rectangle, int, bool)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + 'g' + 'HeaderControl.DrawThemedSortIndicator(Graphics, Rectangle)' + 'Graphics' + 'IDeviceContext' + + + + + + + + + Unthemed + 'HeaderControl.DrawUnthemedBackground(Graphics, Rectangle, int, bool, HeaderStateStyle)' + + + + + + + + + Unthemed + 'HeaderControl.DrawUnthemedSortIndicator(Graphics, Rectangle)' + + + + + + + + + 'm' + 'HeaderControl.HandleDestroy(ref Message)' + + + + + + + + + 'm' + 'HeaderControl.HandleMouseMove(ref Message)' + + + + + 'm' + + + + + + + + + + + + + + 'HeaderControl.HandleNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'HeaderControl.HandleNotify(ref Message)' ->'HeaderControl.HandleNotify(ref Message)' + + + 'HeaderControl.HandleNotify(ref Message)' + 'Message.LParam.get()' + ->'HeaderControl.HandleNotify(ref Message)' ->'HeaderControl.HandleNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + + + 'value' + 'HeaderControl.HotFontStyle.set(FontStyle)' + + + + + + + + + + + Flags + 'HeaderControl.TextFormatFlags' + + + + + + + + + 'HeaderControl.WndProc(ref Message)' + 'NativeWindow.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + 'HeaderControl.WndProc(ref Message)' + 'Message.Msg.get()' + ->'HeaderControl.WndProc(ref Message)' ->'HeaderControl.WndProc(ref Message)' + + + 'HeaderControl.WndProc(ref Message)' + 'NativeWindow.WndProc(ref Message)' + ->'HeaderControl.WndProc(ref Message)' ->'HeaderControl.WndProc(ref Message)' + + + + + + + + + + + + + + + + + + 'text' + 'HighlightTextRenderer.HighlightTextRenderer(string)' + + + + + + + + + + + 'value' + 'HighlightTextRenderer.StringComparison.set(StringComparison)' + + + + + + + + + + + + + 'value' + 'HighlightTextRenderer.TextToHighlight.set(string)' + + + + + + + + + + + + + + + + + 'HyperlinkEventArgs.Column.set(OLVColumn)' + + + + + + + + + + + + + 'HyperlinkEventArgs.ColumnIndex.set(int)' + + + + + + + + + + + + + 'HyperlinkEventArgs.Item.set(OLVListItem)' + + + + + + + + + + + + + 'HyperlinkEventArgs.ListView.set(ObjectListView)' + + + + + + + + + + + + + 'HyperlinkEventArgs.Model.set(object)' + + + + + + + + + + + + + 'HyperlinkEventArgs.RowIndex.set(int)' + + + + + + + + + + + + + 'HyperlinkEventArgs.SubItem.set(OLVListSubItem)' + + + + + + + + + + + 'HyperlinkEventArgs.Url' + + + + + + + + + 'HyperlinkEventArgs.Url.set(string)' + + + + + + + + + + + + + + + 'ImageRenderer.GetImageFromAspect()' + + + + + + + + + + + + + + + + + + + + 'IntUpDown.Value.get()' + + + + + + + + + + + + + + 'IntUpDown.Value.set(int)' + + + + + + + + + + + + + + + + + + + + 'IVirtualListDataSource.GetObjectCount()' + + + + + + + + + + + + + + + + Multi + 'MultiImageRenderer' + + + + + + + + + + + + + + 'MultiImageRenderer.ImageSelector' + 'BaseRenderer.GetImageSelector()' + + + + + + + + + + + + + 'Munger.GetValue(object)' + 'object' + + + + + + + + + + + + + + 'Munger.PutValue(object, object)' + 'object' + + + 'Munger.PutValue(object, object)' + 'object' + + + + + + + + + + + + + + + + + + 'NativeMethods.ChangeSize(IWin32Window, int, int)' + + + + + + + + + 'NativeMethods.ChangeZOrder(IWin32Window, IWin32Window)' + + + + + + + + + 'NativeMethods.DeleteObject(IntPtr)' + + + + + + + + + 'NativeMethods.DrawImageList(Graphics, ImageList, int, int, int, bool)' + + + + + + + + + 'NativeMethods.GetClientRect(IntPtr, ref Rectangle)' + + + + + + + + + 'NativeMethods.GetColumnSides(ObjectListView, int)' + + + + + 'NativeMethods.GetColumnSides(ObjectListView, int)' + 'Point' + + + + + + + + + 'NativeMethods.GetGroupInfo(ObjectListView, int, ref NativeMethods.LVGROUP2)' + + + + + + + + + 'NativeMethods.GetScrollInfo(IntPtr, int, NativeMethods.SCROLLINFO)' + + + + + + + + + 'NativeMethods.GetUpdateRect(Control)' + 'NativeMethods.GetUpdateRectInternal(IntPtr, ref Rectangle, bool)' + + + + + + + + + 'eraseBackground' + 'NativeMethods.GetUpdateRectInternal(IntPtr, ref Rectangle, bool)' + + + + + + + + + 'NativeMethods.GetWindowLong32(IntPtr, int)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + + + + + 'NativeMethods.GetWindowLongPtr64(IntPtr, int)' + 'user32.dll' + GetWindowLongPtr + + + + + + + + + 'NativeMethods.ImageList_Draw(IntPtr, int, IntPtr, int, int, int)' + + + + + 'NativeMethods.ImageList_Draw(IntPtr, int, IntPtr, int, int, int)' + + + + + + + + + 'erase' + 'NativeMethods.InvalidateRect(IntPtr, int, bool)' + + + 'NativeMethods.InvalidateRect(IntPtr, int, bool)' + + + + + + + + + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP)' + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUP2)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVGROUPMETRICS)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVHITTESTINFO)' + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, ref NativeMethods.LVHITTESTINFO)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + 'lParam' + 'NativeMethods.SendMessage(IntPtr, int, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessage(IntPtr, int, int, IntPtr)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'lParam' + 'NativeMethods.SendMessage(IntPtr, int, IntPtr, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageHDItem(IntPtr, int, int, ref NativeMethods.HDITEM)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessageIUnknown(IntPtr, int, object, int)' + + + + + 'lParam' + 'NativeMethods.SendMessageIUnknown(IntPtr, int, object, int)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SendMessageLVBKIMAGE(IntPtr, int, int, ref NativeMethods.LVBKIMAGE)' + + + + + 'wParam' + 'NativeMethods.SendMessageLVBKIMAGE(IntPtr, int, int, ref NativeMethods.LVBKIMAGE)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageLVItem(IntPtr, int, int, ref NativeMethods.LVITEM)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageRECT(IntPtr, int, int, ref NativeMethods.RECT)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageString(IntPtr, int, int, string)' + 4 + 64-bit + 8 + 'int' + + + + + 'lParam' + + + + + + + + + 'wParam' + 'NativeMethods.SendMessageTOOLINFO(IntPtr, int, int, NativeMethods.TOOLINFO)' + 4 + 64-bit + 8 + 'int' + + + + + + + + + 'NativeMethods.SetBackgroundImage(ListView, Image)' + + + + + + + + + 'NativeMethods.SetBkColor(IntPtr, int)' + + + + + + + + + 'NativeMethods.SetSelectedColumn(ListView, ColumnHeader)' + + + + + + + + + 'NativeMethods.SetTextColor(IntPtr, int)' + + + + + + + + + 'NativeMethods.SetTooltipControl(ListView, ToolTipControl)' + + + + + + + + + 'NativeMethods.SetWindowLongPtr32(IntPtr, int, int)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + + + + + 'dwNewLong' + 'NativeMethods.SetWindowLongPtr64(IntPtr, int, int)' + 4 + 64-bit + 8 + 'int' + + + + + 'NativeMethods.SetWindowLongPtr64(IntPtr, int, int)' + 'user32.dll' + SetWindowLongPtr + + + + + + + + + 'NativeMethods.SetWindowPos(IntPtr, IntPtr, int, int, int, int, uint)' + + + + + + + + + 'NativeMethods.SetWindowTheme(IntPtr, string, string)' + 8 + 64-bit + 4 + 'IntPtr' + + + + + 'subApp' + + + + + 'subIdList' + + + + + + + + + 'NativeMethods.ShowWindow(IntPtr, int)' + + + + + + + + + 'NativeMethods.ValidatedRectInternal(IntPtr, ref Rectangle)' + + + + + + + + + 'NativeMethods.ValidateRect(Control, Rectangle)' + + + + + + + + + + + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'HeaderControl.ColumnIndexUnderCursor.get()' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'HeaderControl.IsCursorOverLockedDivider.get()' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'OLVListItem.GetSubItemBounds(int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int, ItemBoundsPortion)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.CalculateCellBounds(OLVListItem, int, ItemBoundsPortion)' ->'ObjectListView.CalculateCellTextBounds(OLVListItem, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'ObjectListView.OlvHitTest(int, int)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'NativeMethods.GetScrolledColumnSides(ListView, int)' ->'TintedColumnDecoration.Draw(ObjectListView, Graphics, Rectangle)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.EnsureGroupVisible(ListViewGroup)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.HandleBeginScroll(ref Message)' + + + 'NativeMethods.SCROLLINFO.SCROLLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.SCROLLINFO.SCROLLINFO()' ->'NativeMethods.GetScrollPosition(ListView, bool)' ->'ObjectListView.HandleKeyDown(ref Message)' + + + + + + + + + + + + + + + + + + 'NativeMethods.TOOLINFO.TOOLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'ToolTipControl.MakeToolInfoStruct(IWin32Window)' ->'ToolTipControl.AddTool(IWin32Window)' + + + 'NativeMethods.TOOLINFO.TOOLINFO()' + 'Marshal.SizeOf(Type)' + ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'NativeMethods.TOOLINFO.TOOLINFO()' ->'ToolTipControl.MakeToolInfoStruct(IWin32Window)' ->'ToolTipControl.RemoveToolTip(IWin32Window)' + + + + + + + + + + + + + + + + + + 'ObjectListView.AllColumns' + + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.AllColumns' + + + + + + + + + + + + + + 'rowIndex' + 'ObjectListView.ApplyHyperlinkStyle(int, OLVListItem)' + + + + + + + + + 'ObjectListView.BooleanCheckStateGetter' + + + + + + + + + 'ObjectListView.BooleanCheckStatePutter' + + + + + + + + + 'item' + 'ObjectListView.CalculateCellBounds(OLVListItem, int)' + 'OLVListItem' + 'ListViewItem' + + + + + + + + + + + + + + 'ObjectListView.CellEditor' + 'ObjectListView.GetCellEditor(OLVListItem, int)' + + + + + + + + + 'ObjectListView.CellEditor_Validating(object, CancelEventArgs)' + + + + + + + + + 'ObjectListView.CellToolTip' + 'ObjectListView.GetCellToolTip(int, int)' + + + + + + + + + 'ObjectListView.CheckedObject' + 'ObjectListView.GetCheckedObject()' + + + + + + + + + 'ObjectListView.CheckedObjects' + 'ObjectListView.GetCheckedObjects()' + + + + + 'ObjectListView.CheckedObjects' + + + + + + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.ColumnsInDisplayOrder' + + + + + + + + + + + + + + 'ObjectListView.ConfigureAutoComplete(TextBox, OLVColumn)' + tb + 'tb' + + + + + + + + + 'ObjectListView.ConfigureAutoComplete(TextBox, OLVColumn, int)' + tb + 'tb' + + + + + + + + + 'List<OLVListItem>' + 'ObjectListView.DrawAllDecorations(Graphics, List<OLVListItem>)' + + + + + + + + + 'ObjectListView.EditorRegistry' + + + + + + + + + 'ObjectListView.EnsureGroupVisible(ListViewGroup)' + lvg + 'lvg' + + + + + + + + + 'ObjectListView.FilterObjects(IEnumerable, IModelFilter, IListFilter)' + a + 'aListFilter' + + + 'ObjectListView.FilterObjects(IEnumerable, IModelFilter, IListFilter)' + a + 'aModelFilter' + + + + + + + + + 'control' + 'CheckBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + 'control' + 'ComboBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + 'control' + 'TextBox' + 'ObjectListView.GetControlValue(Control)' + castclass + + + + + + + + + 'List<OLVColumn>' + 'ObjectListView.GetFilteredColumns(View)' + + + + + + + + + + + + + + 'selectedColumn' + + + + + + + + + + + + + + 'ObjectListView.GetItemCount()' + + + + + + + + + + + + + + 'ObjectListView.GetLastItemInDisplayOrder()' + + + + + + + + + 'ObjectListView.GetSelectedObject()' + + + + + + + + + + + + + + 'ObjectListView.GetSelectedObjects()' + + + + + + + + + + + + + + 'ObjectListView.HandleApplicationIdle(object, EventArgs)' + + + + + + + + + 'ObjectListView.HandleApplicationIdle_ResizeColumns(object, EventArgs)' + + + + + + + + + 'ObjectListView.HandleCellToolTipShowing(object, ToolTipShowingEventArgs)' + + + + + + + + + 'ObjectListView.HandleChar(ref Message)' + 'Control.ProcessKeyEventArgs(ref Message)' + ->'ObjectListView.HandleChar(ref Message)' ->'ObjectListView.HandleChar(ref Message)' + + + 'ObjectListView.HandleChar(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleChar(ref Message)' ->'ObjectListView.HandleChar(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleColumnClick(object, ColumnClickEventArgs)' + + + + + + + + + 'ObjectListView.HandleColumnWidthChanged(object, ColumnWidthChangedEventArgs)' + + + + + + + + + 'ObjectListView.HandleColumnWidthChanging(object, ColumnWidthChangingEventArgs)' + + + + + + + + + 'ObjectListView.HandleContextMenu(ref Message)' + 'Message.LParam.get()' + ->'ObjectListView.HandleContextMenu(ref Message)' ->'ObjectListView.HandleContextMenu(ref Message)' + + + 'ObjectListView.HandleContextMenu(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleContextMenu(ref Message)' ->'ObjectListView.HandleContextMenu(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.Result.set(IntPtr)' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + 'ObjectListView.HandleFindItem(ref Message)' + 'Message.WParam.get()' + ->'ObjectListView.HandleFindItem(ref Message)' ->'ObjectListView.HandleFindItem(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleHeaderToolTipShowing(object, ToolTipShowingEventArgs)' + + + + + + + + + 'ObjectListView.HandleLayout(object, LayoutEventArgs)' + + + + + + + + + 'ObjectListView.HandleNotify(ref Message)' + 'Marshal.PtrToStructure(IntPtr, Type)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Marshal.StructureToPtr(object, IntPtr, bool)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'Message.Result.set(IntPtr)' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + 'ObjectListView.HandleNotify(ref Message)' + 'NativeWindow.Handle.get()' + ->'ObjectListView.HandleNotify(ref Message)' ->'ObjectListView.HandleNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Marshal.StructureToPtr(object, IntPtr, bool)' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Message.GetLParam(Type)' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + 'ObjectListView.HandleReflectNotify(ref Message)' + 'Message.LParam.get()' + ->'ObjectListView.HandleReflectNotify(ref Message)' ->'ObjectListView.HandleReflectNotify(ref Message)' + + + + + + + + + + 'm' + + + + + + + + + + + + + + 'ObjectListView.HeaderToolTip' + 'ObjectListView.GetHeaderToolTip(int)' + + + + + + + + + 'url' + 'ObjectListView.IsUrlVisited(string)' + + + + + + + + + 'url' + 'ObjectListView.MarkUrlVisited(string)' + + + + + + + + + Unsort + 'ObjectListView.MenuLabelUnsort' + + + + + + + + + 'ObjectListView.ProcessDialogKey(Keys)' + 'Control.ProcessDialogKey(Keys)' + [UIPermission(SecurityAction.LinkDemand, Window = UIPermissionWindow.AllWindows)] + + + + + 'ObjectListView.ProcessDialogKey(Keys)' + 'Control.ProcessDialogKey(Keys)' + ->'ObjectListView.ProcessDialogKey(Keys)' ->'ObjectListView.ProcessDialogKey(Keys)' + + + + + + + + + + + + + + 'ObjectListView.SelectedObject' + 'ObjectListView.GetSelectedObject()' + + + + + + + + + 'ObjectListView.SelectedObjects' + 'ObjectListView.GetSelectedObjects()' + + + + + 'ObjectListView.SelectedObjects' + + + + + + + + + + + + + + 'control' + 'ComboBox' + 'ObjectListView.SetControlValue(Control, object, string)' + castclass + + + + + + + + + Checkedness + 'ObjectListView.SetObjectCheckedness(object, CheckState)' + + + + + + + + + + + + + + 'item' + 'ObjectListView.SetSubItemImages(int, OLVListItem, bool)' + 'OLVListItem' + 'ListViewItem' + + + + + + + + + + + + + + 'columnToSort' + 'ObjectListView.ShowSortIndicator(OLVColumn, SortOrder)' + 'OLVColumn' + 'ColumnHeader' + + + + + + + + + + + + + + 'ObjectListView.SORT_INDICATOR_DOWN_KEY' + + + + + + + + + + + + + + 'ObjectListView.SORT_INDICATOR_UP_KEY' + + + + + + + + + + + + + + 'ObjectListView' + 'ISupportInitialize.BeginInit()' + + + + + + + + + 'ObjectListView' + 'ISupportInitialize.EndInit()' + + + + + + + + + Renderering + 'ObjectListView.TextRendereringHint' + + + + + + + + + Unsort + 'ObjectListView.Unsort()' + + + + + + + + + 'ObjectListView.WndProc(ref Message)' + 'ListView.WndProc(ref Message)' + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + + + + + 'ObjectListView.WndProc(ref Message)' + 'Control.DefWndProc(ref Message)' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + 'ObjectListView.WndProc(ref Message)' + 'ListView.WndProc(ref Message)' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + 'ObjectListView.WndProc(ref Message)' + 'Message.Msg.get()' + ->'ObjectListView.WndProc(ref Message)' ->'ObjectListView.WndProc(ref Message)' + + + + + + + + + + + + + + + + + + 'ObjectListView.ObjectListViewState.VersionNumber' + + + + + + + + + + + + + + + + 'OLVColumnAttribute' + + + + + 'OLVColumnAttribute.Title' + 'title' + + + + + 'OLVColumnAttribute' + + + + + + + + + Cutoffs + 'OLVColumnAttribute.GroupCutoffs' + + + + + 'OLVColumnAttribute.GroupCutoffs' + + + + + + + + + 'OLVColumnAttribute.GroupDescriptions' + + + + + + + + + + + + + 'OLVDataObject.ConvertToHtmlFragment(string)' + 'string.IndexOf(string)' + 'string.IndexOf(string, StringComparison)' + + + + + + + + + + + + + 'OLVGroup.GetState()' + + + + + + + + + 'OLVGroup.State' + 'OLVGroup.GetState()' + + + + + + + + + Subseted + 'OLVGroup.Subseted' + + + + + + + + + + + 'OLVListItem' + + + + + + + + + 'OLVListItem.Bounds' + 'ListViewItem.GetBounds(ItemBoundsPortion)' + + + + + + + + + + + 'value' + 'string' + 'OLVListItem.ImageSelector.set(object)' + castclass + + + + + + + + + + + + + + + 'OLVListSubItem.Url' + + + + + + + + + + + 'SimpleDropSink' + 'Timer' + + + + + + + + + 'Timer.Interval.set(int)' + 'SimpleDropSink.SimpleDropSink()' + + + + + + + + + + + 'TextAdornment' + 'StringFormat' + + + + + + + + + + + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[])' + StringComparison.InvariantCultureIgnoreCase + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[], TextMatchFilter.MatchKind, StringComparison)' + + + + + + + + + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, TextMatchFilter.MatchKind)' + StringComparison.InvariantCultureIgnoreCase + 'TextMatchFilter.TextMatchFilter(ObjectListView, string, OLVColumn[], TextMatchFilter.MatchKind, StringComparison)' + + + + + + + + + 'TextMatchFilter.Columns' + + + + + + + + + 'TextMatchFilter.IsIncluded(OLVColumn)' + + + + + + + + + + + 'TextMatchFilter.MatchKind' + + + + + + + + + + + + + + 'TintedColumnDecoration' + 'SolidBrush' + + + + + + + + + + + Disp + 'ToolTipControl.HandleGetDispInfo(ref Message)' + + + + + + + + + + + + + + 'window' + 'ToolTipControl.PopToolTip(IWin32Window)' + + + + + + + + + + + 'ToolTipControl.StandardIcons' + + + + + + + + + + + 'List<TreeListView.Branch>' + 'TreeListView.Branch.ChildBranches' + + + + + + + + + 'List<TreeListView.Branch>' + 'TreeListView.Branch.FilteredChildBranches' + + + + + + + + + Flag + 'TreeListView.Branch.ManageLastChildFlag(MethodInvoker)' + + + + + + + + + + + Flags + 'TreeListView.Branch.BranchFlags' + + + + + + + + + + + 'TreeListView.Tree.GetBranchComparer()' + + + + + + + + + + + + + + + + + + 'TreeListView.TreeRenderer.PIXELS_PER_LEVEL' + + + + + + + + + + + + + 'TypedObjectListView<T>.BooleanCheckStateGetter' + + + + + + + + + 'TypedObjectListView<T>.BooleanCheckStatePutter' + + + + + + + + + 'TypedObjectListView<T>.CellToolTipGetter' + + + + + + + + + 'TypedObjectListView<T>.CheckedObjects' + + + + + + + + + + + + + + 'TypedObjectListView<T>.SelectedObjects' + + + + + + + + + + + + + + + + + + + + 'UintUpDown.Value.get()' + + + + + + + + + + + + + + 'UintUpDown.Value.set(uint)' + + + + + + + + + + + + + + + + + + + + 'VirtualObjectListView.CheckedObjects' + + + + + + + + + + + + + + 'VirtualObjectListView.HandleCacheVirtualItems(object, CacheVirtualItemsEventArgs)' + + + + + + + + + 'VirtualObjectListView.HandleRetrieveVirtualItem(object, RetrieveVirtualItemEventArgs)' + + + + + + + + + 'VirtualObjectListView.HandleSearchForVirtualItem(object, SearchForVirtualItemEventArgs)' + + + + + + + + + 'VirtualObjectListView.SetVirtualListSize(int)' + 'Exception' + + + + + + + + + + + + + + + + + + + + + These should be methods rather than properties + The out parameter is necessary since this method returns two pieces of information: the item under the point and the subitem item too + This is used to ensure we understand the newly load state. + All these properties should be assignable. + Our project is build with unsafe code enabled, so it automatically has the SecurityProperty set + These initializations are not unnecessary + We have to pass the windows message by reference + Instances of this class do not need to be disposable + These are utility methods that could well be used at runtime + Old style constants. Can't change now + These are OK like this. We need List<>, not IList<> since only List has a ToArray() method + Legacy cases that have to be kept like this + These are acceptable as methods rather than properties + windows messages should be passed by reference + This is not a security risk + There are several problems that can occur here and we want to ignore them all + These spellings are acceptable + These will only be used by OL types + + + Not appropriate here + Can't change now + we want to catch everthing + MS! + not flags + MS! + MS + + + + + Sign {0} with a strong name key. + + + Consider a design that does not require that {0} be an out parameter. + + + {0} appears to have no upstream public or protected callers. + + + Seal {0}, if possible. + + + It appears that field {0} is never used or is only ever assigned to. Use this field or remove it. + + + Change {0} to be read-only by removing the property setter. + + + Consider changing the type of parameter {0} in {1} from {2} to its base type {3}. This method appears to only require base class members in its implementation. Suppress this violation if there is a compelling reason to require the more derived type in the method signature. + + + Remove the property setter from {0} or reduce its accessibility because it corresponds to positional argument {1}. + + + {0}, a parameter, is cast to type {1} multiple times in method {2}. Cache the result of the 'as' operator or direct cast in order to eliminate the redundant {3} instruction. + + + Modify {0} to catch a more specific exception than {1} or rethrow the exception. + + + Change {0} in {1} to use Collection<T>, ReadOnlyCollection<T> or KeyedCollection<K,V> + + + {0} calls {1} but does not use the HRESULT or error code that the method returns. This could lead to unexpected behavior in error conditions or low-resource situations. Use the result in a conditional statement, assign the result to a variable, or pass it as an argument to another method. + {0} creates a new instance of {1} which is never used. Pass the instance as an argument to another method, assign the instance to a variable, or remove the object creation if it is unnecessary. + + + + {0} initializes field {1} of type {2} to {3}. Remove this initialization because it will be done automatically by the runtime. + + + {0} is marked with FlagsAttribute but a discrete member cannot be found for every settable bit that is used across the range of enum values. Remove FlagsAttribute from the type or define new members for the following (currently missing) values: {1} + + + + Modify the call to {0} in method {1} to set the timer interval to a value that's greater than or equal to one second. + + + In enum {0}, change the name of {1} to 'None'. + + + If enumeration name {0} is singular, change it to a plural form. + + + Correct the spelling of '{0}' in member name {1} or remove it entirely if it represents any sort of Hungarian notation. + In method {0}, correct the spelling of '{1}' in parameter name {2} or remove it entirely if it represents any sort of Hungarian notation. + Correct the spelling of '{0}' in type name {1}. + + + + Make {0} sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of {1} and is visible to derived classes. + + + Specify AttributeUsage on {0}. + + + Add the MarshalAsAttribute to parameter {0} of P/Invoke {1}. If the corresponding unmanaged parameter is a 4-byte Win32 'BOOL', use [MarshalAs(UnmanagedType.Bool)]. For a 1-byte C++ 'bool', use MarshalAs(UnmanagedType.U1). + Add the MarshalAsAttribute to the return type of P/Invoke {0}. If the corresponding unmanaged return type is a 4-byte Win32 'BOOL', use MarshalAs(UnmanagedType.Bool). For a 1-byte C++ 'bool', use MarshalAs(UnmanagedType.U1). + + + The constituent members of {0} appear to represent flags that can be combined rather than discrete values. If this is correct, mark the enumeration with FlagsAttribute. + + + Add [Serializable] to {0} as this type implements ISerializable. + + + Consider making {0} non-public or a constant. + + + If the name {0} is plural, change it to its singular form. + + + Add the following security attribute to {0} in order to match a LinkDemand on base method {1}: {2}. + + + As it is declared in your code, parameter {0} of P/Invoke {1} will be {2} bytes wide on {3} platforms. This is not correct, as the actual native declaration of this API indicates it should be {4} bytes wide on {3} platforms. Consult the MSDN Platform SDK documentation for help determining what data type should be used instead of {5}. + As it is declared in your code, the return type of P/Invoke {0} will be {1} bytes wide on {2} platforms. This is not correct, as the actual native declaration of this API indicates it should be {3} bytes wide on {2} platforms. Consult the MSDN Platform SDK documentation for help determining what data type should be used instead of {4}. + + + Correct the declaration of {0} so that it correctly points to an existing entry point in {1}. The unmanaged entry point name currently linked to is {2}. + + + Because property {0} is write-only, either add a property getter with an accessibility that is greater than or equal to its setter or convert this property into a method. + + + Change {0} to return a collection or make it a method. + + + The property name {0} is confusing given the existence of inherited method {1}. Rename or remove this property. + The property name {0} is confusing given the existence of method {1}. Rename or remove one of these members. + + + Parameter {0} of {1} is never used. Remove the parameter or use it in the method body. + + + Consider making {0} not externally visible. + + + To reduce security risk, marshal parameter {0} as Unicode, by setting DllImport.CharSet to CharSet.Unicode, or by explicitly marshaling the parameter as UnmanagedType.LPWStr. If you need to marshal this string as ANSI or system-dependent, set BestFitMapping=false; for added security, also set ThrowOnUnmappableChar=true. + + + {0} makes a call to {1} that does not explicitly provide a StringComparison. This should be replaced with a call to {2}. + + + Implement IDisposable on {0} because it creates members of the following IDisposable types: {1}. If {0} has previously shipped, adding new members that implement IDisposable to this type is considered a breaking change to existing consumers. + + + Change the type of parameter {0} of method {1} from string to System.Uri, or provide an overload of {1}, that allows {0} to be passed as a System.Uri object. + + + Change the type of property {0} from string to System.Uri. + + + Remove {0} and replace its usage with EventHandler<T> + + + {0} passes {1} as an argument to {2}. Replace this usage with StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase if appropriate. + + + Replace the term '{0}' in member name {1} with an appropriate alternate or remove it entirely. + Replace the term '{0}' in type name {1} with an appropriate alternate or remove it entirely. + + + Change {0} to a property if appropriate. + + + + diff --git a/ObjectListView/ObjectListView.cs b/ObjectListView/ObjectListView.cs new file mode 100644 index 0000000..d3ac03a --- /dev/null +++ b/ObjectListView/ObjectListView.cs @@ -0,0 +1,10482 @@ +/* + * ObjectListView - A listview to show various aspects of a collection of objects + * + * Author: Phillip Piper + * Date: 9/10/2006 11:15 AM + * + * Change log + * 2014-10-11 JPP - Fixed some XP-only flicker issues + * 2014-09-26 JPP - Fixed intricate bug involving checkboxes on non-owner-drawn virtual lists. + * - Fixed long standing (but previously unreported) error on non-details virtual lists where + * users could not click on checkboxes. + * 2014-09-07 JPP - (Major) Added ability to have checkboxes in headers + * - CellOver events are raised when the mouse moves over the header. Set TriggerCellOverEventsWhenOverHeader + * to false to disable this behaviour. + * - Freeze/Unfreeze now use BeginUpdate/EndUpdate to disable Window level drawing + * - Changed default value of ObjectListView.HeaderUsesThemes from true to false. Too many people were + * being confused, trying to make something interesting appear in the header and nothing showing up + * 2014-08-04 JPP - Final attempt to fix the multiple hyperlink events being raised. This involves turning + * a NM_CLICK notification into a NM_RCLICK. + * 2014-05-21 JPP - (Major) Added ability to disable rows. DisabledObjects, DisableObjects(), DisabledItemStyle. + * 2014-04-25 JPP - Fixed issue where virtual lists containing a single row didn't update hyperlinks on MouseOver + * - Added sanity check before BuildGroups() + * 2014-03-22 JPP - Fixed some subtle bugs resulting from misuse of TryGetValue() + * 2014-03-09 JPP - Added CollapsedGroups property + * - Several minor Resharper complaints quiesced. + * v2.7 + * 2014-02-14 JPP - Fixed issue with ShowHeaderInAllViews (another one!) where setting it to false caused the list to lose + * its other extended styles, leading to nasty flickering and worse. + * 2014-02-06 JPP - Fix issue on virtual lists where the filter was not correctly reapplied after columns were added or removed. + * - Made disposing of cell editors optional (defaults to true). This allows controls to be cached and reused. + * - Bracketed column resizing with BeginUpdate/EndUpdate to smooth redraws (thanks to Davide) + * 2014-02-01 JPP - Added static property ObjectListView.GroupTitleDefault to allow the default group title to be localised. + * 2013-09-24 JPP - Fixed issue in RefreshObjects() when model objects overrode the Equals()/GetHashCode() methods. + * - Made sure get state checker were used when they should have been + * 2013-04-21 JPP - Clicking on a non-groupable column header when showing groups will now sort + * the group contents by that column. + * v2.6 + * 2012-08-16 JPP - Added ObjectListView.EditModel() -- a convienence method to start an edit operation on a model + * 2012-08-10 JPP - Don't trigger selection changed events during sorting/grouping or add/removing columns + * 2012-08-06 JPP - Don't start a cell edit operation when the user clicks on the background of a checkbox cell. + * - Honor values from the BeforeSorting event when calling a CustomSorter + * 2012-08-02 JPP - Added CellVerticalAlignment and CellPadding properties. + * 2012-07-04 JPP - Fixed issue with cell editing where the cell editing didn't finish until the first idle event. + * This meant that if you clicked and held on the scroll thumb to finish a cell edit, the editor + * wouldn't be removed until the mouse was released. + * 2012-07-03 JPP - Fixed issue with SingleClick cell edit mode where the cell editing would not begin until the + * mouse moved after the click. + * 2012-06-25 JPP - Fixed bug where removing a column from a LargeIcon or SmallIcon view would crash the control. + * 2012-06-15 JPP - Added Reset() method, which definitively removes all rows *and* columns from an ObjectListView. + * 2012-06-11 JPP - Added FilteredObjects property which returns the collection of objects that survives any installed filters. + * 2012-06-04 JPP - [Big] Added UseNotifyPropertyChanged to allow OLV to listen for INotifyPropertyChanged events on models. + * 2012-05-30 JPP - Added static property ObjectListView.IgnoreMissingAspects. If this is set to true, all + * ObjectListViews will silently ignore missing aspect errors. Read the remarks to see why this would be useful. + * 2012-05-23 JPP - Setting UseFilterIndicator to true now sets HeaderUsesTheme to false. + * Also, changed default value of UseFilterIndicator to false. Previously, HeaderUsesTheme and UseFilterIndicator + * defaulted to true, which was pointless since when the HeaderUsesTheme is true, UseFilterIndicator does nothing. + * v2.5.1 + * 2012-05-06 JPP - Fix bug where collapsing the first group would cause decorations to stop being drawn (SR #3502608) + * 2012-04-23 JPP - Trigger GroupExpandingCollapsing event to allow the expand/collapse to be cancelled + * - Fixed SetGroupSpacing() so it corrects updates the space between all groups. + * - ResizeLastGroup() now does nothing since it was broken and I can't remember what it was + * even supposed to do :) + * 2012-04-18 JPP - Upgraded hit testing to include hits on groups. + * - HotItemChanged is now correctly recalculated on each mouse move. Includes "hot" group information. + * 2012-04-14 JPP - Added GroupStateChanged event. Useful for knowing when a group is collapsed/expanded. + * - Added AdditionalFilter property. This filter is combined with the Excel-like filtering that + * the end user might enact at runtime. + * 2012-04-10 JPP - Added PersistentCheckBoxes property to allow primary checkboxes to remember their values + * across list rebuilds. + * 2012-04-05 JPP - Reverted some code to .NET 2.0 standard. + * - Tweaked some code + * 2012-02-05 JPP - Fixed bug when selecting a separator on a drop down menu + * 2011-06-24 JPP - Added CanUseApplicationIdle property to cover cases where Application.Idle events + * are not triggered. For example, when used within VS (and probably Office) extensions + * Application.Idle is never triggered. Set CanUseApplicationIdle to false to handle + * these cases. + * - Handle cases where a second tool tip is installed onto the ObjectListView. + * - Correctly recolour rows after an Insert or Move + * - Removed m.LParam cast which could cause overflow issues on Win7/64 bit. + * v2.5.0 + * 2011-05-31 JPP - SelectObject() and SelectObjects() no longer deselect all other rows. + Set the SelectedObject or SelectedObjects property to do that. + * - Added CheckedObjectsEnumerable + * - Made setting CheckedObjects more efficient on large collections + * - Deprecated GetSelectedObject() and GetSelectedObjects() + * 2011-04-25 JPP - Added SubItemChecking event + * - Fixed bug in handling of NewValue on CellEditFinishing event + * 2011-04-12 JPP - Added UseFilterIndicator + * - Added some more localizable messages + * 2011-04-10 JPP - FormatCellEventArgs now has a CellValue property, which is the model value displayed + * by the cell. For example, for the Birthday column, the CellValue might be + * DateTime(1980, 12, 31), whereas the cell's text might be 'Dec 31, 1980'. + * 2011-04-04 JPP - Tweaked UseTranslucentSelection and UseTranslucentHotItem to look (a little) more + * like Vista/Win7. + * - Alternate colours are now only applied in Details view (as they always should have been) + * - Alternate colours are now correctly recalculated after removing objects + * 2011-03-29 JPP - Added SelectColumnsOnRightClickBehaviour to allow the selecting of columns mechanism + * to be changed. Can now be InlineMenu (the default), SubMenu, or ModelDialog. + * - ColumnSelectionForm was moved from the demo into the ObjectListView project itself. + * - Ctrl-C copying is now able to use the DragSource to create the data transfer object. + * 2011-03-19 JPP - All model object comparisons now use Equals rather than == (thanks to vulkanino) + * - [Small Break] GetNextItem() and GetPreviousItem() now accept and return OLVListView + * rather than ListViewItems. + * 2011-03-07 JPP - [Big] Added Excel-style filtering. Right click on a header to show a Filtering menu. + * - Added CellEditKeyEngine to allow key handling when cell editing to be completely customised. + * Add CellEditTabChangesRows and CellEditEnterChangesRows to show some of these abilities. + * 2011-03-06 JPP - Added OLVColumn.AutoCompleteEditorMode in preference to AutoCompleteEditor + * (which is now just a wrapper). Thanks to Clive Haskins + * - Added lots of docs to new classes + * 2011-02-25 JPP - Preserve word wrap settings on TreeListView + * - Resize last group to keep it on screen (thanks to ?) + * 2010-11-16 JPP - Fixed (once and for all) DisplayIndex problem with Generator + * - Changed the serializer used in SaveState()/RestoreState() so that it resolves on + * class name alone. + * - Fixed bug in GroupWithItemCountSingularFormatOrDefault + * - Fixed strange flickering in grouped, owner drawn OLV's using RefreshObject() + * v2.4.1 + * 2010-08-25 JPP - Fixed bug where setting OLVColumn.CheckBoxes to false gave it a renderer + * specialized for checkboxes. Oddly, this made Generator created owner drawn + * lists appear to be completely empty. + * - In IDE, all ObjectListView properties are now in a single "ObjectListView" category, + * rather than splitting them between "Appearance" and "Behavior" categories. + * - Added GroupingParameters.GroupComparer to allow groups to be sorted in a customizable fashion. + * - Sorting of items within a group can be disabled by setting + * GroupingParameters.PrimarySortOrder to None. + * 2010-08-24 JPP - Added OLVColumn.IsHeaderVertical to make a column draw its header vertical. + * - Added OLVColumn.HeaderTextAlign to control the alignment of a column's header text. + * - Added HeaderMaximumHeight to limit how tall the header section can become + * 2010-08-18 JPP - Fixed long standing bug where having 0 columns caused a InvalidCast exception. + * - Added IncludeAllColumnsInDataObject property + * - Improved BuildList(bool) so that it preserves scroll position even when + * the listview is grouped. + * 2010-08-08 JPP - Added OLVColumn.HeaderImageKey to allow column headers to have an image. + * - CellEdit validation and finish events now have NewValue property. + * 2010-08-03 JPP - Subitem checkboxes improvments: obey IsEditable, can be hot, can be disabled. + * - No more flickering of selection when tabbing between cells + * - Added EditingCellBorderDecoration to make it clearer which cell is being edited. + * 2010-08-01 JPP - Added ObjectListView.SmoothingMode to control the smoothing of all graphics + * operations + * - Columns now cache their group item format strings so that they still work as + * grouping columns after they have been removed from the listview. This cached + * value is only used when the column is not part of the listview. + * 2010-07-25 JPP - Correctly trigger a Click event when the mouse is clicked. + * 2010-07-16 JPP - Invalidate the control before and after cell editing to make sure it looks right + * 2010-06-23 JPP - Right mouse clicks on checkboxes no longer confuse them + * 2010-06-21 JPP - Avoid bug in underlying ListView control where virtual lists in SmallIcon view + * generate GETTOOLINFO msgs with invalid item indicies. + * - Fixed bug where FastObjectListView would throw an exception when showing hyperlinks + * in any view except Details. + * 2010-06-15 JPP - Fixed bug in ChangeToFilteredColumns() that resulted in column display order + * being lost when a column was hidden. + * - Renamed IsVista property to IsVistaOrLater which more accurately describes its function. + * v2.4 + * 2010-04-14 JPP - Prevent object disposed errors when mouse event handlers cause the + * ObjectListView to be destroyed (e.g. closing a form during a + * double click event). + * - Avoid checkbox munging bug in standard ListView when shift clicking on non-primary + * columns when FullRowSelect is true. + * 2010-04-12 JPP - Fixed bug in group sorting (thanks Mike). + * 2010-04-07 JPP - Prevent hyperlink processing from triggering spurious MouseUp events. + * This showed itself by launching the same url multiple times. + * 2010-04-06 JPP - Space filling columns correctly resize upon initial display + * - ShowHeaderInAllViews is better but still not working reliably. + * See comments on property for more details. + * 2010-03-23 JPP - Added ObjectListView.HeaderFormatStyle and OLVColumn.HeaderFormatStyle. + * This makes HeaderFont and HeaderForeColor properties unnecessary -- + * they will be marked obsolete in the next version and removed after that. + * 2010-03-16 JPP - Changed object checking so that objects can be pre-checked before they + * are added to the list. Normal ObjectListViews managed "checkedness" in + * the ListViewItem, so this won't work for them, unless check state getters + * and putters have been installed. It will work on on virtual lists (thus fast lists and + * tree views) since they manage their own check state. + * 2010-03-06 JPP - Hide "Items" and "Groups" from the IDE properties grid since they shouldn't be set like that. + * They can still be accessed through "Custom Commands" and there's nothing we can do + * about that. + * 2010-03-05 JPP - Added filtering + * 2010-01-18 JPP - Overlays can be turned off. They also only work on 32-bit displays + * v2.3 + * 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics() + * 2009-10-28 JPP - Fix bug when right clicking in the empty area of the header + * 2009-10-20 JPP - Redraw the control after setting EmptyListMsg property + * v2.3 + * 2009-09-30 JPP - Added Dispose() method to properly release resources + * 2009-09-16 JPP - Added OwnerDrawnHeader, which you can set to true if you want to owner draw + * the header yourself. + * 2009-09-15 JPP - Added UseExplorerTheme, which allow complete visual compliance with Vista explorer. + * But see property documentation for its many limitations. + * - Added ShowHeaderInAllViews. To make this work, Columns are no longer + * changed when switching to/from Tile view. + * 2009-09-11 JPP - Added OLVColumn.AutoCompleteEditor to allow the autocomplete of cell editors + * to be disabled. + * 2009-09-01 JPP - Added ObjectListView.TextRenderingHint property which controls the + * text rendering hint of all drawn text. + * 2009-08-28 JPP - [BIG] Added group formatting to supercharge what is possible with groups + * - [BIG] Virtual groups now work + * - Extended MakeGroupies() to handle more aspects of group creation + * 2009-08-19 JPP - Added ability to show basic column commands when header is right clicked + * - Added SelectedRowDecoration, UseTranslucentSelection and UseTranslucentHotItem. + * - Added PrimarySortColumn and PrimarySortOrder + * 2009-08-15 JPP - Correct problems with standard hit test and subitems + * 2009-08-14 JPP - [BIG] Support Decorations + * - [BIG] Added header formatting capabilities: font, color, word wrap + * - Gave ObjectListView its own designer to hide unwanted properties + * - Separated design time stuff into separate file + * - Added FormatRow and FormatCell events + * 2009-08-09 JPP - Get around bug in HitTest when not FullRowSelect + * - Added OLVListItem.GetSubItemBounds() method which works correctly + * for all columns including column 0 + * 2009-08-07 JPP - Added Hot* properties that track where the mouse is + * - Added HotItemChanged event + * - Overrode TextAlign on columns so that column 0 can have something other + * than just left alignment. This is only honored when owner drawn. + * v2.2.1 + * 2009-08-03 JPP - Subitem edit rectangles always allowed for an image in the cell, even if there was none. + * Now they only allow for an image when there actually is one. + * - Added Bounds property to OLVListItem which handles items being part of collapsed groups. + * 2009-07-29 JPP - Added GetSubItem() methods to ObjectListView and OLVListItem + * 2009-07-26 JPP - Avoided bug in .NET framework involving column 0 of owner drawn listviews not being + * redrawn when the listview was scrolled horizontally (this was a LOT of work to track + * down and fix!) + * - The cell edit rectangle is now correctly calculated when the listview is scrolled + * horizontally. + * 2009-07-14 JPP - If the user clicks/double clicks on a tree list cell, an edit operation will no longer begin + * if the click was to the left of the expander. This is implemented in such a way that + * other renderers can have similar "dead" zones. + * 2009-07-11 JPP - CalculateCellBounds() messed with the FullRowSelect property, which confused the + * tooltip handling on the underlying control. It no longer does this. + * - The cell edit rectangle is now correctly calculated for owner-drawn, non-Details views. + * 2009-07-08 JPP - Added Cell events (CellClicked, CellOver, CellRightClicked) + * - Made BuildList(), AddObject() and RemoveObject() thread-safe + * 2009-07-04 JPP - Space bar now properly toggles checkedness of selected rows + * 2009-07-02 JPP - Fixed bug with tooltips when the underlying Windows control was destroyed. + * - CellToolTipShowing events are now triggered in all views. + * v2.2 + * 2009-06-02 JPP - BeforeSortingEventArgs now has a Handled property to let event handlers do + * the item sorting themselves. + * - AlwaysGroupByColumn works again, as does SortGroupItemsByPrimaryColumn and all their + * various permutations. + * - SecondarySortOrder and SecondarySortColumn are now "null" by default + * 2009-05-15 JPP - Fixed bug so that KeyPress events are again triggered + * 2009-05-10 JPP - Removed all unsafe code + * 2009-05-07 JPP - Don't use glass panel for overlays when in design mode. It's too confusing. + * 2009-05-05 JPP - Added Scroll event (thanks to Christophe Hosten for the complete patch to implement this) + * - Added Unfocused foreground and background colors (also thanks to Christophe Hosten) + * 2009-04-29 JPP - Added SelectedColumn property, which puts a slight tint on that column. Combine + * this with TintSortColumn property and the sort column is automatically tinted. + * - Use an overlay to implement "empty list" msg. Default empty list msg is now prettier. + * 2009-04-28 JPP - Fixed bug where DoubleClick events were not triggered when CheckBoxes was true + * 2009-04-23 JPP - Fixed various bugs under Vista. + * - Made groups collapsible - Vista only. Thanks to Crustyapplesniffer. + * - Forward events from DropSink to the control itself. This allows handlers to be defined + * within the IDE for drop events + * 2009-04-16 JPP - Made several properties localizable. + * 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard + * 2009-04-11 JPP - Implemented overlay architecture, based on CustomDraw scheme. + * This unified drag drop feedback, empty list msgs and overlay images. + * - Added OverlayImage and friends, which allows an image to be drawn + * transparently over the listview + * 2009-04-10 JPP - Fixed long-standing annoying flicker on owner drawn virtual lists! + * This means, amongst other things, that grid lines no longer get confused, + * and drag-select no longer flickers. + * 2009-04-07 JPP - Calculate edit rectangles more accurately + * 2009-04-06 JPP - Double-clicking no longer toggles the checkbox + * - Double-clicking on a checkbox no longer confuses the checkbox + * 2009-03-16 JPP - Optimized the build of autocomplete lists + * v2.1 + * 2009-02-24 JPP - Fix bug where double-clicking VERY quickly on two different cells + * could give two editors + * - Maintain focused item when rebuilding list (SF #2547060) + * 2009-02-22 JPP - Reworked checkboxes so that events are triggered for virtual lists + * 2009-02-15 JPP - Added ObjectListView.ConfigureAutoComplete utility method + * 2009-02-02 JPP - Fixed bug with AlwaysGroupByColumn where column header clicks would not resort groups. + * 2009-02-01 JPP - OLVColumn.CheckBoxes and TriStateCheckBoxes now work. + * 2009-01-28 JPP - Complete overhaul of renderers! + * - Use IRenderer + * - Added ObjectListView.ItemRenderer to draw whole items + * 2009-01-23 JPP - Simple Checkboxes now work properly + * - Added TriStateCheckBoxes property to control whether the user can + * set the row checkbox to have the Indeterminate value + * - CheckState property is now just a wrapper around the StateImageIndex property + * 2009-01-20 JPP - Changed to always draw columns when owner drawn, rather than falling back on DrawDefault. + * This simplified several owner drawn problems + * - Added DefaultRenderer property to help with the above + * - HotItem background color is applied to all cells even when FullRowSelect is false + * - Allow grouping by CheckedAspectName columns + * - Commented out experimental animations. Still needs work. + * 2009-01-17 JPP - Added HotItemStyle and UseHotItem to highlight the row under the cursor + * - Added UseCustomSelectionColors property + * - Owner draw mode now honors ForeColor and BackColor settings on the list + * 2009-01-16 JPP - Changed to use EditorRegistry rather than hard coding cell editors + * 2009-01-10 JPP - Changed to use Equals() method rather than == to compare model objects. + * v2.0.1 + * 2009-01-08 JPP - Fixed long-standing "multiple columns generated" problem. + * Thanks to pinkjones for his help with solving this one! + * - Added EnsureGroupVisible() + * 2009-01-07 JPP - Made all public and protected methods virtual + * - FinishCellEditing, PossibleFinishCellEditing and CancelCellEditing are now public + * 2008-12-20 JPP - Fixed bug with group comparisons when a group key was null (SF#2445761) + * 2008-12-19 JPP - Fixed bug with space filling columns and layout events + * - Fixed RowHeight so that it only changes the row height, not the width of the images. + * v2.0 + * 2008-12-10 JPP - Handle Backspace key. Resets the seach-by-typing state without delay + * - Made some changes to the column collection editor to try and avoid + * the multiple column generation problem. + * - Updated some documentation + * 2008-12-07 JPP - Search-by-typing now works when showing groups + * - Added BeforeSearching and AfterSearching events which are triggered when the user types + * into the list. + * - Added secondary sort information to Before/AfterSorting events + * - Reorganized group sorting code. Now triggers Sorting events. + * - Added GetItemIndexInDisplayOrder() + * - Tweaked in the interaction of the column editor with the IDE so that we (normally) + * don't rely on a hack to find the owning ObjectListView + * - Changed all 'DefaultValue(typeof(Color), "Empty")' to 'DefaultValue(typeof(Color), "")' + * since the first does not given Color.Empty as I thought, but the second does. + * 2008-11-28 JPP - Fixed long standing bug with horizontal scrollbar when shrinking the window. + * (thanks to Bartosz Borowik) + * 2008-11-25 JPP - Added support for dynamic tooltips + * - Split out comparers and header controls stuff into their own files + * 2008-11-21 JPP - Fixed bug where enabling grouping when there was not a sort column would not + * produce a grouped list. Grouping column now defaults to column 0. + * - Preserve selection on virtual lists when sorting + * 2008-11-20 JPP - Added ability to search by sort column to ObjectListView. Unified this with + * ability that was already in VirtualObjectListView + * 2008-11-19 JPP - Fixed bug in ChangeToFilteredColumns() where DisplayOrder was not always restored correctly. + * 2008-10-29 JPP - Event argument blocks moved to directly within the namespace, rather than being + * nested inside ObjectListView class. + * - Removed OLVColumn.CellEditor since it was never used. + * - Marked OLVColumn.AspectGetterAutoGenerated as obsolete (it has not been used for + * several versions now). + * 2008-10-28 JPP - SelectedObjects is now an IList, rather than an ArrayList. This allows + * it to accept generic list (eg List). + * 2008-10-09 JPP - Support indeterminate checkbox values. + * [BREAKING CHANGE] CheckStateGetter/CheckStatePutter now use CheckState types only. + * BooleanCheckStateGetter and BooleanCheckStatePutter added to ease transition. + * 2008-10-08 JPP - Added setFocus parameter to SelectObject(), which allows focus to be set + * at the same time as selecting. + * 2008-09-27 JPP - BIG CHANGE: Fissioned this file into separate files for each component + * 2008-09-24 JPP - Corrected bug with owner drawn lists where a column 0 with a renderer + * would draw at column 0 even if column 0 was dragged to another position. + * - Correctly handle space filling columns when columns are added/removed + * 2008-09-16 JPP - Consistently use try..finally for BeginUpdate()/EndUpdate() pairs + * 2008-08-24 JPP - If LastSortOrder is None when adding objects, don't force a resort. + * 2008-08-22 JPP - Catch and ignore some problems with setting TopIndex on FastObjectListViews. + * 2008-08-05 JPP - In the right-click column select menu, columns are now sorted by display order, rather than alphabetically + * v1.13 + * 2008-07-23 JPP - Consistently use copy-on-write semantics with Add/RemoveObject methods + * 2008-07-10 JPP - Enable validation on cell editors through a CellEditValidating event. + * (thanks to Artiom Chilaru for the initial suggestion and implementation). + * 2008-07-09 JPP - Added HeaderControl.Handle to allow OLV to be used within UserControls. + * (thanks to Michael Coffey for tracking this down). + * 2008-06-23 JPP - Split the more generally useful CopyObjectsToClipboard() method + * out of CopySelectionToClipboard() + * 2008-06-22 JPP - Added AlwaysGroupByColumn and AlwaysGroupBySortOrder, which + * force the list view to always be grouped by a particular column. + * 2008-05-31 JPP - Allow check boxes on FastObjectListViews + * - Added CheckedObject and CheckedObjects properties + * 2008-05-11 JPP - Allow selection foreground and background colors to be changed. + * Windows doesn't allow this, so we can only make it happen when owner + * drawing. Set the HighlightForegroundColor and HighlightBackgroundColor + * properties and then call EnableCustomSelectionColors(). + * v1.12 + * 2008-05-08 JPP - Fixed bug where the column select menu would not appear if the + * ObjectListView has a context menu installed. + * 2008-05-05 JPP - Non detail views can now be owner drawn. The renderer installed for + * primary column is given the chance to render the whole item. + * See BusinessCardRenderer in the demo for an example. + * - BREAKING CHANGE: RenderDelegate now returns a bool to indicate if default + * rendering should be done. Previously returned void. Only important if your + * code used RendererDelegate directly. Renderers derived from BaseRenderer + * are unchanged. + * 2008-05-03 JPP - Changed cell editing to use values directly when the values are Strings. + * Previously, values were always handed to the AspectToStringConverter. + * - When editing a cell, tabbing no longer tries to edit the next subitem + * when not in details view! + * 2008-05-02 JPP - MappedImageRenderer can now handle a Aspects that return a collection + * of values. Each value will be drawn as its own image. + * - Made AddObjects() and RemoveObjects() work for all flavours (or at least not crash) + * - Fixed bug with clearing virtual lists that has been scrolled vertically + * - Made TopItemIndex work with virtual lists. + * 2008-05-01 JPP - Added AddObjects() and RemoveObjects() to allow faster mods to the list + * - Reorganised public properties. Now alphabetical. + * - Made the class ObjectListViewState internal, as it always should have been. + * v1.11 + * 2008-04-29 JPP - Preserve scroll position when building the list or changing columns. + * - Added TopItemIndex property. Due to problems with the underlying control, this + * property is not always reliable. See property docs for info. + * 2008-04-27 JPP - Added SelectedIndex property. + * - Use a different, more general strategy to handle Invoke(). Removed all delegates + * that were only declared to support Invoke(). + * - Check all native structures for 64-bit correctness. + * 2008-04-25 JPP - Released on SourceForge. + * 2008-04-13 JPP - Added ColumnRightClick event. + * - Made the assembly CLS-compliant. To do this, our cell editors were made internal, and + * the constraint on FlagRenderer template parameter was removed (the type must still + * be an IConvertible, but if it isn't, the error will be caught at runtime, not compile time). + * 2008-04-12 JPP - Changed HandleHeaderRightClick() to have a columnIndex parameter, which tells + * exactly which column was right-clicked. + * 2008-03-31 JPP - Added SaveState() and RestoreState() + * - When cell editing, scrolling with a mouse wheel now ends the edit operation. + * v1.10 + * 2008-03-25 JPP - Added space filling columns. See OLVColumn.FreeSpaceProportion property for details. + * A space filling columns fills all (or a portion) of the width unoccupied by other columns. + * 2008-03-23 JPP - Finished tinkering with support for Mono. Compile with conditional compilation symbol 'MONO' + * to enable. On Windows, current problems with Mono: + * - grid lines on virtual lists crashes + * - when grouped, items sometimes are not drawn when any item is scrolled out of view + * - i can't seem to get owner drawing to work + * - when editing cell values, the editing controls always appear behind the listview, + * where they function fine -- the user just can't see them :-) + * 2008-03-16 JPP - Added some methods suggested by Chris Marlowe (thanks for the suggestions Chris) + * - ClearObjects() + * - GetCheckedObject(), GetCheckedObjects() + * - GetItemAt() variation that gets both the item and the column under a point + * 2008-02-28 JPP - Fixed bug with subitem colors when using OwnerDrawn lists and a RowFormatter. + * v1.9.1 + * 2008-01-29 JPP - Fixed bug that caused owner-drawn virtual lists to use 100% CPU + * - Added FlagRenderer to help draw bitwise-OR'ed flag values + * 2008-01-23 JPP - Fixed bug (introduced in v1.9) that made alternate row colour with groups not quite right + * - Ensure that DesignerSerializationVisibility.Hidden is set on all non-browsable properties + * - Make sure that sort indicators are shown after changing which columns are visible + * 2008-01-21 JPP - Added FastObjectListView + * v1.9 + * 2008-01-18 JPP - Added IncrementalUpdate() + * 2008-01-16 JPP - Right clicking on column header will allow the user to choose which columns are visible. + * Set SelectColumnsOnRightClick to false to prevent this behaviour. + * - Added ImagesRenderer to draw more than one images in a column + * - Changed the positioning of the empty list m to use all the client area. Thanks to Matze. + * 2007-12-13 JPP - Added CopySelectionToClipboard(). Ctrl-C invokes this method. Supports text + * and HTML formats. + * 2007-12-12 JPP - Added support for checkboxes via CheckStateGetter and CheckStatePutter properties. + * - Made ObjectListView and OLVColumn into partial classes so that others can extend them. + * 2007-12-09 JPP - Added ability to have hidden columns, i.e. columns that the ObjectListView knows + * about but that are not visible to the user. Controlled by OLVColumn.IsVisible. + * Added ColumnSelectionForm to the project to show how it could be used in an application. + * + * v1.8 + * 2007-11-26 JPP - Cell editing fully functional + * 2007-11-21 JPP - Added SelectionChanged event. This event is triggered once when the + * selection changes, no matter how many items are selected or deselected (in + * contrast to SelectedIndexChanged which is called once for every row that + * is selected or deselected). Thanks to lupokehl42 (Daniel) for his suggestions and + * improvements on this idea. + * 2007-11-19 JPP - First take at cell editing + * 2007-11-17 JPP - Changed so that items within a group are not sorted if lastSortOrder == None + * - Only call MakeSortIndicatorImages() if we haven't already made the sort indicators + * (Corrected misspelling in the name of the method too) + * 2007-11-06 JPP - Added ability to have secondary sort criteria when sorting + * (SecondarySortColumn and SecondarySortOrder properties) + * - Added SortGroupItemsByPrimaryColumn to allow group items to be sorted by the + * primary column. Previous default was to sort by the grouping column. + * v1.7 + * No big changes to this version but made to work with ListViewPrinter and released with it. + * + * 2007-11-05 JPP - Changed BaseRenderer to use DrawString() rather than TextRenderer, since TextRenderer + * does not work when printing. + * v1.6 + * 2007-11-03 JPP - Fixed some bugs in the rebuilding of DataListView. + * 2007-10-31 JPP - Changed to use builtin sort indicators on XP and later. This also avoids alignment + * problems on Vista. (thanks to gravybod for the suggestion and example implementation) + * 2007-10-21 JPP - Added MinimumWidth and MaximumWidth properties to OLVColumn. + * - Added ability for BuildList() to preserve selection. Calling BuildList() directly + * tries to preserve selection; calling SetObjects() does not. + * - Added SelectAll() and DeselectAll() methods. Useful for working with large lists. + * 2007-10-08 JPP - Added GetNextItem() and GetPreviousItem(), which walk sequentially through the + * listview items, even when the view is grouped. + * - Added SelectedItem property + * 2007-09-28 JPP - Optimized aspect-to-string conversion. BuildList() 15% faster. + * - Added empty implementation of RefreshObjects() to VirtualObjectListView since + * RefreshObjects() cannot work on virtual lists. + * 2007-09-13 JPP - Corrected bug with custom sorter in VirtualObjectListView (thanks for mpgjunky) + * 2007-09-07 JPP - Corrected image scaling bug in DrawAlignedImage() (thanks to krita970) + * 2007-08-29 JPP - Allow item count labels on groups to be set per column (thanks to cmarlow for idea) + * 2007-08-14 JPP - Major rework of DataListView based on Ian Griffiths's great work + * 2007-08-11 JPP - When empty, the control can now draw a "List Empty" m + * - Added GetColumn() and GetItem() methods + * v1.5 + * 2007-08-03 JPP - Support animated GIFs in ImageRenderer + * - Allow height of rows to be specified - EXPERIMENTAL! + * 2007-07-26 JPP - Optimised redrawing of owner-drawn lists by remembering the update rect + * - Allow sort indicators to be turned off + * 2007-06-30 JPP - Added RowFormatter delegate + * - Allow a different label when there is only one item in a group (thanks to cmarlow) + * v1.4 + * 2007-04-12 JPP - Allow owner drawn on steriods! + * - Column headers now display sort indicators + * - ImageGetter delegates can now return ints, strings or Images + * (Images are only visible if the list is owner drawn) + * - Added OLVColumn.MakeGroupies to help with group partitioning + * - All normal listview views are now supported + * - Allow dotted aspect names, e.g. Owner.Workgroup.Name (thanks to OlafD) + * - Added SelectedObject and SelectedObjects properties + * v1.3 + * 2007-03-01 JPP - Added DataListView + * - Added VirtualObjectListView + * - Added Freeze/Unfreeze capabilities + * - Allowed sort handler to be installed + * - Simplified sort comparisons: handles 95% of cases with only 6 lines of code! + * - Fixed bug with alternative line colors on unsorted lists (thanks to cmarlow) + * 2007-01-13 JPP - Fixed bug with lastSortOrder (thanks to Kwan Fu Sit) + * - Non-OLVColumns are no longer allowed + * 2007-01-04 JPP - Clear sorter before rebuilding list. 10x faster! (thanks to aaberg) + * - Include GetField in GetAspectByName() so field values can be Invoked too. + * - Fixed subtle bug in RefreshItem() that erased background colors. + * 2006-11-01 JPP - Added alternate line colouring + * 2006-10-20 JPP - Refactored all sorting comparisons and made it extendable. See ComparerManager. + * - Improved IDE integration + * - Made control DoubleBuffered + * - Added object selection methods + * 2006-10-13 JPP Implemented grouping and column sorting + * 2006-10-09 JPP Initial version + * + * TO DO: + * - Support undocumented group features: subseted groups, group footer items + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using System.Runtime.Serialization.Formatters; +using System.Threading; + +namespace BrightIdeasSoftware +{ + /// + /// An ObjectListView is a much easier to use, and much more powerful, version of the ListView. + /// + /// + /// + /// An ObjectListView automatically populates a ListView control with information taken + /// from a given collection of objects. It can do this because each column is configured + /// to know which bit of the model object (the "aspect") it should be displaying. Columns similarly + /// understand how to sort the list based on their aspect, and how to construct groups + /// using their aspect. + /// + /// + /// Aspects are extracted by giving the name of a method to be called or a + /// property to be fetched. These names can be simple names or they can be dotted + /// to chain property access e.g. "Owner.Address.Postcode". + /// Aspects can also be extracted by installing a delegate. + /// + /// + /// An ObjectListView can show a "this list is empty" message when there is nothing to show in the list, + /// so that the user knows the control is supposed to be empty. + /// + /// + /// Right clicking on a column header should present a menu which can contain: + /// commands (sort, group, ungroup); filtering; and column selection. Whether these + /// parts of the menu appear is controlled by ShowCommandMenuOnRightClick, + /// ShowFilterMenuOnRightClick and SelectColumnsOnRightClick respectively. + /// + /// + /// The groups created by an ObjectListView can be configured to include other formatting + /// information, including a group icon, subtitle and task button. Using some undocumented + /// interfaces, these groups can even on virtual lists. + /// + /// + /// ObjectListView supports dragging rows to other places, including other application. + /// Special support is provide for drops from other ObjectListViews in the same application. + /// In many cases, an ObjectListView becomes a full drag source by setting to + /// true. Similarly, to accept drops, it is usually enough to set to true, + /// and then handle the and events (or the and + /// events, if you only want to handle drops from other ObjectListViews in your application). + /// + /// + /// For these classes to build correctly, the project must have references to these assemblies: + /// + /// + /// System + /// System.Data + /// System.Design + /// System.Drawing + /// System.Windows.Forms (obviously) + /// + /// + [Designer(typeof(BrightIdeasSoftware.Design.ObjectListViewDesigner))] + public partial class ObjectListView : ListView, ISupportInitialize { + + #region Life and death + + /// + /// Create an ObjectListView + /// + public ObjectListView() { + this.ColumnClick += new ColumnClickEventHandler(this.HandleColumnClick); + this.Layout += new LayoutEventHandler(this.HandleLayout); + this.ColumnWidthChanging += new ColumnWidthChangingEventHandler(this.HandleColumnWidthChanging); + this.ColumnWidthChanged += new ColumnWidthChangedEventHandler(this.HandleColumnWidthChanged); + + base.View = View.Details; +// ReSharper disable DoNotCallOverridableMethodsInConstructor + this.DoubleBuffered = true; // kill nasty flickers. hiss... me hates 'em + this.ShowSortIndicators = true; + + // Setup the overlays that will be controlled by the IDE settings + this.InitializeStandardOverlays(); + this.InitializeEmptyListMsgOverlay(); +// ReSharper restore DoNotCallOverridableMethodsInConstructor + } + + /// + /// Dispose of any resources this instance has been using + /// + /// + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + + if (!disposing) + return; + + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.Unbind(); + glassPanel.Dispose(); + } + this.glassPanels.Clear(); + + this.UnsubscribeNotifications(null); + } + + #endregion + + // TODO + //public CheckBoxSettings CheckBoxSettings { + // get { return checkBoxSettings; } + // private set { checkBoxSettings = value; } + //} + + #region Static properties + + /// + /// Gets whether the program running on Vista or later? + /// + static public bool IsVistaOrLater { + get { + if (!ObjectListView.sIsVistaOrLater.HasValue) + ObjectListView.sIsVistaOrLater = Environment.OSVersion.Version.Major >= 6; + return ObjectListView.sIsVistaOrLater.Value; + } + } + static private bool? sIsVistaOrLater; + + /// + /// Gets whether the program running on Win7 or later? + /// + static public bool IsWin7OrLater { + get { + if (!ObjectListView.sIsWin7OrLater.HasValue) { + // For some reason, Win7 is v6.1, not v7.0 + Version version = Environment.OSVersion.Version; + ObjectListView.sIsWin7OrLater = version.Major > 6 || (version.Major == 6 && version.Minor > 0); + } + return ObjectListView.sIsWin7OrLater.Value; + } + } + static private bool? sIsWin7OrLater; + + /// + /// Gets or sets how what smoothing mode will be applied to graphic operations. + /// + static public System.Drawing.Drawing2D.SmoothingMode SmoothingMode { + get { return ObjectListView.sSmoothingMode; } + set { ObjectListView.sSmoothingMode = value; } + } + static private System.Drawing.Drawing2D.SmoothingMode sSmoothingMode = + System.Drawing.Drawing2D.SmoothingMode.HighQuality; + + /// + /// Gets or sets how should text be renderered. + /// + static public System.Drawing.Text.TextRenderingHint TextRenderingHint { + get { return ObjectListView.sTextRendereringHint; } + set { ObjectListView.sTextRendereringHint = value; } + } + static private System.Drawing.Text.TextRenderingHint sTextRendereringHint = + System.Drawing.Text.TextRenderingHint.SystemDefault; + + /// + /// Gets or sets the string that will be used to title groups when the group key is null. + /// Exposed so it can be localized. + /// + static public string GroupTitleDefault { + get { return ObjectListView.sGroupTitleDefault; } + set { ObjectListView.sGroupTitleDefault = value ?? "{null}"; } + }static private string sGroupTitleDefault = "{null}"; + + /// + /// Convert the given enumerable into an ArrayList as efficiently as possible + /// + /// The source collection + /// If true, this method will always create a new + /// collection. + /// An ArrayList with the same contents as the given collection. + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + /// + public static ArrayList EnumerableToArray(IEnumerable collection, bool alwaysCreate) { + if (collection == null) + return new ArrayList(); + + if (!alwaysCreate) { + ArrayList array = collection as ArrayList; + if (array != null) + return array; + + IList iList = collection as IList; + if (iList != null) + return ArrayList.Adapter(iList); + } + + ICollection iCollection = collection as ICollection; + if (iCollection != null) + return new ArrayList(iCollection); + + ArrayList newObjects = new ArrayList(); + foreach (object x in collection) + newObjects.Add(x); + return newObjects; + } + + + /// + /// Return the count of items in the given enumerable + /// + /// + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + public static int EnumerableCount(IEnumerable collection) { + if (collection == null) + return 0; + + ICollection iCollection = collection as ICollection; + if (iCollection != null) + return iCollection.Count; + + int i = 0; +// ReSharper disable once UnusedVariable + foreach (object x in collection) + i++; + return i; + } + + /// + /// Return whether or not the given enumerable is empty. A string is regarded as + /// an empty collection. + /// + /// + /// True if the given collection is null or empty + /// + /// When we move to .NET 3.5, we can use LINQ and not need this method. + /// + public static bool IsEnumerableEmpty(IEnumerable collection) { + return collection == null || (collection is string) || !collection.GetEnumerator().MoveNext(); + } + + /// + /// Gets or sets whether all ObjectListViews will silently ignore missing aspect errors. + /// + /// + /// + /// By default, if an ObjectListView is asked to display an aspect + /// (i.e. a field/property/method) + /// that does not exist from a model, it displays an error message in that cell, since that + /// condition is normally a programming error. There are some use cases where + /// this is not an error -- in those cases, set this to true and ObjectListView will + /// simply display an empty cell. + /// + /// Be warned: if you set this to true, it can be very difficult to track down + /// typing mistakes or name changes in AspectNames. + /// + public static bool IgnoreMissingAspects { + get { return Munger.IgnoreMissingAspects; } + set { Munger.IgnoreMissingAspects = value; } + } + + /// + /// Gets or sets whether the control will draw a rectangle in each cell showing the cell padding. + /// + /// + /// + /// This can help with debugging display problems from cell padding. + /// + /// As with all cell padding, this setting only takes effect when the control is owner drawn. + /// + public static bool ShowCellPaddingBounds { + get { return sShowCellPaddingBounds; } + set { sShowCellPaddingBounds = value; } + } + private static bool sShowCellPaddingBounds; + + /// + /// Gets the style that will be used by default to format disabled rows + /// + public static SimpleItemStyle DefaultDisabledItemStyle + { + get + { + if (sDefaultDisabledItemStyle == null) + { + sDefaultDisabledItemStyle = new SimpleItemStyle(); + sDefaultDisabledItemStyle.ForeColor = Color.DarkGray; + } + return sDefaultDisabledItemStyle; + } + } + private static SimpleItemStyle sDefaultDisabledItemStyle; + + #endregion + + #region Public properties + + /// + /// Gets or sets an model filter that is combined with any column filtering that the end-user specifies. + /// + /// This is different from the ModelFilter property, since setting that will replace + /// any column filtering, whereas setting this will combine this filter with the column filtering + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IModelFilter AdditionalFilter + { + get { return this.additionalFilter; } + set + { + if (this.additionalFilter == value) + return; + this.additionalFilter = value; + this.UpdateColumnFiltering(); + } + } + private IModelFilter additionalFilter; + + /// + /// Get or set all the columns that this control knows about. + /// Only those columns where IsVisible is true will be seen by the user. + /// + /// + /// + /// If you want to add new columns programmatically, add them to + /// AllColumns and then call RebuildColumns(). Normally, you do not have to + /// deal with this property directly. Just use the IDE. + /// + /// If you do add or remove columns from the AllColumns collection, + /// you have to call RebuildColumns() to make those changes take effect. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public virtual List AllColumns { + get { return this.allColumns; } + set { this.allColumns = value ?? new List(); } + } + private List allColumns = new List(); + + /// + /// Gets or sets the background color of every second row + /// + [Category("ObjectListView"), + Description("If using alternate colors, what color should the background of alterate rows be?"), + DefaultValue(typeof(Color), "")] + public Color AlternateRowBackColor { + get { return alternateRowBackColor; } + set { alternateRowBackColor = value; } + } + private Color alternateRowBackColor = Color.Empty; + + /// + /// Gets the alternate row background color that has been set, or the default color + /// + [Browsable(false)] + public virtual Color AlternateRowBackColorOrDefault { + get { + return this.alternateRowBackColor == Color.Empty ? Color.LemonChiffon : this.alternateRowBackColor; + } + } + + /// + /// This property forces the ObjectListView to always group items by the given column. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn AlwaysGroupByColumn { + get { return alwaysGroupByColumn; } + set { alwaysGroupByColumn = value; } + } + private OLVColumn alwaysGroupByColumn; + + /// + /// If AlwaysGroupByColumn is not null, this property will be used to decide how + /// those groups are sorted. If this property has the value SortOrder.None, then + /// the sort order will toggle according to the users last header click. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder AlwaysGroupBySortOrder { + get { return alwaysGroupBySortOrder; } + set { alwaysGroupBySortOrder = value; } + } + private SortOrder alwaysGroupBySortOrder = SortOrder.None; + + /// + /// Give access to the image list that is actually being used by the control + /// + /// + /// Normally, it is preferable to use SmallImageList. Only use this property + /// if you know exactly what you are doing. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual ImageList BaseSmallImageList { + get { return base.SmallImageList; } + set { base.SmallImageList = value; } + } + + /// + /// How does the user indicate that they want to edit a cell? + /// None means that the listview cannot be edited. + /// + /// Columns can also be marked as editable. + [Category("ObjectListView"), + Description("How does the user indicate that they want to edit a cell?"), + DefaultValue(CellEditActivateMode.None)] + public virtual CellEditActivateMode CellEditActivation { + get { return cellEditActivation; } + set { + cellEditActivation = value; + if (this.Created) + this.Invalidate(); + } + } + private CellEditActivateMode cellEditActivation = CellEditActivateMode.None; + + /// + /// Gets or sets the engine that will handle key presses during a cell edit operation. + /// Settings this to null will reset it to default value. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public CellEditKeyEngine CellEditKeyEngine { + get { return this.cellEditKeyEngine ?? (this.cellEditKeyEngine = new CellEditKeyEngine()); } + set { this.cellEditKeyEngine = value; } + } + private CellEditKeyEngine cellEditKeyEngine; + + /// + /// Gets the control that is currently being used for editing a cell. + /// + /// This will obviously be null if no cell is being edited. + [Browsable(false)] + public Control CellEditor { + get { + return this.cellEditor; + } + } + + /// + /// Gets or sets the behaviour of the Tab key when editing a cell on the left or right + /// edge of the control. If this is false (the default), pressing Tab will wrap to the other side + /// of the same row. If this is true, pressing Tab when editing the right most cell will advance + /// to the next row + /// and Shift-Tab when editing the left-most cell will change to the previous row. + /// + [Category("ObjectListView"), + Description("Should Tab/Shift-Tab change rows while cell editing?"), + DefaultValue(false)] + public virtual bool CellEditTabChangesRows { + get { return cellEditTabChangesRows; } + set { + cellEditTabChangesRows = value; + if (cellEditTabChangesRows) { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.ChangeRow); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab|Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.ChangeRow); + } else { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab, CellEditCharacterBehaviour.ChangeColumnRight, CellEditAtEdgeBehaviour.Wrap); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Tab | Keys.Shift, CellEditCharacterBehaviour.ChangeColumnLeft, CellEditAtEdgeBehaviour.Wrap); + } + } + } + private bool cellEditTabChangesRows; + + /// + /// Gets or sets the behaviour of the Enter keys while editing a cell. + /// If this is false (the default), pressing Enter will simply finish the editing operation. + /// If this is true, Enter will finish the edit operation and start a new edit operation + /// on the cell below the current cell, wrapping to the top of the next row when at the bottom cell. + /// + [Category("ObjectListView"), + Description("Should Enter change rows while cell editing?"), + DefaultValue(false)] + public virtual bool CellEditEnterChangesRows { + get { return cellEditEnterChangesRows; } + set { + cellEditEnterChangesRows = value; + if (cellEditEnterChangesRows) { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.ChangeRowDown, CellEditAtEdgeBehaviour.ChangeColumn); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.ChangeRowUp, CellEditAtEdgeBehaviour.ChangeColumn); + } else { + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); + this.CellEditKeyEngine.SetKeyBehaviour(Keys.Enter | Keys.Shift, CellEditCharacterBehaviour.EndEdit, CellEditAtEdgeBehaviour.EndEdit); + } + } + } + private bool cellEditEnterChangesRows; + + /// + /// Gets the tool tip control that shows tips for the cells + /// + [Browsable(false)] + public ToolTipControl CellToolTip { + get { + if (this.cellToolTip == null) { + this.CreateCellToolTip(); + } + return this.cellToolTip; + } + } + private ToolTipControl cellToolTip; + + /// + /// Gets or sets how many pixels will be left blank around each cell of this item. + /// Cell contents are aligned after padding has been taken into account. + /// + /// + /// Each value of the given rectangle will be treated as an inset from + /// the corresponding side. The width of the rectangle is the padding for the + /// right cell edge. The height of the rectangle is the padding for the bottom + /// cell edge. + /// + /// + /// So, this.olv1.CellPadding = new Rectangle(1, 2, 3, 4); will leave one pixel + /// of space to the left of the cell, 2 pixels at the top, 3 pixels of space + /// on the right edge, and 4 pixels of space at the bottom of each cell. + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// This setting only affects the contents of the cell. The background is + /// not affected. + /// If you set this to a foolish value, your control will appear to be empty. + /// + [Category("ObjectListView"), + Description("How much padding will be applied to each cell in this control?"), + DefaultValue(null)] + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how cells will be vertically aligned by default. + /// + /// This setting only takes effect when the control is owner drawn. It will only be noticable + /// when RowHeight has been set such that there is some vertical space in each row. + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(StringAlignment.Center)] + public virtual StringAlignment CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment cellVerticalAlignment = StringAlignment.Center; + + /// + /// Should this list show checkboxes? + /// + public new bool CheckBoxes { + get { + return base.CheckBoxes; + } + set { + base.CheckBoxes = value; + + // Initialize the state image list so we can display indetermined values. + this.InitializeStateImageList(); + } + } + + /// + /// Return the model object of the row that is checked or null if no row is checked + /// or more than one row is checked + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual Object CheckedObject { + get { + IList checkedObjects = this.CheckedObjects; + return checkedObjects.Count == 1 ? checkedObjects[0] : null; + } + set { + this.CheckedObjects = new ArrayList(new Object[] { value }); + } + } + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivilent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// .NET's CheckedItems property is not helpful. It is just a short-hand for + /// iterating through the list looking for items that are checked. + /// + /// + /// The performance of the get method is O(n), where n is the number of items + /// in the control. The performance of the set method is + /// O(n + m) where m is the number of objects being checked. Be careful on long lists. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IList CheckedObjects { + get { + ArrayList list = new ArrayList(); + if (this.CheckBoxes) { + for (int i = 0; i < this.GetItemCount(); i++) { + OLVListItem olvi = this.GetItem(i); + if (olvi.CheckState == CheckState.Checked) + list.Add(olvi.RowObject); + } + } + return list; + } + set { + if (!this.CheckBoxes) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + // Set up an efficient way of testing for the presence of a particular model + Hashtable table = new Hashtable(this.GetItemCount()); + if (value != null) { + foreach (object x in value) + table[x] = true; + } + + this.BeginUpdate(); + foreach (Object x in this.Objects) { + this.SetObjectCheckedness(x, table.ContainsKey(x) ? CheckState.Checked : CheckState.Unchecked); + } + this.EndUpdate(); + + Debug.WriteLine(String.Format("PERF - Setting CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + + } + } + + /// + /// Gets or sets the checked objects from an enumerable. + /// + /// + /// Useful for checking all objects in the list. + /// + /// + /// this.olv1.CheckedObjectsEnumerable = this.olv1.Objects; + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable CheckedObjectsEnumerable { + get { + return this.CheckedObjects; + } + set { + this.CheckedObjects = ObjectListView.EnumerableToArray(value, true); + } + } + + /// + /// Gets Columns for this list. We hide the original so we can associate + /// a specialised editor with it. + /// + [Editor("BrightIdeasSoftware.Design.OLVColumnCollectionEditor", "System.Drawing.Design.UITypeEditor")] + new public ListView.ColumnHeaderCollection Columns { + get { + return base.Columns; + } + } + + /// + /// Get/set the list of columns that should be used when the list switches to tile view. + /// + [Browsable(false), + Obsolete("Use GetFilteredColumns() and OLVColumn.IsTileViewColumn instead"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public List ColumnsForTileView { + get { return this.GetFilteredColumns(View.Tile); } + } + + /// + /// Return the visible columns in the order they are displayed to the user + /// + [Browsable(false)] + public virtual List ColumnsInDisplayOrder { + get { + OLVColumn[] columnsInDisplayOrder = new OLVColumn[this.Columns.Count]; + foreach (OLVColumn col in this.Columns) { + columnsInDisplayOrder[col.DisplayIndex] = col; + } + return new List(columnsInDisplayOrder); + } + } + + + /// + /// Get the area of the control that shows the list, minus any header control + /// + [Browsable(false)] + public Rectangle ContentRectangle { + get { + Rectangle r = this.ClientRectangle; + + // If the listview has a header control, remove the header from the control area + if ((this.View == View.Details || this.ShowHeaderInAllViews) && this.HeaderControl != null) { + Rectangle hdrBounds = new Rectangle(); + NativeMethods.GetClientRect(this.HeaderControl.Handle, ref hdrBounds); + r.Y = hdrBounds.Height; + r.Height = r.Height - hdrBounds.Height; + } + + return r; + } + } + + /// + /// Gets or sets if the selected rows should be copied to the clipboard when the user presses Ctrl-C + /// + [Category("ObjectListView"), + Description("Should the control copy the selection to the clipboard when the user presses Ctrl-C?"), + DefaultValue(true)] + public virtual bool CopySelectionOnControlC { + get { return copySelectionOnControlC; } + set { copySelectionOnControlC = value; } + } + private bool copySelectionOnControlC = true; + + + /// + /// Gets or sets whether the Control-C copy to clipboard functionality should use + /// the installed DragSource to create the data object that is placed onto the clipboard. + /// + /// This is normally what is desired, unless a custom DragSource is installed + /// that does some very specialized drag-drop behaviour. + [Category("ObjectListView"), + Description("Should the Ctrl-C copy process use the DragSource to create the Clipboard data object?"), + DefaultValue(true)] + public bool CopySelectionOnControlCUsesDragSource { + get { return this.copySelectionOnControlCUsesDragSource; } + set { this.copySelectionOnControlCUsesDragSource = value; } + } + private bool copySelectionOnControlCUsesDragSource = true; + + /// + /// Gets the list of decorations that will be drawn the ListView + /// + /// + /// + /// Do not modify the contents of this list directly. Use the AddDecoration() and RemoveDecoration() methods. + /// + /// + /// A decoration scrolls with the list contents. An overlay is fixed in place. + /// + /// + [Browsable(false)] + protected IList Decorations { + get { return this.decorations; } + } + private readonly List decorations = new List(); + + /// + /// When owner drawing, this renderer will draw columns that do not have specific renderer + /// given to them + /// + /// If you try to set this to null, it will revert to a BaseRenderer + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IRenderer DefaultRenderer { + get { return this.defaultRenderer; } + set { this.defaultRenderer = value ?? new BaseRenderer(); } + } + private IRenderer defaultRenderer = new BaseRenderer(); + + /// + /// Gets or sets the style that will be applied to disabled items. + /// + /// If this is not set explicitly, will be used. + [Category("ObjectListView"), + Description("The style that will be applied to disabled items"), + DefaultValue(null)] + public SimpleItemStyle DisabledItemStyle + { + get { return disabledItemStyle; } + set { disabledItemStyle = value; } + } + private SimpleItemStyle disabledItemStyle; + + /// + /// Gets or sets the list of model objects that are disabled. + /// Disabled objects cannot be selected or activated. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable DisabledObjects + { + get + { + return disabledObjects.Keys; + } + set + { + this.disabledObjects.Clear(); + DisableObjects(value); + } + } + private readonly Hashtable disabledObjects = new Hashtable(); + + /// + /// Is this given model object disabled? + /// + /// + /// + public bool IsDisabled(object model) + { + return model != null && this.disabledObjects.ContainsKey(model); + } + + /// + /// Disable the given model object. + /// Disabled objects cannot be selected or activated. + /// + /// Must not be null + public void DisableObject(object model) { + ArrayList list = new ArrayList(); + list.Add(model); + this.DisableObjects(list); + } + + /// + /// Disable all the given model objects + /// + /// + public void DisableObjects(IEnumerable models) + { + if (models == null) + return; + ArrayList list = ObjectListView.EnumerableToArray(models, false); + foreach (object model in list) + { + if (model == null) + continue; + + this.disabledObjects[model] = true; + int modelIndex = this.IndexOf(model); + if (modelIndex >= 0) + NativeMethods.DeselectOneItem(this, modelIndex); + } + this.RefreshObjects(list); + } + + /// + /// Enable the given model object, so it can be selected and activated again. + /// + /// Must not be null + public void EnableObject(object model) + { + this.disabledObjects.Remove(model); + this.RefreshObject(model); + } + + /// + /// Enable all the given model objects + /// + /// + public void EnableObjects(IEnumerable models) + { + if (models == null) + return; + ArrayList list = ObjectListView.EnumerableToArray(models, false); + foreach (object model in list) + { + if (model != null) + this.disabledObjects.Remove(model); + } + this.RefreshObjects(list); + } + + /// + /// Forget all disabled objects. This does not trigger a redraw or rebuild + /// + protected void ClearDisabledObjects() + { + this.disabledObjects.Clear(); + } + + /// + /// Gets or sets the object that controls how drags start from this control + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDragSource DragSource { + get { return this.dragSource; } + set { this.dragSource = value; } + } + private IDragSource dragSource; + + /// + /// Gets or sets the object that controls how drops are accepted and processed + /// by this ListView. + /// + /// + /// + /// If the given sink is an instance of SimpleDropSink, then events from the drop sink + /// will be automatically forwarded to the ObjectListView (which means that handlers + /// for those event can be configured within the IDE). + /// + /// If this is set to null, the control will not accept drops. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDropSink DropSink { + get { return this.dropSink; } + set { + if (this.dropSink == value) + return; + + // Stop listening for events on the old sink + SimpleDropSink oldSink = this.dropSink as SimpleDropSink; + if (oldSink != null) { + oldSink.CanDrop -= new EventHandler(this.DropSinkCanDrop); + oldSink.Dropped -= new EventHandler(this.DropSinkDropped); + oldSink.ModelCanDrop -= new EventHandler(this.DropSinkModelCanDrop); + oldSink.ModelDropped -= new EventHandler(this.DropSinkModelDropped); + } + + this.dropSink = value; + this.AllowDrop = (value != null); + if (this.dropSink != null) + this.dropSink.ListView = this; + + // Start listening for events on the new sink + SimpleDropSink newSink = value as SimpleDropSink; + if (newSink != null) { + newSink.CanDrop += new EventHandler(this.DropSinkCanDrop); + newSink.Dropped += new EventHandler(this.DropSinkDropped); + newSink.ModelCanDrop += new EventHandler(this.DropSinkModelCanDrop); + newSink.ModelDropped += new EventHandler(this.DropSinkModelDropped); + } + } + } + private IDropSink dropSink; + + // Forward events from the drop sink to the control itself + void DropSinkCanDrop(object sender, OlvDropEventArgs e) { this.OnCanDrop(e); } + void DropSinkDropped(object sender, OlvDropEventArgs e) { this.OnDropped(e); } + void DropSinkModelCanDrop(object sender, ModelDropEventArgs e) { this.OnModelCanDrop(e); } + void DropSinkModelDropped(object sender, ModelDropEventArgs e) { this.OnModelDropped(e); } + + /// + /// This registry decides what control should be used to edit what cells, based + /// on the type of the value in the cell. + /// + /// + /// All instances of ObjectListView share the same editor registry. +// ReSharper disable FieldCanBeMadeReadOnly.Global + static public EditorRegistry EditorRegistry = new EditorRegistry(); +// ReSharper restore FieldCanBeMadeReadOnly.Global + + /// + /// Gets or sets the text that should be shown when there are no items in this list view. + /// + /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, + /// this property does nothing + [Category("ObjectListView"), + Description("When the list has no items, show this message in the control"), + DefaultValue(null), + Localizable(true)] + public virtual String EmptyListMsg { + get { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + return overlay == null ? null : overlay.Text; + } + set { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + if (overlay != null) { + overlay.Text = value; + this.Invalidate(); + } + } + } + + /// + /// Gets or sets the font in which the List Empty message should be drawn + /// + /// If the EmptyListMsgOverlay has been changed to something other than a TextOverlay, + /// this property does nothing + [Category("ObjectListView"), + Description("What font should the 'list empty' message be drawn in?"), + DefaultValue(null)] + public virtual Font EmptyListMsgFont { + get { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + return overlay == null ? null : overlay.Font; + } + set { + TextOverlay overlay = this.EmptyListMsgOverlay as TextOverlay; + if (overlay != null) + overlay.Font = value; + } + } + + /// + /// Return the font for the 'list empty' message or a reasonable default + /// + [Browsable(false)] + public virtual Font EmptyListMsgFontOrDefault { + get { + return this.EmptyListMsgFont ?? new Font("Tahoma", 14); + } + } + + /// + /// Gets or sets the overlay responsible for drawing the List Empty msg. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IOverlay EmptyListMsgOverlay { + get { return this.emptyListMsgOverlay; } + set { + if (this.emptyListMsgOverlay != value) { + this.emptyListMsgOverlay = value; + this.Invalidate(); + } + } + } + private IOverlay emptyListMsgOverlay; + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + /// + /// + /// This collection is the result of filtering the current list of objects. + /// It is not a snapshot of the filtered list that was last used to build the control. + /// + /// + /// Normal warnings apply when using this with virtual lists. It will work, but it + /// may take a while. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + virtual public IEnumerable FilteredObjects { + get { + if (this.IsFiltering) + return this.FilterObjects(this.Objects, this.ModelFilter, this.ListFilter); + + return this.Objects; + } + } + + /// + /// Gets or sets the strategy object that will be used to build the Filter menu + /// + /// If this is null, no filter menu will be built. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public FilterMenuBuilder FilterMenuBuildStrategy { + get { return filterMenuBuilder; } + set { filterMenuBuilder = value; } + } + private FilterMenuBuilder filterMenuBuilder = new FilterMenuBuilder(); + + /// + /// Hide the Groups collection so it's not visible in the Properties grid. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + new public ListViewGroupCollection Groups { + get { return base.Groups; } + } + + /// + /// Gets or sets the image list from which group header will take their images + /// + /// If this is not set, then group headers will not show any images. + [Category("ObjectListView"), + Description("The image list from which group header will take their images"), + DefaultValue(null)] + public ImageList GroupImageList { + get { return this.groupImageList; } + set { + this.groupImageList = value; + if (this.Created) { + NativeMethods.SetGroupImageList(this, value); + } + } + } + private ImageList groupImageList; + + /// + /// Gets how the group label should be formatted when a group is empty or + /// contains more than one item + /// + /// + /// The given format string must have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group + /// + /// + /// "{0} [{1} items]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public virtual string GroupWithItemCountFormat { + get { return groupWithItemCountFormat; } + set { groupWithItemCountFormat = value; } + } + private string groupWithItemCountFormat; + + /// + /// Return this.GroupWithItemCountFormat or a reasonable default + /// + [Browsable(false)] + public virtual string GroupWithItemCountFormatOrDefault { + get { + return String.IsNullOrEmpty(this.GroupWithItemCountFormat) ? "{0} [{1} items]" : this.GroupWithItemCountFormat; + } + } + + /// + /// Gets how the group label should be formatted when a group contains only a single item + /// + /// + /// The given format string must have two placeholders: + /// + /// {0} - the original group title + /// {1} - the number of items in the group (always 1) + /// + /// + /// "{0} [{1} item]" + [Category("ObjectListView"), + Description("The format to use when suffixing item counts to group titles"), + DefaultValue(null), + Localizable(true)] + public virtual string GroupWithItemCountSingularFormat { + get { return groupWithItemCountSingularFormat; } + set { groupWithItemCountSingularFormat = value; } + } + private string groupWithItemCountSingularFormat; + + /// + /// Gets GroupWithItemCountSingularFormat or a reasonable default + /// + [Browsable(false)] + public virtual string GroupWithItemCountSingularFormatOrDefault { + get { + return String.IsNullOrEmpty(this.GroupWithItemCountSingularFormat) ? "{0} [{1} item]" : this.GroupWithItemCountSingularFormat; + } + } + + /// + /// Gets or sets whether or not the groups in this ObjectListView should be collapsible. + /// + /// + /// This feature only works under Vista and later. + /// + [Browsable(true), + Category("ObjectListView"), + Description("Should the groups in this control be collapsible (Vista and later only)."), + DefaultValue(true)] + public bool HasCollapsibleGroups { + get { return hasCollapsibleGroups; } + set { hasCollapsibleGroups = value; } + } + private bool hasCollapsibleGroups = true; + + /// + /// Does this listview have a message that should be drawn when the list is empty? + /// + [Browsable(false)] + public virtual bool HasEmptyListMsg { + get { return !String.IsNullOrEmpty(this.EmptyListMsg); } + } + + /// + /// Get whether there are any overlays to be drawn + /// + [Browsable(false)] + public bool HasOverlays { + get { + return (this.Overlays.Count > 2 || + this.imageOverlay.Image != null || + !String.IsNullOrEmpty(this.textOverlay.Text)); + } + } + + /// + /// Gets the header control for the ListView + /// + [Browsable(false)] + public HeaderControl HeaderControl { + get { return this.headerControl ?? (this.headerControl = new HeaderControl(this)); } + } + private HeaderControl headerControl; + + /// + /// Gets or sets the font in which the text of the column headers will be drawn + /// + /// Individual columns can override this through their HeaderFormatStyle property. + [DefaultValue(null)] + [Browsable(false)] + [Obsolete("Use a HeaderFormatStyle instead", false)] + public Font HeaderFont { + get { return this.HeaderFormatStyle == null ? null : this.HeaderFormatStyle.Normal.Font; } + set { + if (value == null && this.HeaderFormatStyle == null) + return; + + if (this.HeaderFormatStyle == null) + this.HeaderFormatStyle = new HeaderFormatStyle(); + + this.HeaderFormatStyle.SetFont(value); + } + } + + /// + /// Gets or sets the style that will be used to draw the columm headers of the listview + /// + /// + /// + /// This is only used when HeaderUsesThemes is false. + /// + /// + /// Individual columns can override this through their HeaderFormatStyle property. + /// + /// + [Category("ObjectListView"), + Description("What style will be used to draw the control's header"), + DefaultValue(null)] + public HeaderFormatStyle HeaderFormatStyle { + get { return this.headerFormatStyle; } + set { this.headerFormatStyle = value; } + } + private HeaderFormatStyle headerFormatStyle; + + /// + /// Gets or sets the maximum height of the header. -1 means no maximum. + /// + [Category("ObjectListView"), + Description("What is the maximum height of the header? -1 means no maximum"), + DefaultValue(-1)] + public int HeaderMaximumHeight { + get { return headerMaximumHeight; } + set { headerMaximumHeight = value; } + } + private int headerMaximumHeight = -1; + + /// + /// Gets or sets whether the header will be drawn strictly according to the OS's theme. + /// + /// + /// + /// If this is set to true, the header will be rendered completely by the system, without + /// any of ObjectListViews fancy processing -- no images in header, no filter indicators, + /// no word wrapping, no header styling, no checkboxes. + /// + /// If this is set to false, ObjectListView will render the header as it thinks best. + /// If no special features are required, then ObjectListView will delegate rendering to the OS. + /// Otherwise, ObjectListView will draw the header according to the configuration settings. + /// + /// + /// The effect of not being themed will be different from OS to OS. At + /// very least, the sort indicator will not be standard. + /// + /// + [Category("ObjectListView"), + Description("Will the column headers be drawn strictly according to OS theme?"), + DefaultValue(false)] + public bool HeaderUsesThemes { + get { return this.headerUsesThemes; } + set { this.headerUsesThemes = value; } + } + private bool headerUsesThemes = false; + + /// + /// Gets or sets the whether the text in the header will be word wrapped. + /// + /// + /// Line breaks will be applied between words. Words that are too long + /// will still be ellipsed. + /// + /// As with all settings that make the header look different, HeaderUsesThemes must be set to false, otherwise + /// the OS will be responsible for drawing the header, and it does not allow word wrapped text. + /// + /// + [Category("ObjectListView"), + Description("Will the text of the column headers be word wrapped?"), + DefaultValue(false)] + public bool HeaderWordWrap { + get { return this.headerWordWrap; } + set { + this.headerWordWrap = value; + if (this.headerControl != null) + this.headerControl.WordWrap = value; + } + } + private bool headerWordWrap; + + /// + /// Gets the tool tip that shows tips for the column headers + /// + [Browsable(false)] + public ToolTipControl HeaderToolTip { + get { + return this.HeaderControl.ToolTip; + } + } + + /// + /// Gets the index of the row that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int HotRowIndex { + get { return this.hotRowIndex; } + protected set { this.hotRowIndex = value; } + } + private int hotRowIndex; + + /// + /// Gets the index of the subitem that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int HotColumnIndex { + get { return this.hotColumnIndex; } + protected set { this.hotColumnIndex = value; } + } + private int hotColumnIndex; + + /// + /// Gets the part of the item/subitem that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HitTestLocation HotCellHitLocation { + get { return this.hotCellHitLocation; } + protected set { this.hotCellHitLocation = value; } + } + private HitTestLocation hotCellHitLocation; + + /// + /// Gets an extended indication of the part of item/subitem/group that the mouse is currently over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HitTestLocationEx HotCellHitLocationEx + { + get { return this.hotCellHitLocationEx; } + protected set { this.hotCellHitLocationEx = value; } + } + private HitTestLocationEx hotCellHitLocationEx; + + /// + /// Gets the group that the mouse is over + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVGroup HotGroup + { + get { return hotGroup; } + internal set { hotGroup = value; } + } + private OLVGroup hotGroup; + + /// + /// The index of the item that is 'hot', i.e. under the cursor. -1 means no item. + /// + [Browsable(false), + Obsolete("Use HotRowIndex instead", false)] + public virtual int HotItemIndex { + get { return this.HotRowIndex; } + } + + /// + /// What sort of formatting should be applied to the row under the cursor? + /// + /// This only takes effect when UseHotItem is true. + [Category("ObjectListView"), + Description("How should the row under the cursor be highlighted"), + DefaultValue(null)] + public virtual HotItemStyle HotItemStyle { + get { return this.hotItemStyle; } + set { + if (this.HotItemStyle != null) + this.RemoveOverlay(this.HotItemStyle.Overlay); + this.hotItemStyle = value; + if (this.HotItemStyle != null) + this.AddOverlay(this.HotItemStyle.Overlay); + } + } + private HotItemStyle hotItemStyle; + + /// + /// What sort of formatting should be applied to hyperlinks? + /// + [Category("ObjectListView"), + Description("How should hyperlinks be drawn"), + DefaultValue(null)] + public virtual HyperlinkStyle HyperlinkStyle { + get { return this.hyperlinkStyle; } + set { this.hyperlinkStyle = value; } + } + private HyperlinkStyle hyperlinkStyle; + + /// + /// What color should be used for the background of selected rows? + /// + /// Windows does not give the option of changing the selection background. + /// So the control has to be owner drawn to see the result of this setting. + /// Setting UseCustomSelectionColors = true will do this for you. + [Category("ObjectListView"), + Description("The background foregroundColor of selected rows when the control is owner drawn"), + DefaultValue(typeof(Color), "")] + public virtual Color HighlightBackgroundColor { + get { return highlightBackgroundColor; } + set { highlightBackgroundColor = value; } + } + private Color highlightBackgroundColor = Color.Empty; + + /// + /// Return the color should be used for the background of selected rows or a reasonable default + /// + [Browsable(false)] + public virtual Color HighlightBackgroundColorOrDefault { + get { + return this.HighlightBackgroundColor.IsEmpty ? SystemColors.Highlight : this.HighlightBackgroundColor; + } + } + + /// + /// What color should be used for the foreground of selected rows? + /// + /// Windows does not give the option of changing the selection foreground (text color). + /// So the control has to be owner drawn to see the result of this setting. + /// Setting UseCustomSelectionColors = true will do this for you. + [Category("ObjectListView"), + Description("The foreground foregroundColor of selected rows when the control is owner drawn"), + DefaultValue(typeof(Color), "")] + public virtual Color HighlightForegroundColor { + get { return highlightForegroundColor; } + set { highlightForegroundColor = value; } + } + private Color highlightForegroundColor = Color.Empty; + + /// + /// Return the color should be used for the foreground of selected rows or a reasonable default + /// + [Browsable(false)] + public virtual Color HighlightForegroundColorOrDefault { + get { + return this.HighlightForegroundColor.IsEmpty ? SystemColors.HighlightText : this.HighlightForegroundColor; + } + } + + /// + /// Gets or sets whether or not hidden columns should be included in the text representation + /// of rows that are copied or dragged to another application. If this is false (the default), + /// only visible columns will be included. + /// + [Category("ObjectListView"), + Description("When rows are copied or dragged, will data in hidden columns be included in the text? If this is false, only visible columns will be included."), + DefaultValue(false)] + public virtual bool IncludeHiddenColumnsInDataTransfer { + get { return includeHiddenColumnsInDataTransfer; } + set { includeHiddenColumnsInDataTransfer = value; } + } + private bool includeHiddenColumnsInDataTransfer; + + /// + /// Gets or sets whether or not hidden columns should be included in the text representation + /// of rows that are copied or dragged to another application. If this is false (the default), + /// only visible columns will be included. + /// + [Category("ObjectListView"), + Description("When rows are copied, will column headers be in the text?."), + DefaultValue(false)] + public virtual bool IncludeColumnHeadersInCopy + { + get { return includeColumnHeadersInCopy; } + set { includeColumnHeadersInCopy = value; } + } + private bool includeColumnHeadersInCopy; + + /// + /// Return true if a cell edit operation is currently happening + /// + [Browsable(false)] + public virtual bool IsCellEditing { + get { return this.cellEditor != null; } + } + + /// + /// Return true if the ObjectListView is being used within the development environment. + /// + [Browsable(false)] + public virtual bool IsDesignMode { + get { return this.DesignMode; } + } + + /// + /// Gets whether or not the current list is filtering its contents + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + virtual public bool IsFiltering { + get { return this.UseFiltering && (this.ModelFilter != null || this.ListFilter != null); } + } + + /// + /// When the user types into a list, should the values in the current sort column be searched to find a match? + /// If this is false, the primary column will always be used regardless of the sort column. + /// + /// When this is true, the behavior is like that of ITunes. + [Category("ObjectListView"), + Description("When the user types into a list, should the values in the current sort column be searched to find a match?"), + DefaultValue(true)] + public virtual bool IsSearchOnSortColumn { + get { return isSearchOnSortColumn; } + set { isSearchOnSortColumn = value; } + } + private bool isSearchOnSortColumn = true; + + /// + /// Gets or sets if this control will use a SimpleDropSink to receive drops + /// + /// + /// + /// Setting this replaces any previous DropSink. + /// + /// + /// After setting this to true, the SimpleDropSink will still need to be configured + /// to say when it can accept drops and what should happen when something is dropped. + /// The need to do these things makes this property mostly useless :( + /// + /// + [Category("ObjectListView"), + Description("Should this control will use a SimpleDropSink to receive drops."), + DefaultValue(false)] + public virtual bool IsSimpleDropSink { + get { return this.DropSink != null; } + set { + this.DropSink = value ? new SimpleDropSink() : null; + } + } + + /// + /// Gets or sets if this control will use a SimpleDragSource to initiate drags + /// + /// Setting this replaces any previous DragSource + [Category("ObjectListView"), + Description("Should this control use a SimpleDragSource to initiate drags out from this control"), + DefaultValue(false)] + public virtual bool IsSimpleDragSource { + get { return this.DragSource != null; } + set { + this.DragSource = value ? new SimpleDragSource() : null; + } + } + + /// + /// Hide the Items collection so it's not visible in the Properties grid. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + new public ListViewItemCollection Items { + get { return base.Items; } + } + + /// + /// This renderer draws the items when in the list is in non-details view. + /// In details view, the renderers for the individuals columns are responsible. + /// + [Category("ObjectListView"), + Description("The owner drawn renderer that draws items when the list is in non-Details view."), + DefaultValue(null)] + public IRenderer ItemRenderer { + get { return itemRenderer; } + set { itemRenderer = value; } + } + private IRenderer itemRenderer; + + /// + /// Which column did we last sort by + /// + /// This is an alias for PrimarySortColumn + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn LastSortColumn { + get { return this.PrimarySortColumn; } + set { this.PrimarySortColumn = value; } + } + + /// + /// Which direction did we last sort + /// + /// This is an alias for PrimarySortOrder + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder LastSortOrder { + get { return this.PrimarySortOrder; } + set { this.PrimarySortOrder = value; } + } + + /// + /// Gets or sets the filter that is applied to our whole list of objects. + /// + /// + /// The list is updated immediately to reflect this filter. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IListFilter ListFilter { + get { return listFilter; } + set { + listFilter = value; + if (this.UseFiltering) + this.UpdateFiltering(); + } + } + private IListFilter listFilter; + + /// + /// Gets or sets the filter that is applied to each model objects in the list + /// + /// + /// You may want to consider using instead of this property, + /// since AdditionalFilter combines with column filtering at runtime. Setting this property simply + /// replaces any column filter the user may have given. + /// + /// The list is updated immediately to reflect this filter. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IModelFilter ModelFilter { + get { return modelFilter; } + set { + modelFilter = value; + if (this.UseFiltering) + this.UpdateFiltering(); + } + } + private IModelFilter modelFilter; + + + /// + /// Gets the hit test info last time the mouse was moved. + /// + /// Useful for hot item processing. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OlvListViewHitTestInfo MouseMoveHitTest { + get { return mouseMoveHitTest; } + private set { mouseMoveHitTest = value; } + } + private OlvListViewHitTestInfo mouseMoveHitTest; + + /// + /// Gets or sets the list of groups shown by the listview. + /// + /// + /// This property does not work like the .NET Groups property. It should + /// be treated as a read-only property. + /// Changes made to the list are NOT reflected in the ListView itself -- it is pointless to add + /// or remove groups to/from this list. Such modifications will do nothing. + /// To do such things, you must listen for + /// BeforeCreatingGroups or AboutToCreateGroups events, and change the list of + /// groups in those events. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IList OLVGroups { + get { return this.olvGroups; } + set { this.olvGroups = value; } + } + private IList olvGroups; + + /// + /// Gets or sets the collection of OLVGroups that are collapsed. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IEnumerable CollapsedGroups { + get + { + if (this.OLVGroups != null) + { + foreach (OLVGroup group in this.OLVGroups) + { + if (group.Collapsed) + yield return group; + } + } + } + set + { + if (this.OLVGroups == null) + return; + + Hashtable shouldCollapse = new Hashtable(); + if (value != null) + { + foreach (OLVGroup group in value) + shouldCollapse[group.Key] = true; + } + foreach (OLVGroup group in this.OLVGroups) + { + group.Collapsed = shouldCollapse.ContainsKey(group.Key); + } + + } + } + + /// + /// Gets or sets whether the user wants to owner draw the header control + /// themselves. If this is false (the default), ObjectListView will use + /// custom drawing to render the header, if needed. + /// + /// + /// If you listen for the DrawColumnHeader event, you need to set this to true, + /// otherwise your event handler will not be called. + /// + [Category("ObjectListView"), + Description("Should the DrawColumnHeader event be triggered"), + DefaultValue(false)] + public bool OwnerDrawnHeader { + get { return ownerDrawnHeader; } + set { ownerDrawnHeader = value; } + } + private bool ownerDrawnHeader; + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// This method preserves selection, if possible. Use if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code and performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// The property DOES work on virtual lists: setting is problem-free, but if you try to get it + /// and the list has 10 million objects, it may take some time to return. + /// This collection is unfiltered. Use to access just those objects + /// that survive any installed filters. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable Objects { + get { return this.objects; } + set { this.SetObjects(value, true); } + } + private IEnumerable objects; + + /// + /// Gets the collection of objects that will be considered when creating clusters + /// (which are used to generate Excel-like column filters) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable ObjectsForClustering { + get { return this.Objects; } + } + + /// + /// Gets or sets the image that will be drawn over the top of the ListView + /// + [Category("ObjectListView"), + Description("The image that will be drawn over the top of the ListView"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ImageOverlay OverlayImage { + get { return this.imageOverlay; } + set { + if (this.imageOverlay == value) + return; + + this.RemoveOverlay(this.imageOverlay); + this.imageOverlay = value; + this.AddOverlay(this.imageOverlay); + } + } + private ImageOverlay imageOverlay; + + /// + /// Gets or sets the text that will be drawn over the top of the ListView + /// + [Category("ObjectListView"), + Description("The text that will be drawn over the top of the ListView"), + DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public TextOverlay OverlayText { + get { return this.textOverlay; } + set { + if (this.textOverlay == value) + return; + + this.RemoveOverlay(this.textOverlay); + this.textOverlay = value; + this.AddOverlay(this.textOverlay); + } + } + private TextOverlay textOverlay; + + /// + /// Gets or sets the transparency of all the overlays. + /// 0 is completely transparent, 255 is completely opaque. + /// + /// + /// This is obsolete. Use Transparency on each overlay. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int OverlayTransparency { + get { return this.overlayTransparency; } + set { this.overlayTransparency = Math.Min(255, Math.Max(0, value)); } + } + private int overlayTransparency = 128; + + /// + /// Gets the list of overlays that will be drawn on top of the ListView + /// + /// + /// You can add new overlays and remove overlays that you have added, but + /// don't mess with the overlays that you didn't create. + /// + [Browsable(false)] + protected IList Overlays { + get { return this.overlays; } + } + private readonly List overlays = new List(); + + /// + /// Gets or sets whether or not primary checkboxes will persistent their values across list rebuild + /// and filtering operations. + /// + /// + /// + /// This property is only useful when you don't explicitly set CheckStateGetter/Putter. + /// If you use CheckStateGetter/Putter, the checkedness of a row will already be persisted + /// by those methods. + /// + /// This defaults to true. If this is false, checkboxes will lose their values when the + /// list if rebuild or filtered. + /// If you set it to false on virtual lists, + /// you have to install CheckStateGetter/Putters. + /// + [Category("ObjectListView"), + Description("Will primary checkboxes persistent their values across list rebuilds"), + DefaultValue(true)] + public virtual bool PersistentCheckBoxes { + get { return persistentCheckBoxes; } + set { + if (persistentCheckBoxes == value) + return; + persistentCheckBoxes = value; + this.ClearPersistentCheckState(); + } + } + private bool persistentCheckBoxes = true; + + /// + /// Gets or sets a dictionary that remembers the check state of model objects + /// + /// This is used when PersistentCheckBoxes is true and for virtual lists. + protected Dictionary CheckStateMap { + get { return checkStateMap ?? (checkStateMap = new Dictionary()); } + set { checkStateMap = value; } + } + private Dictionary checkStateMap; + + /// + /// Which column did we last sort by + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn PrimarySortColumn { + get { return this.primarySortColumn; } + set { + this.primarySortColumn = value; + if (this.TintSortColumn) + this.SelectedColumn = value; + } + } + private OLVColumn primarySortColumn; + + /// + /// Which direction did we last sort + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder PrimarySortOrder { + get { return primarySortOrder; } + set { primarySortOrder = value; } + } + private SortOrder primarySortOrder; + + /// + /// Gets or sets if non-editable checkboxes are drawn as disabled. Default is false. + /// + /// + /// This only has effect in owner drawn mode. + /// + [Category("ObjectListView"), + Description("Should non-editable checkboxes be drawn as disabled?"), + DefaultValue(false)] + public virtual bool RenderNonEditableCheckboxesAsDisabled { + get { return renderNonEditableCheckboxesAsDisabled; } + set { renderNonEditableCheckboxesAsDisabled = value; } + } + private bool renderNonEditableCheckboxesAsDisabled; + + /// + /// Specify the height of each row in the control in pixels. + /// + /// The row height in a listview is normally determined by the font size and the small image list size. + /// This setting allows that calculation to be overridden (within reason: you still cannot set the line height to be + /// less than the line height of the font used in the control). + /// Setting it to -1 means use the normal calculation method. + /// This feature is experiemental! Strange things may happen to your program, + /// your spouse or your pet if you use it. + /// + [Category("ObjectListView"), + Description("Specify the height of each row in pixels. -1 indicates default height"), + DefaultValue(-1)] + public virtual int RowHeight { + get { return rowHeight; } + set { + if (value < 1) + rowHeight = -1; + else + rowHeight = value; + if (this.DesignMode) + return; + this.SetupBaseImageList(); + if (this.CheckBoxes) + this.InitializeStateImageList(); + } + } + private int rowHeight = -1; + + /// + /// How many pixels high is each row? + /// + [Browsable(false)] + public virtual int RowHeightEffective { + get { + switch (this.View) { + case View.List: + case View.SmallIcon: + case View.Details: + return Math.Max(this.SmallImageSize.Height, this.Font.Height); + + case View.Tile: + return this.TileSize.Height; + + case View.LargeIcon: + if (this.LargeImageList == null) + return this.Font.Height; + + return Math.Max(this.LargeImageList.ImageSize.Height, this.Font.Height); + + default: + // This should never happen + return 0; + } + } + } + + /// + /// How many rows appear on each page of this control + /// + [Browsable(false)] + public virtual int RowsPerPage { + get { + return NativeMethods.GetCountPerPage(this); + } + } + + /// + /// Get/set the column that will be used to resolve comparisons that are equal when sorting. + /// + /// There is no user interface for this setting. It must be set programmatically. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVColumn SecondarySortColumn { + get { return this.secondarySortColumn; } + set { this.secondarySortColumn = value; } + } + private OLVColumn secondarySortColumn; + + /// + /// When the SecondarySortColumn is used, in what order will it compare results? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortOrder SecondarySortOrder { + get { return this.secondarySortOrder; } + set { this.secondarySortOrder = value; } + } + private SortOrder secondarySortOrder = SortOrder.None; + + /// + /// Gets or sets if all rows should be selected when the user presses Ctrl-A + /// + [Category("ObjectListView"), + Description("Should the control select all rows when the user presses Ctrl-A?"), + DefaultValue(true)] + public virtual bool SelectAllOnControlA { + get { return selectAllOnControlA; } + set { selectAllOnControlA = value; } + } + private bool selectAllOnControlA = true; + + /// + /// When the user right clicks on the column headers, should a menu be presented which will allow + /// them to choose which columns will be shown in the view? + /// + /// This is just a compatibility wrapper for the SelectColumnsOnRightClickBehaviour + /// property. + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, should a menu be presented which will allow them to choose which columns will be shown in the view?"), + DefaultValue(true)] + public virtual bool SelectColumnsOnRightClick { + get { return this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None; } + set { + if (value) { + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.None) + this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; + } else { + this.SelectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.None; + } + } + } + + /// + /// Gets or sets how the user will be able to select columns when the header is right clicked + /// + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, how will the user be able to select columns?"), + DefaultValue(ColumnSelectBehaviour.InlineMenu)] + public virtual ColumnSelectBehaviour SelectColumnsOnRightClickBehaviour { + get { return selectColumnsOnRightClickBehaviour; } + set { selectColumnsOnRightClickBehaviour = value; } + } + private ColumnSelectBehaviour selectColumnsOnRightClickBehaviour = ColumnSelectBehaviour.InlineMenu; + + /// + /// When the column select menu is open, should it stay open after an item is selected? + /// Staying open allows the user to turn more than one column on or off at a time. + /// + /// This only works when SelectColumnsOnRightClickBehaviour is set to InlineMenu. + /// It has no effect when the behaviour is set to SubMenu. + [Category("ObjectListView"), + Description("When the column select inline menu is open, should it stay open after an item is selected?"), + DefaultValue(true)] + public virtual bool SelectColumnsMenuStaysOpen { + get { return selectColumnsMenuStaysOpen; } + set { selectColumnsMenuStaysOpen = value; } + } + private bool selectColumnsMenuStaysOpen = true; + + /// + /// Gets or sets the column that is drawn with a slight tint. + /// + /// + /// + /// If TintSortColumn is true, the sort column will automatically + /// be made the selected column. + /// + /// + /// The colour of the tint is controlled by SelectedColumnTint. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVColumn SelectedColumn { + get { return this.selectedColumn; } + set { + this.selectedColumn = value; + if (value == null) { + this.RemoveDecoration(this.selectedColumnDecoration); + } else { + if (!this.HasDecoration(this.selectedColumnDecoration)) + this.AddDecoration(this.selectedColumnDecoration); + } + } + } + private OLVColumn selectedColumn; + private readonly TintedColumnDecoration selectedColumnDecoration = new TintedColumnDecoration(); + + /// + /// Gets or sets the decoration that will be drawn on all selected rows + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IDecoration SelectedRowDecoration { + get { return this.selectedRowDecoration; } + set { this.selectedRowDecoration = value; } + } + private IDecoration selectedRowDecoration; + + /// + /// What color should be used to tint the selected column? + /// + /// + /// The tint color must be alpha-blendable, so if the given color is solid + /// (i.e. alpha = 255), it will be changed to have a reasonable alpha value. + /// + [Category("ObjectListView"), + Description("The color that will be used to tint the selected column"), + DefaultValue(typeof(Color), "")] + public virtual Color SelectedColumnTint { + get { return selectedColumnTint; } + set { + this.selectedColumnTint = value.A == 255 ? Color.FromArgb(15, value) : value; + this.selectedColumnDecoration.Tint = this.selectedColumnTint; + } + } + private Color selectedColumnTint = Color.Empty; + + /// + /// Gets or sets the index of the row that is currently selected. + /// When getting the index, if no row is selected,or more than one is selected, return -1. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int SelectedIndex { + get { return this.SelectedIndices.Count == 1 ? this.SelectedIndices[0] : -1; } + set { + this.SelectedIndices.Clear(); + if (value >= 0 && value < this.Items.Count) + this.SelectedIndices.Add(value); + } + } + + /// + /// Gets or sets the ListViewItem that is currently selected . If no row is selected, or more than one is selected, return null. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual OLVListItem SelectedItem { + get { + return this.SelectedIndices.Count == 1 ? this.GetItem(this.SelectedIndices[0]) : null; + } + set { + this.SelectedIndices.Clear(); + if (value != null) + this.SelectedIndices.Add(value.Index); + } + } + + /// + /// Gets the model object from the currently selected row, if there is only one row selected. + /// If no row is selected, or more than one is selected, returns null. + /// When setting, this will select the row that is displaying the given model object and focus on it. + /// All other rows are deselected. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual Object SelectedObject { + get { + return this.SelectedIndices.Count == 1 ? this.GetModelObject(this.SelectedIndices[0]) : null; + } + set { + // If the given model is already selected, don't do anything else (prevents an flicker) + object selectedObject = this.SelectedObject; + if (selectedObject != null && selectedObject.Equals(value)) + return; + + this.SelectedIndices.Clear(); + this.SelectObject(value, true); + } + } + + /// + /// Get the model objects from the currently selected rows. If no row is selected, the returned List will be empty. + /// When setting this value, select the rows that is displaying the given model objects. All other rows are deselected. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IList SelectedObjects { + get { + ArrayList list = new ArrayList(); + foreach (int index in this.SelectedIndices) + list.Add(this.GetModelObject(index)); + return list; + } + set { + this.SelectedIndices.Clear(); + this.SelectObjects(value); + } + } + + /// + /// When the user right clicks on the column headers, should a menu be presented which will allow + /// them to choose common tasks to perform on the listview? + /// + [Category("ObjectListView"), + Description("When the user right clicks on the column headers, should a menu be presented which will allow them to perform common tasks on the listview?"), + DefaultValue(false)] + public virtual bool ShowCommandMenuOnRightClick { + get { return showCommandMenuOnRightClick; } + set { showCommandMenuOnRightClick = value; } + } + private bool showCommandMenuOnRightClick; + + /// + /// Gets or sets whether this ObjectListView will show Excel like filtering + /// menus when the header control is right clicked + /// + [Category("ObjectListView"), + Description("If this is true, right clicking on a column header will show a Filter menu option"), + DefaultValue(true)] + public bool ShowFilterMenuOnRightClick { + get { return showFilterMenuOnRightClick; } + set { showFilterMenuOnRightClick = value; } + } + private bool showFilterMenuOnRightClick = true; + + /// + /// Should this list show its items in groups? + /// + [Category("Appearance"), + Description("Should the list view show items in groups?"), + DefaultValue(true)] + new public virtual bool ShowGroups { + get { return base.ShowGroups; } + set { + this.GroupImageList = this.GroupImageList; + base.ShowGroups = value; + } + } + + /// + /// Should the list view show a bitmap in the column header to show the sort direction? + /// + /// + /// The only reason for not wanting to have sort indicators is that, on pre-XP versions of + /// Windows, having sort indicators required the ListView to have a small image list, and + /// as soon as you give a ListView a SmallImageList, the text of column 0 is bumped 16 + /// pixels to the right, even if you never used an image. + /// + [Category("ObjectListView"), + Description("Should the list view show sort indicators in the column headers?"), + DefaultValue(true)] + public virtual bool ShowSortIndicators { + get { return showSortIndicators; } + set { showSortIndicators = value; } + } + private bool showSortIndicators; + + /// + /// Should the list view show images on subitems? + /// + /// + /// Virtual lists have to be owner drawn in order to show images on subitems + /// + [Category("ObjectListView"), + Description("Should the list view show images on subitems?"), + DefaultValue(false)] + public virtual bool ShowImagesOnSubItems { + get { return showImagesOnSubItems; } + set { + showImagesOnSubItems = value; + if (this.Created) + this.ApplyExtendedStyles(); + if (value && this.VirtualMode) + this.OwnerDraw = true; + } + } + private bool showImagesOnSubItems; + + /// + /// This property controls whether group labels will be suffixed with a count of items. + /// + /// + /// The format of the suffix is controlled by GroupWithItemCountFormat/GroupWithItemCountSingularFormat properties + /// + [Category("ObjectListView"), + Description("Will group titles be suffixed with a count of the items in the group?"), + DefaultValue(false)] + public virtual bool ShowItemCountOnGroups { + get { return showItemCountOnGroups; } + set { showItemCountOnGroups = value; } + } + private bool showItemCountOnGroups; + + /// + /// Gets or sets whether the control will show column headers in all + /// views (true), or only in Details view (false) + /// + /// + /// + /// This property is not working correctly. JPP 2010/04/06. + /// It works fine if it is set before the control is created. + /// But if it turned off once the control is created, the control + /// loses its checkboxes (weird!) + /// + /// + /// To changed this setting after the control is created, things + /// are complicated. If it is off and we want it on, we have + /// to change the View and the header will appear. If it is currently + /// on and we want to turn it off, we have to both change the view + /// AND recreate the handle. Recreating the handle is a problem + /// since it makes our checkbox style disappear. + /// + /// + /// This property doesn't work on XP. + /// + [Category("ObjectListView"), + Description("Will the control will show column headers in all views?"), + DefaultValue(true)] + public bool ShowHeaderInAllViews { + get { return ObjectListView.IsVistaOrLater && showHeaderInAllViews; } + set { + if (showHeaderInAllViews == value) + return; + + showHeaderInAllViews = value; + + // If the control isn't already created, everything is fine. + if (!this.Created) + return; + + // If the header is being hidden, we have to recreate the control + // to remove the style (not sure why this is) + if (showHeaderInAllViews) + this.ApplyExtendedStyles(); + else + this.RecreateHandle(); + + // Still more complications. The change doesn't become visible until the View is changed + if (this.View != View.Details) { + View temp = this.View; + this.View = View.Details; + this.View = temp; + } + } + } + private bool showHeaderInAllViews = true; + + /// + /// Override the SmallImageList property so we can correctly shadow its operations. + /// + /// If you use the RowHeight property to specify the row height, the SmallImageList + /// must be fully initialised before setting/changing the RowHeight. If you add new images to the image + /// list after setting the RowHeight, you must assign the imagelist to the control again. Something as simple + /// as this will work: + /// listView1.SmallImageList = listView1.SmallImageList; + /// + new public ImageList SmallImageList { + get { return this.shadowedImageList; } + set { + this.shadowedImageList = value; + if (this.UseSubItemCheckBoxes) + this.SetupSubItemCheckBoxes(); + this.SetupBaseImageList(); + } + } + private ImageList shadowedImageList; + + /// + /// Return the size of the images in the small image list or a reasonable default + /// + [Browsable(false)] + public virtual Size SmallImageSize { + get { + return this.BaseSmallImageList == null ? new Size(16, 16) : this.BaseSmallImageList.ImageSize; + } + } + + /// + /// When the listview is grouped, should the items be sorted by the primary column? + /// If this is false, the items will be sorted by the same column as they are grouped. + /// + [Category("ObjectListView"), + Description("When the listview is grouped, should the items be sorted by the primary column? If this is false, the items will be sorted by the same column as they are grouped."), + DefaultValue(true)] + public virtual bool SortGroupItemsByPrimaryColumn { + get { return this.sortGroupItemsByPrimaryColumn; } + set { this.sortGroupItemsByPrimaryColumn = value; } + } + private bool sortGroupItemsByPrimaryColumn = true; + + /// + /// When the listview is grouped, how many pixels should exist between the end of one group and the + /// beginning of the next? + /// + [Category("ObjectListView"), + Description("How many pixels of space will be between groups"), + DefaultValue(0)] + public virtual int SpaceBetweenGroups { + get { return this.spaceBetweenGroups; } + set { + if (this.spaceBetweenGroups == value) + return; + + this.spaceBetweenGroups = value; + this.SetGroupSpacing(); + } + } + private int spaceBetweenGroups; + + private void SetGroupSpacing() { + if (!this.IsHandleCreated) + return; + + NativeMethods.LVGROUPMETRICS metrics = new NativeMethods.LVGROUPMETRICS(); + metrics.cbSize = ((uint)Marshal.SizeOf(typeof(NativeMethods.LVGROUPMETRICS))); + metrics.mask = (uint)GroupMetricsMask.LVGMF_BORDERSIZE; + metrics.Bottom = (uint)this.SpaceBetweenGroups; + NativeMethods.SetGroupMetrics(this, metrics); + } + + /// + /// Should the sort column show a slight tinge? + /// + [Category("ObjectListView"), + Description("Should the sort column show a slight tinting?"), + DefaultValue(false)] + public virtual bool TintSortColumn { + get { return this.tintSortColumn; } + set { + this.tintSortColumn = value; + if (value && this.PrimarySortColumn != null) + this.SelectedColumn = this.PrimarySortColumn; + else + this.SelectedColumn = null; + } + } + private bool tintSortColumn; + + /// + /// Should each row have a tri-state checkbox? + /// + /// + /// If this is true, the user can choose the third state (normally Indeterminate). Otherwise, user clicks + /// alternate between checked and unchecked. CheckStateGetter can still return Indeterminate when this + /// setting is false. + /// + [Category("ObjectListView"), + Description("Should the primary column have a checkbox that behaves as a tri-state checkbox?"), + DefaultValue(false)] + public virtual bool TriStateCheckBoxes { + get { return triStateCheckBoxes; } + set { + triStateCheckBoxes = value; + if (value && !this.CheckBoxes) + this.CheckBoxes = true; + this.InitializeStateImageList(); + } + } + private bool triStateCheckBoxes; + + /// + /// Get or set the index of the top item of this listview + /// + /// + /// + /// This property only works when the listview is in Details view and not showing groups. + /// + /// + /// The reason that it does not work when showing groups is that, when groups are enabled, + /// the Windows msg LVM_GETTOPINDEX always returns 0, regardless of the + /// scroll position. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual int TopItemIndex { + get { + if (this.View == View.Details && this.IsHandleCreated) + return NativeMethods.GetTopIndex(this); + + return -1; + } + set { + int newTopIndex = Math.Min(value, this.GetItemCount() - 1); + if (this.View != View.Details || newTopIndex < 0) + return; + + try { + this.TopItem = this.Items[newTopIndex]; + + // Setting the TopItem sometimes gives off by one errors, + // that (bizarrely) are correct on a second attempt + if (this.TopItem != null && this.TopItem.Index != newTopIndex) + this.TopItem = this.GetItem(newTopIndex); + } + catch (NullReferenceException) { + // There is a bug in the .NET code where setting the TopItem + // will sometimes throw null reference exceptions + // There is nothing we can do to get around it. + } + } + } + + /// + /// Gets or sets whether moving the mouse over the header will trigger CellOver events. + /// Defaults to true. + /// + /// + /// Moving the mouse over the header did not previously trigger CellOver events, since the + /// header is considered a separate control. + /// If this change in behaviour causes your application problems, set this to false. + /// If you are interested in knowing when the mouse moves over the header, set this property to true (the default). + /// + [Category("ObjectListView"), + Description("Should moving the mouse over the header trigger CellOver events?"), + DefaultValue(true)] + public bool TriggerCellOverEventsWhenOverHeader + { + get { return triggerCellOverEventsWhenOverHeader; } + set { triggerCellOverEventsWhenOverHeader = value; } + } + private bool triggerCellOverEventsWhenOverHeader = true; + + /// + /// When resizing a column by dragging its divider, should any space filling columns be + /// resized at each mouse move? If this is false, the filling columns will be + /// updated when the mouse is released. + /// + /// + /// + /// If you have a space filling column + /// is in the left of the column that is being resized, this will look odd: + /// the right edge of the column will be dragged, but + /// its left edge will move since the space filling column is shrinking. + /// + /// This is logical behaviour -- it just looks wrong. + /// + /// + /// Given the above behavior is probably best to turn this property off if your space filling + /// columns aren't the right-most columns. + /// + [Category("ObjectListView"), + Description("When resizing a column by dragging its divider, should any space filling columns be resized at each mouse move?"), + DefaultValue(true)] + public virtual bool UpdateSpaceFillingColumnsWhenDraggingColumnDivider { + get { return updateSpaceFillingColumnsWhenDraggingColumnDivider; } + set { updateSpaceFillingColumnsWhenDraggingColumnDivider = value; } + } + private bool updateSpaceFillingColumnsWhenDraggingColumnDivider = true; + + /// + /// What color should be used for the background of selected rows when the control doesn't have the focus? + /// + /// Windows does not give the option of changing the selection background. + /// So the control has to be owner drawn to see the result of this setting. + /// Setting UseCustomSelectionColors = true will do this for you. + [Category("ObjectListView"), + Description("The background color of selected rows when the control is owner drawn and doesn't have the focus"), + DefaultValue(typeof(Color), "")] + public virtual Color UnfocusedHighlightBackgroundColor { + get { return unfocusedHighlightBackgroundColor; } + set { unfocusedHighlightBackgroundColor = value; } + } + private Color unfocusedHighlightBackgroundColor = Color.Empty; + + /// + /// Return the color should be used for the background of selected rows when the control doesn't have the focus or a reasonable default + /// + [Browsable(false)] + public virtual Color UnfocusedHighlightBackgroundColorOrDefault { + get { + return this.UnfocusedHighlightBackgroundColor.IsEmpty ? SystemColors.Control : this.UnfocusedHighlightBackgroundColor; + } + } + + /// + /// What color should be used for the foreground of selected rows when the control doesn't have the focus? + /// + /// Windows does not give the option of changing the selection foreground (text color). + /// So the control has to be owner drawn to see the result of this setting. + /// Setting UseCustomSelectionColors = true will do this for you. + [Category("ObjectListView"), + Description("The foreground color of selected rows when the control is owner drawn and doesn't have the focus"), + DefaultValue(typeof(Color), "")] + public virtual Color UnfocusedHighlightForegroundColor { + get { return unfocusedHighlightForegroundColor; } + set { unfocusedHighlightForegroundColor = value; } + } + private Color unfocusedHighlightForegroundColor = Color.Empty; + + /// + /// Return the color should be used for the foreground of selected rows when the control doesn't have the focus or a reasonable default + /// + [Browsable(false)] + public virtual Color UnfocusedHighlightForegroundColorOrDefault { + get { + return this.UnfocusedHighlightForegroundColor.IsEmpty ? SystemColors.ControlText : this.UnfocusedHighlightForegroundColor; + } + } + + /// + /// Should the list give a different background color to every second row? + /// + /// The color of the alternate rows is given by AlternateRowBackColor. + /// There is a "feature" in .NET for listviews in non-full-row-select mode, where + /// selected rows are not drawn with their correct background color. + [Category("ObjectListView"), + Description("Should the list view use a different backcolor to alternate rows?"), + DefaultValue(false)] + public virtual bool UseAlternatingBackColors { + get { return useAlternatingBackColors; } + set { useAlternatingBackColors = value; } + } + private bool useAlternatingBackColors; + + /// + /// Should FormatCell events be called for each cell in the control? + /// + /// + /// In many situations, no cell level formatting is performed. ObjectListView + /// can run somewhat faster if it does not trigger a format cell event for every cell + /// unless it is required. So, by default, it does not raise an event for each cell. + /// + /// ObjectListView *does* raise a FormatRow event every time a row is rebuilt. + /// Individual rows can decide whether to raise FormatCell + /// events for every cell in row. + /// + /// + [Category("ObjectListView"), + Description("Should FormatCell events be triggered to every cell that is built?"), + DefaultValue(false)] + public bool UseCellFormatEvents { + get { return useCellFormatEvents; } + set { useCellFormatEvents = value; } + } + private bool useCellFormatEvents; + + /// + /// Should the selected row be drawn with non-standard foreground and background colors? + /// + /// + /// When this is enabled, the control becomes owner drawn. + /// + [Category("ObjectListView"), + Description("Should the selected row be drawn with non-standard foreground and background colors?"), + DefaultValue(false)] + public bool UseCustomSelectionColors { + get { return this.useCustomSelectionColors; } + set { + this.useCustomSelectionColors = value; + + if (!this.DesignMode && value) + this.OwnerDraw = true; + } + } + private bool useCustomSelectionColors; + + /// + /// Gets or sets whether this ObjectListView will use the same hot item and selection + /// mechanism that Vista Explorer does. + /// + /// This property has many imperfections: + /// + /// This only works on Vista and later + /// It does nothing for owner drawn lists. + /// Owner drawn lists are (naturally) controlled by their renderers. + /// It does not work well with AlternateRowBackColors. + /// It does not play well with HotItemStyles. + /// It looks a little bit silly is FullRowSelect is false. + /// + /// But if you absolutely have to look like Vista, this is your property. + /// Do not complain if settings this messes up other things. + /// + [Category("ObjectListView"), + Description("Should the list use the same hot item and selection mechanism as Vista?"), + DefaultValue(false)] + public bool UseExplorerTheme { + get { return useExplorerTheme; } + set { + useExplorerTheme = value; + if (this.Created) + NativeMethods.SetWindowTheme(this.Handle, value ? "explorer" : "", null); + } + } + private bool useExplorerTheme; + + /// + /// Gets or sets whether the list should enable filtering + /// + [Category("ObjectListView"), + Description("Should the list enable filtering?"), + DefaultValue(false)] + virtual public bool UseFiltering { + get { return useFiltering; } + set { + if (useFiltering == value) + return; + useFiltering = value; + this.UpdateFiltering(); + } + } + private bool useFiltering; + + /// + /// Gets or sets whether the list should put an indicator into a column's header to show that + /// it is filtering on that column + /// + /// If you set this to true, HeaderUsesThemes is automatically set to false, since + /// we can only draw a filter indicator when not using a themed header. + [Category("ObjectListView"), + Description("Should an image be drawn in a column's header when that column is being used for filtering?"), + DefaultValue(false)] + virtual public bool UseFilterIndicator { + get { return useFilterIndicator; } + set { + if (this.useFilterIndicator == value) + return; + useFilterIndicator = value; + if (this.useFilterIndicator) + this.HeaderUsesThemes = false; + this.Invalidate(); + } + } + private bool useFilterIndicator; + + /// + /// Should the item under the cursor be formatted in a special way? + /// + [Category("ObjectListView"), + Description("Should HotTracking be used? Hot tracking applies special formatting to the row under the cursor"), + DefaultValue(false)] + public bool UseHotItem { + get { return this.useHotItem; } + set { + this.useHotItem = value; + if (this.HotItemStyle != null) { + if (value) + this.AddOverlay(this.HotItemStyle.Overlay); + else + this.RemoveOverlay(this.HotItemStyle.Overlay); + } + } + } + private bool useHotItem; + + /// + /// Gets or sets whether this listview should show hyperlinks in the cells. + /// + [Category("ObjectListView"), + Description("Should hyperlinks be shown on this control?"), + DefaultValue(false)] + public bool UseHyperlinks { + get { return this.useHyperlinks; } + set { + this.useHyperlinks = value; + if (value && this.HyperlinkStyle == null) + this.HyperlinkStyle = new HyperlinkStyle(); + } + } + private bool useHyperlinks; + + /// + /// Should this control show overlays + /// + /// Overlays are enabled by default and would only need to be disabled + /// if they were causing problems in your development environment. + [Category("ObjectListView"), + Description("Should this control show overlays"), + DefaultValue(true)] + public bool UseOverlays { + get { return this.useOverlays; } + set { this.useOverlays = value; } + } + private bool useOverlays = true; + + /// + /// Should this control be configured to show check boxes on subitems? + /// + /// If this is set to True, the control will be given a SmallImageList if it + /// doesn't already have one. Also, if it is a virtual list, it will be set to owner + /// drawn, since virtual lists can't draw check boxes without being owner drawn. + [Category("ObjectListView"), + Description("Should this control be configured to show check boxes on subitems."), + DefaultValue(false)] + public bool UseSubItemCheckBoxes { + get { return this.useSubItemCheckBoxes; } + set { + this.useSubItemCheckBoxes = value; + if (value) + this.SetupSubItemCheckBoxes(); + } + } + private bool useSubItemCheckBoxes; + + /// + /// Gets or sets if the ObjectListView will use a translucent selection mechanism like Vista. + /// + /// + /// + /// Unlike UseExplorerTheme, this Vista-like scheme works on XP and for both + /// owner and non-owner drawn lists. + /// + /// + /// This will replace any SelectedRowDecoration that has been installed. + /// + /// + /// If you don't like the colours used for the selection, ignore this property and + /// just create your own RowBorderDecoration and assigned it to SelectedRowDecoration, + /// just like this property setter does. + /// + /// + [Category("ObjectListView"), + Description("Should the list use a translucent selection mechanism (like Vista)"), + DefaultValue(false)] + public bool UseTranslucentSelection { + get { return useTranslucentSelection; } + set { + useTranslucentSelection = value; + if (value) { + RowBorderDecoration rbd = new RowBorderDecoration(); + rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); + rbd.FillBrush = new SolidBrush(Color.FromArgb(48, 163, 217, 225)); + rbd.BoundsPadding = new Size(0, 0); + rbd.CornerRounding = 6.0f; + this.SelectedRowDecoration = rbd; + } else + this.SelectedRowDecoration = null; + } + } + private bool useTranslucentSelection; + + /// + /// Gets or sets if the ObjectListView will use a translucent hot row highlighting mechanism like Vista. + /// + /// + /// + /// Setting this will replace any HotItemStyle that has been installed. + /// + /// + /// If you don't like the colours used for the hot item, ignore this property and + /// just create your own HotItemStyle, fill in the values you want, and assigned it to HotItemStyle property, + /// just like this property setter does. + /// + /// + [Category("ObjectListView"), + Description("Should the list use a translucent hot row highlighting mechanism (like Vista)"), + DefaultValue(false)] + public bool UseTranslucentHotItem { + get { return useTranslucentHotItem; } + set { + useTranslucentHotItem = value; + if (value) { + this.HotItemStyle = new HotItemStyle(); + RowBorderDecoration rbd = new RowBorderDecoration(); + rbd.BorderPen = new Pen(Color.FromArgb(154, 223, 251)); + rbd.BoundsPadding = new Size(0, 0); + rbd.CornerRounding = 6.0f; + rbd.FillGradientFrom = Color.FromArgb(0, 255, 255, 255); + rbd.FillGradientTo = Color.FromArgb(64, 183, 237, 240); + this.HotItemStyle.Decoration = rbd; + } else + this.HotItemStyle = null; + this.UseHotItem = value; + } + } + private bool useTranslucentHotItem; + + /// + /// Get/set the style of view that this listview is using + /// + /// Switching to tile or details view installs the columns appropriate to that view. + /// Confusingly, in tile view, every column is shown as a row of information. + new public View View { + get { return base.View; } + set { + if (base.View == value) + return; + + if (this.Frozen) { + base.View = value; + this.SetupBaseImageList(); + } else { + this.Freeze(); + + if (value == View.Tile) + this.CalculateReasonableTileSize(); + + base.View = value; + this.SetupBaseImageList(); + this.Unfreeze(); + } + } + } + + #endregion + + #region Callbacks + + /// + /// This delegate fetches the checkedness of an object as a boolean only. + /// + /// Use this if you never want to worry about the + /// Indeterminate state (which is fairly common). + /// + /// This is a convenience wrapper around the CheckStateGetter property. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual BooleanCheckStateGetterDelegate BooleanCheckStateGetter { + set { + if (value == null) + this.CheckStateGetter = null; + else + this.CheckStateGetter = delegate(Object x) { + return value(x) ? CheckState.Checked : CheckState.Unchecked; + }; + } + } + + /// + /// This delegate sets the checkedness of an object as a boolean only. It must return + /// true or false indicating if the object was checked or not. + /// + /// Use this if you never want to worry about the + /// Indeterminate state (which is fairly common). + /// + /// This is a convenience wrapper around the CheckStatePutter property. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual BooleanCheckStatePutterDelegate BooleanCheckStatePutter { + set { + if (value == null) + this.CheckStatePutter = null; + else + this.CheckStatePutter = delegate(Object x, CheckState state) { + bool isChecked = (state == CheckState.Checked); + return value(x, isChecked) ? CheckState.Checked : CheckState.Unchecked; + }; + } + } + + /// + /// Gets whether or not this listview is capabale of showing groups + /// + [Browsable(false)] + public virtual bool CanShowGroups { + get { + return true; + } + } + + /// + /// Gets or sets whether ObjectListView can rely on Application.Idle events + /// being raised. + /// + /// In some host environments (e.g. when running as an extension within + /// VisualStudio and possibly Office), Application.Idle events are never raised. + /// Set this to false when Idle events will not be raised, and ObjectListView will + /// raise those events itself. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool CanUseApplicationIdle { + get { return this.canUseApplicationIdle; } + set { this.canUseApplicationIdle = value; } + } + private bool canUseApplicationIdle = true; + + /// + /// This delegate is called when the list wants to show a tooltip for a particular cell. + /// The delegate should return the text to display, or null to use the default behavior + /// (which is to show the full text of truncated cell values). + /// + /// + /// Displaying the full text of truncated cell values only work for FullRowSelect listviews. + /// This is MS's behavior, not mine. Don't complain to me :) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CellToolTipGetterDelegate CellToolTipGetter { + get { return cellToolTipGetter; } + set { cellToolTipGetter = value; } + } + private CellToolTipGetterDelegate cellToolTipGetter; + + /// + /// The name of the property (or field) that holds whether or not a model is checked. + /// + /// + /// The property be modifiable. It must have a return type of bool (or of bool? if + /// TriStateCheckBoxes is true). + /// Setting this property replaces any CheckStateGetter or CheckStatePutter that have been installed. + /// Conversely, later setting the CheckStateGetter or CheckStatePutter properties will take precedence + /// over the behavior of this property. + /// + [Category("ObjectListView"), + Description("The name of the property or field that holds the 'checkedness' of the model"), + DefaultValue(null)] + public virtual string CheckedAspectName { + get { return checkedAspectName; } + set { + checkedAspectName = value; + if (String.IsNullOrEmpty(checkedAspectName)) { + this.checkedAspectMunger = null; + this.CheckStateGetter = null; + this.CheckStatePutter = null; + } else { + this.checkedAspectMunger = new Munger(checkedAspectName); + this.CheckStateGetter = delegate(Object modelObject) { + bool? result = this.checkedAspectMunger.GetValue(modelObject) as bool?; + if (result.HasValue) + return result.Value ? CheckState.Checked : CheckState.Unchecked; + return this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; + }; + this.CheckStatePutter = delegate(Object modelObject, CheckState newValue) { + if (this.TriStateCheckBoxes && newValue == CheckState.Indeterminate) + this.checkedAspectMunger.PutValue(modelObject, null); + else + this.checkedAspectMunger.PutValue(modelObject, newValue == CheckState.Checked); + return this.CheckStateGetter(modelObject); + }; + } + } + } + private string checkedAspectName; + private Munger checkedAspectMunger; + + /// + /// This delegate will be called whenever the ObjectListView needs to know the check state + /// of the row associated with a given model object. + /// + /// + /// .NET has no support for indeterminate values, but as of v2.0, this class allows + /// indeterminate values. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CheckStateGetterDelegate CheckStateGetter { + get { return checkStateGetter; } + set { checkStateGetter = value; } + } + private CheckStateGetterDelegate checkStateGetter; + + /// + /// This delegate will be called whenever the user tries to change the check state of a row. + /// The delegate should return the state that was actually set, which may be different + /// to the state given. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CheckStatePutterDelegate CheckStatePutter { + get { return checkStatePutter; } + set { checkStatePutter = value; } + } + private CheckStatePutterDelegate checkStatePutter; + + /// + /// This delegate can be used to sort the table in a custom fasion. + /// + /// + /// + /// The delegate must install a ListViewItemSorter on the ObjectListView. + /// Installing the ItemSorter does the actual work of sorting the ListViewItems. + /// See ColumnComparer in the code for an example of what an ItemSorter has to do. + /// + /// + /// Do not install a CustomSorter on a VirtualObjectListView. Override the SortObjects() + /// method of the IVirtualListDataSource instead. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual SortDelegate CustomSorter { + get { return customSorter; } + set { customSorter = value; } + } + private SortDelegate customSorter; + + /// + /// This delegate is called when the list wants to show a tooltip for a particular header. + /// The delegate should return the text to display, or null to use the default behavior + /// (which is to not show any tooltip). + /// + /// + /// Installing a HeaderToolTipGetter takes precedence over any text in OLVColumn.ToolTipText. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual HeaderToolTipGetterDelegate HeaderToolTipGetter { + get { return headerToolTipGetter; } + set { headerToolTipGetter = value; } + } + private HeaderToolTipGetterDelegate headerToolTipGetter; + + /// + /// This delegate can be used to format a OLVListItem before it is added to the control. + /// + /// + /// The model object for the row can be found through the RowObject property of the OLVListItem object. + /// All subitems normally have the same style as list item, so setting the forecolor on one + /// subitem changes the forecolor of all subitems. + /// To allow subitems to have different attributes, do this: + /// myListViewItem.UseItemStyleForSubItems = false;. + /// + /// If UseAlternatingBackColors is true, the backcolor of the listitem will be calculated + /// by the control and cannot be controlled by the RowFormatter delegate. + /// In general, trying to use a RowFormatter + /// when UseAlternatingBackColors is true does not work well. + /// As it says in the summary, this is called before the item is added to the control. + /// Many properties of the OLVListItem itself are not available at that point, including: + /// Index, Selected, Focused, Bounds, Checked, DisplayIndex. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual RowFormatterDelegate RowFormatter { + get { return rowFormatter; } + set { rowFormatter = value; } + } + private RowFormatterDelegate rowFormatter; + + #endregion + + #region List commands + + /// + /// Add the given model object to this control. + /// + /// The model object to be displayed + /// See AddObjects() for more details + public virtual void AddObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.AddObject(modelObject); }); + else + this.AddObjects(new object[] { modelObject }); + } + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// + /// The added objects will appear in their correct sort position, if sorting + /// is active (i.e. if PrimarySortColumn is not null). Otherwise, they will appear at the end of the list. + /// No check is performed to see if any of the objects are already in the ListView. + /// Null objects are silently ignored. + /// + public virtual void AddObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.AddObjects(modelObjects); }); + return; + } + this.InsertObjects(ObjectListView.EnumerableCount(this.Objects), modelObjects); + this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Resize the columns to the maximum of the header width and the data. + /// + public virtual void AutoResizeColumns() + { + foreach (OLVColumn c in this.Columns) + { + c.Width = -2; + } + } + + + /// + /// Set up any automatically initialized column widths (columns that + /// have a width of 0 or -1 will be resized to the width of their + /// contents or header respectively). + /// + public virtual void AutoSizeColumns() { + // If we are supposed to resize to content, but there is no content, resize to + // the header size instead. + ColumnHeaderAutoResizeStyle resizeToContentStyle = ColumnHeaderAutoResizeStyle.ColumnContent; + if (this.GetItemCount() == 0) + resizeToContentStyle = ColumnHeaderAutoResizeStyle.HeaderSize; + foreach (ColumnHeader column in this.Columns) { + switch (column.Width) { + case 0: + this.AutoResizeColumn(column.Index, resizeToContentStyle); + break; + case -1: + this.AutoResizeColumn(column.Index, ColumnHeaderAutoResizeStyle.HeaderSize); + break; + } + } + } + + /// + /// Organise the view items into groups, based on the last sort column or the first column + /// if there is no last sort column + /// + public virtual void BuildGroups() { + this.BuildGroups(this.PrimarySortColumn, this.PrimarySortOrder == SortOrder.None ? SortOrder.Ascending : this.PrimarySortOrder); + } + + /// + /// Organise the view items into groups, based on the given column + /// + /// + /// + /// If the AlwaysGroupByColumn property is not null, + /// the list view items will be organisd by that column, + /// and the 'column' parameter will be ignored. + /// + /// This method triggers sorting events: BeforeSorting and AfterSorting. + /// + /// The column whose values should be used for sorting. + /// + public virtual void BuildGroups(OLVColumn column, SortOrder order) { + // Sanity + if (this.GetItemCount() == 0 || this.Columns.Count == 0) + return; + + BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(column, order); + this.OnBeforeSorting(args); + if (args.Canceled) + return; + + this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, + args.ColumnToSort, args.SortOrder, args.SecondaryColumnToSort, args.SecondarySortOrder); + + this.OnAfterSorting(new AfterSortingEventArgs(args)); + } + + private BeforeSortingEventArgs BuildBeforeSortingEventArgs(OLVColumn column, SortOrder order) { + OLVColumn groupBy = this.AlwaysGroupByColumn ?? column ?? this.GetColumn(0); + SortOrder groupByOrder = this.AlwaysGroupBySortOrder; + if (order == SortOrder.None) { + order = this.Sorting; + if (order == SortOrder.None) + order = SortOrder.Ascending; + } + if (groupByOrder == SortOrder.None) + groupByOrder = order; + + BeforeSortingEventArgs args = new BeforeSortingEventArgs( + groupBy, groupByOrder, + column, order, + this.SecondarySortColumn ?? this.GetColumn(0), + this.SecondarySortOrder == SortOrder.None ? order : this.SecondarySortOrder); + if (column != null) + args.Canceled = !column.Sortable; + return args; + } + + /// + /// Organise the view items into groups, based on the given columns + /// + /// What column will be used for grouping + /// What ordering will be used for groups + /// The column whose values should be used for sorting. Cannot be null + /// The order in which the values from column will be sorted + /// When the values from 'column' are equal, use the values provided by this column + /// How will the secondary values be sorted + /// This method does not trigger sorting events. Use BuildGroups() to do that + public virtual void BuildGroups(OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn column, SortOrder order, OLVColumn secondaryColumn, SortOrder secondaryOrder) { + // Sanity checks + if (groupByColumn == null) + return; + + // Getting the Count forces any internal cache of the ListView to be flushed. Without + // this, iterating over the Items will not work correctly if the ListView handle + // has not yet been created. +#pragma warning disable 168 +// ReSharper disable once UnusedVariable + int dummy = this.Items.Count; +#pragma warning restore 168 + + // Collect all the information that governs the creation of groups + GroupingParameters parms = this.CollectGroupingParameters(groupByColumn, groupByOrder, + column, order, secondaryColumn, secondaryOrder); + + // Trigger an event to let the world create groups if they want + CreateGroupsEventArgs args = new CreateGroupsEventArgs(parms); + if (parms.GroupByColumn != null) + args.Canceled = !parms.GroupByColumn.Groupable; + this.OnBeforeCreatingGroups(args); + if (args.Canceled) + return; + + // If the event didn't create them for us, use our default strategy + if (args.Groups == null) + args.Groups = this.MakeGroups(parms); + + // Give the world a chance to munge the groups before they are created + this.OnAboutToCreateGroups(args); + if (args.Canceled) + return; + + // Create the groups now + this.OLVGroups = args.Groups; + this.CreateGroups(args.Groups); + + // Tell the world that new groups have been created + this.OnAfterCreatingGroups(args); + lastGroupingParameters = args.Parameters; + } + private GroupingParameters lastGroupingParameters; + + /// + /// Collect and return all the variables that influence the creation of groups + /// + /// + protected virtual GroupingParameters CollectGroupingParameters(OLVColumn groupByColumn, SortOrder groupByOrder, + OLVColumn sortByColumn, SortOrder sortByOrder, OLVColumn secondaryColumn, SortOrder secondaryOrder) { + + // If the user tries to group by a non-groupable column, keep the current group by + // settings, but use the non-groupable column for sorting + if (!groupByColumn.Groupable && lastGroupingParameters != null) { + sortByColumn = groupByColumn; + sortByOrder = groupByOrder; + groupByColumn = lastGroupingParameters.GroupByColumn; + groupByOrder = lastGroupingParameters.GroupByOrder; + } + + string titleFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountFormatOrDefault : null; + string titleSingularFormat = this.ShowItemCountOnGroups ? groupByColumn.GroupWithItemCountSingularFormatOrDefault : null; + GroupingParameters parms = new GroupingParameters(this, groupByColumn, groupByOrder, + sortByColumn, sortByOrder, secondaryColumn, secondaryOrder, + titleFormat, titleSingularFormat, this.SortGroupItemsByPrimaryColumn); + return parms; + } + + /// + /// Make a list of groups that should be shown according to the given parameters + /// + /// + /// The list of groups to be created + /// This should not change the state of the control. It is possible that the + /// groups created will not be used. They may simply be discarded. + protected virtual IList MakeGroups(GroupingParameters parms) { + + // There is a lot of overlap between this method and FastListGroupingStrategy.MakeGroups() + // Any changes made here may need to be reflected there + + // Separate the list view items into groups, using the group key as the descrimanent + NullableDictionary> map = new NullableDictionary>(); + foreach (OLVListItem olvi in parms.ListView.Items) { + object key = parms.GroupByColumn.GetGroupKey(olvi.RowObject); + if (!map.ContainsKey(key)) + map[key] = new List(); + map[key].Add(olvi); + } + + // Sort the items within each group (unless specifically turned off) + OLVColumn sortColumn = parms.SortItemsByPrimaryColumn ? parms.ListView.GetColumn(0) : parms.PrimarySort; + if (sortColumn != null && parms.PrimarySortOrder != SortOrder.None) { + IComparer itemSorter = parms.ItemComparer ?? + new ColumnComparer(sortColumn, parms.PrimarySortOrder, parms.SecondarySort, parms.SecondarySortOrder); + foreach (object key in map.Keys) { + map[key].Sort(itemSorter); + } + } + + // Make a list of the required groups + List groups = new List(); + foreach (object key in map.Keys) { + string title = parms.GroupByColumn.ConvertGroupKeyToTitle(key); + if (!String.IsNullOrEmpty(parms.TitleFormat)) { + int count = map[key].Count; + string format = (count == 1 ? parms.TitleSingularFormat : parms.TitleFormat); + try { + title = String.Format(format, title, count); + } catch (FormatException) { + title = "Invalid group format: " + format; + } + } + + OLVGroup lvg = new OLVGroup(title); + lvg.Collapsible = this.HasCollapsibleGroups; + lvg.Key = key; + lvg.SortValue = key as IComparable; + lvg.Items = map[key]; + if (parms.GroupByColumn.GroupFormatter != null) + parms.GroupByColumn.GroupFormatter(lvg, parms); + groups.Add(lvg); + } + + // Sort the groups + if (parms.GroupByOrder != SortOrder.None) + groups.Sort(parms.GroupComparer ?? new OLVGroupComparer(parms.GroupByOrder)); + + return groups; + } + + /// + /// Build/rebuild all the list view items in the list, preserving as much state as is possible + /// + public virtual void BuildList() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.BuildList)); + else + this.BuildList(true); + } + + /// + /// Build/rebuild all the list view items in the list + /// + /// If this is true, the control will try to preserve the selection, + /// focused item, and the scroll position (see Remarks) + /// + /// + /// + /// Use this method in situations were the contents of the list is basically the same + /// as previously. + /// + /// + public virtual void BuildList(bool shouldPreserveState) { + if (this.Frozen) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + this.ApplyExtendedStyles(); + this.ClearHotItem(); + int previousTopIndex = this.TopItemIndex; + Point currentScrollPosition = this.LowLevelScrollPosition; + + IList previousSelection = new ArrayList(); + Object previousFocus = null; + if (shouldPreserveState && this.objects != null) { + previousSelection = this.SelectedObjects; + OLVListItem focusedItem = this.FocusedItem as OLVListItem; + if (focusedItem != null) + previousFocus = focusedItem.RowObject; + } + + IEnumerable objectsToDisplay = this.FilteredObjects; + + this.BeginUpdate(); + try { + this.Items.Clear(); + this.ListViewItemSorter = null; + + if (objectsToDisplay != null) { + // Build a list of all our items and then display them. (Building + // a list and then doing one AddRange is about 10-15% faster than individual adds) + List itemList = new List(); // use ListViewItem to avoid co-variant conversion + foreach (object rowObject in objectsToDisplay) { + OLVListItem lvi = new OLVListItem(rowObject); + this.FillInValues(lvi, rowObject); + itemList.Add(lvi); + } + this.Items.AddRange(itemList.ToArray()); + this.Sort(); + + if (shouldPreserveState) { + this.SelectedObjects = previousSelection; + this.FocusedItem = this.ModelToItem(previousFocus); + } + + this.RefreshHotItem(); + } + } finally { + this.EndUpdate(); + } + + // We can only restore the scroll position after the EndUpdate() because + // of caching that the ListView does internally during a BeginUpdate/EndUpdate pair. + if (shouldPreserveState) { + this.RefreshHotItem(); + + // Restore the scroll position. TopItemIndex is best, but doesn't work + // when the control is grouped. + if (this.ShowGroups) + this.LowLevelScroll(currentScrollPosition.X, currentScrollPosition.Y); + else + this.TopItemIndex = previousTopIndex; + } + + System.Diagnostics.Debug.WriteLine(String.Format("PERF - Building list for {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + + /// + /// Clear any cached info this list may have been using + /// + public virtual void ClearCachedInfo() + { + // ObjectListView doesn't currently cache information but subclass do (or might) + } + + /// + /// Apply all required extended styles to our control. + /// + /// + /// + /// Whenever .NET code sets an extended style, it erases all other extended styles + /// that it doesn't use. So, we have to explicit reapply the styles that we have + /// added. + /// + /// + /// Normally, we would override CreateParms property and update + /// the ExStyle member, but ListView seems to ignore all ExStyles that + /// it doesn't already know about. Worse, when we set the LVS_EX_HEADERINALLVIEWS + /// value, bad things happen (the control crashes!). + /// + /// + protected virtual void ApplyExtendedStyles() { + const int LVS_EX_SUBITEMIMAGES = 0x00000002; + //const int LVS_EX_TRANSPARENTBKGND = 0x00400000; + const int LVS_EX_HEADERINALLVIEWS = 0x02000000; + + const int STYLE_MASK = LVS_EX_SUBITEMIMAGES | LVS_EX_HEADERINALLVIEWS; + int style = 0; + + if (this.ShowImagesOnSubItems && !this.VirtualMode) + style ^= LVS_EX_SUBITEMIMAGES; + + if (this.ShowHeaderInAllViews) + style ^= LVS_EX_HEADERINALLVIEWS; + + NativeMethods.SetExtendedStyle(this, style, STYLE_MASK); + } + + /// + /// Give the listview a reasonable size of its tiles, based on the number of lines of + /// information that each tile is going to display. + /// + public virtual void CalculateReasonableTileSize() { + if (this.Columns.Count <= 0) + return; + + List columns = this.AllColumns.FindAll(delegate(OLVColumn x) { + return (x.Index == 0) || x.IsTileViewColumn; + }); + + int imageHeight = (this.LargeImageList == null ? 16 : this.LargeImageList.ImageSize.Height); + int dataHeight = (this.Font.Height + 1) * columns.Count; + int tileWidth = (this.TileSize.Width == 0 ? 200 : this.TileSize.Width); + int tileHeight = Math.Max(this.TileSize.Height, Math.Max(imageHeight, dataHeight)); + this.TileSize = new Size(tileWidth, tileHeight); + } + + /// + /// Rebuild this list for the given view + /// + /// + public virtual void ChangeToFilteredColumns(View view) { + // Store the state + this.SuspendSelectionEvents(); + IList previousSelection = this.SelectedObjects; + int previousTopIndex = this.TopItemIndex; + + this.Freeze(); + this.Clear(); + List columns = this.GetFilteredColumns(view); + if (view == View.Details || this.ShowHeaderInAllViews) { + // Make sure all columns have a reasonable LastDisplayIndex + for (int index = 0; index < columns.Count; index++) + { + if (columns[index].LastDisplayIndex == -1) + columns[index].LastDisplayIndex = index; + } + // ListView will ignore DisplayIndex FOR ALL COLUMNS if there are any errors, + // e.g. duplicates (two columns with the same DisplayIndex) or gaps. + // LastDisplayIndex isn't guaranteed to be unique, so we just sort the columns by + // the last position they were displayed and use that to generate a sequence + // we can use for the DisplayIndex values. + List columnsInDisplayOrder = new List(columns); + columnsInDisplayOrder.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); + int i = 0; + foreach (OLVColumn col in columnsInDisplayOrder) + col.DisplayIndex = i++; + } + +// ReSharper disable once CoVariantArrayConversion + this.Columns.AddRange(columns.ToArray()); + if (view == View.Details || this.ShowHeaderInAllViews) + this.ShowSortIndicator(); + this.UpdateFiltering(); + this.Unfreeze(); + + // Restore the state + this.SelectedObjects = previousSelection; + this.TopItemIndex = previousTopIndex; + this.ResumeSelectionEvents(); + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public virtual void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else + this.SetObjects(null); + } + + /// + /// Reset the memory of which URLs have been visited + /// + public virtual void ClearUrlVisited() { + this.visitedUrlMap = new Dictionary(); + } + + /// + /// Copy a text and html representation of the selected rows onto the clipboard. + /// + /// Be careful when using this with virtual lists. If the user has selected + /// 10,000,000 rows, this method will faithfully try to copy all of them to the clipboard. + /// From the user's point of view, your program will appear to have hung. + public virtual void CopySelectionToClipboard() { + IList selection = this.SelectedObjects; + if (selection.Count == 0) + return; + + // Use the DragSource object to create the data object, if so configured. + // This relies on the assumption that DragSource will handle the selected objects only. + // It is legal for StartDrag to return null. + object data = null; + if (this.CopySelectionOnControlCUsesDragSource && this.DragSource != null) + data = this.DragSource.StartDrag(this, MouseButtons.Left, this.ModelToItem(selection[0])); + + Clipboard.SetDataObject(data ?? new OLVDataObject(this, selection)); + } + + /// + /// Copy a text and html representation of the given objects onto the clipboard. + /// + public virtual void CopyObjectsToClipboard(IList objectsToCopy) { + if (objectsToCopy.Count == 0) + return; + + // We don't know where these objects came from, so we can't use the DragSource to create + // the data object, like we do with CopySelectionToClipboard() above. + OLVDataObject dataObject = new OLVDataObject(this, objectsToCopy); + dataObject.CreateTextFormats(); + Clipboard.SetDataObject(dataObject); + } + + /// + /// Return a html representation of the given objects + /// + public virtual string ObjectsToHtml(IList objectsToConvert) { + if (objectsToConvert.Count == 0) + return String.Empty; + + OLVExporter exporter = new OLVExporter(this, objectsToConvert); + return exporter.ExportTo(OLVExporter.ExportFormat.HTML); + } + + /// + /// Deselect all rows in the listview + /// + public virtual void DeselectAll() { + NativeMethods.DeselectAllItems(this); + } + + /// + /// Setup the list so it will draw selected rows using custom colours. + /// + /// + /// This method makes the list owner drawn, and ensures that all columns have at + /// least a BaseRender installed. + /// + public virtual void EnableCustomSelectionColors() { + this.UseCustomSelectionColors = true; + } + + /// + /// Return the ListViewItem that appears immediately after the given item. + /// If the given item is null, the first item in the list will be returned. + /// Return null if the given item is the last item. + /// + /// The item that is before the item that is returned, or null + /// A ListViewItem + public virtual OLVListItem GetNextItem(OLVListItem itemToFind) { + if (this.ShowGroups) { + bool isFound = (itemToFind == null); + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem olvi in group.Items) { + if (isFound) + return olvi; + isFound = (itemToFind == olvi); + } + } + return null; + } + if (this.GetItemCount() == 0) + return null; + if (itemToFind == null) + return this.GetItem(0); + if (itemToFind.Index == this.GetItemCount() - 1) + return null; + return this.GetItem(itemToFind.Index + 1); + } + + /// + /// Return the last item in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + public virtual OLVListItem GetLastItemInDisplayOrder() { + if (!this.ShowGroups) + return this.GetItem(this.GetItemCount() - 1); + + if (this.Groups.Count > 0) { + ListViewGroup lastGroup = this.Groups[this.Groups.Count - 1]; + if (lastGroup.Items.Count > 0) + return (OLVListItem)lastGroup.Items[lastGroup.Items.Count - 1]; + } + + return null; + } + + /// + /// Return the n'th item (0-based) in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public virtual OLVListItem GetNthItemInDisplayOrder(int n) { + if (!this.ShowGroups || this.Groups.Count == 0) + return this.GetItem(n); + + foreach (ListViewGroup group in this.Groups) { + if (n < group.Items.Count) + return (OLVListItem)group.Items[n]; + + n -= group.Items.Count; + } + + return null; + } + + /// + /// Return the display index of the given listviewitem index. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public virtual int GetDisplayOrderOfItemIndex(int itemIndex) { + if (!this.ShowGroups || this.Groups.Count == 0) + return itemIndex; + + // TODO: This could be optimized + int i = 0; + foreach (ListViewGroup lvg in this.Groups) { + foreach (ListViewItem lvi in lvg.Items) { + if (lvi.Index == itemIndex) + return i; + i++; + } + } + + return -1; + } + + /// + /// Return the ListViewItem that appears immediately before the given item. + /// If the given item is null, the last item in the list will be returned. + /// Return null if the given item is the first item. + /// + /// The item that is before the item that is returned + /// A ListViewItem + public virtual OLVListItem GetPreviousItem(OLVListItem itemToFind) { + if (this.ShowGroups) { + OLVListItem previousItem = null; + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem lvi in group.Items) { + if (lvi == itemToFind) + return previousItem; + + previousItem = lvi; + } + } + return itemToFind == null ? previousItem : null; + } + if (this.GetItemCount() == 0) + return null; + if (itemToFind == null) + return this.GetItem(this.GetItemCount() - 1); + if (itemToFind.Index == 0) + return null; + return this.GetItem(itemToFind.Index - 1); + } + + /// + /// Insert the given collection of objects before the given position + /// + /// Where to insert the objects + /// The objects to be inserted + /// + /// + /// This operation only makes sense of non-sorted, non-grouped + /// lists, since any subsequent sort/group operation will rearrange + /// the list. + /// + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void InsertObjects(int index, ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { + this.InsertObjects(index, modelObjects); + }); + return; + } + if (modelObjects == null) + return; + + this.BeginUpdate(); + try { + // Give the world a chance to cancel or change the added objects + ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); + this.OnItemsAdding(args); + if (args.Canceled) + return; + modelObjects = args.ObjectsToAdd; + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + + // If we are filtering the list, there is no way to efficiently + // insert the objects, so just put them into our collection and rebuild. + if (this.IsFiltering) { + index = Math.Max(0, Math.Min(index, ourObjects.Count)); + ourObjects.InsertRange(index, modelObjects); + this.BuildList(true); + } else { + this.ListViewItemSorter = null; + index = Math.Max(0, Math.Min(index, this.GetItemCount())); + int i = index; + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + ourObjects.Insert(i, modelObject); + OLVListItem lvi = new OLVListItem(modelObject); + this.FillInValues(lvi, modelObject); + this.Items.Insert(i, lvi); + i++; + } + } + + for (i = index; i < this.GetItemCount(); i++) { + OLVListItem lvi = this.GetItem(i); + this.SetSubItemImages(lvi.Index, lvi); + } + + this.PostProcessRows(); + } + + // Tell the world that the list has changed + this.SubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } finally { + this.EndUpdate(); + } + } + + /// + /// Return true if the row representing the given model is selected + /// + /// The model object to look for + /// Is the row selected + public bool IsSelected(object model) { + OLVListItem item = this.ModelToItem(model); + return item != null && item.Selected; + } + + /// + /// Has the given URL been visited? + /// + /// The string to be consider + /// Has it been visited + public virtual bool IsUrlVisited(string url) { + return this.visitedUrlMap.ContainsKey(url); + } + + /// + /// Scroll the ListView by the given deltas. + /// + /// Horizontal delta + /// Vertical delta + public void LowLevelScroll(int dx, int dy) { + NativeMethods.Scroll(this, dx, dy); + } + + /// + /// Return a point that represents the current horizontal and vertical scroll positions + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Point LowLevelScrollPosition + { + get { + return new Point(NativeMethods.GetScrollPosition(this, true), NativeMethods.GetScrollPosition(this, false)); + } + } + + /// + /// Remember that the given URL has been visited + /// + /// The url to be remembered + /// This does not cause the control be redrawn + public virtual void MarkUrlVisited(string url) { + this.visitedUrlMap[url] = true; + } + + /// + /// Move the given collection of objects to the given index. + /// + /// This operation only makes sense on non-grouped ObjectListViews. + /// + /// + public virtual void MoveObjects(int index, ICollection modelObjects) { + + // We are going to remove all the given objects from our list + // and then insert them at the given location + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + + List indicesToRemove = new List(); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + int i = this.IndexOf(modelObject); + if (i >= 0) { + indicesToRemove.Add(i); + ourObjects.Remove(modelObject); + if (i <= index) + index--; + } + } + } + + // Remove the objects in reverse order so earlier + // deletes don't change the index of later ones + indicesToRemove.Sort(); + indicesToRemove.Reverse(); + try { + this.BeginUpdate(); + foreach (int i in indicesToRemove) { + this.Items.RemoveAt(i); + } + this.InsertObjects(index, modelObjects); + } finally { + this.EndUpdate(); + } + } + + /// + /// Calculate what item is under the given point? + /// + /// + /// + /// + new public ListViewHitTestInfo HitTest(int x, int y) { + // Everything costs something. Playing with the layout of the header can cause problems + // with the hit testing. If the header shrinks, the underlying control can throw a tantrum. + try { + return base.HitTest(x, y); + } catch (ArgumentOutOfRangeException) { + return new ListViewHitTestInfo(null, null, ListViewHitTestLocations.None); + } + } + + /// + /// Perform a hit test using the Windows control's SUBITEMHITTEST message. + /// This provides information about group hits that the standard ListView.HitTest() does not. + /// + /// + /// + /// + protected OlvListViewHitTestInfo LowLevelHitTest(int x, int y) { + + // If it's not even in the control, don't bother with anything else + if (!this.ClientRectangle.Contains(x, y)) + return new OlvListViewHitTestInfo(null, null, 0, null, 0); + + // Is the point over the header? + OlvListViewHitTestInfo.HeaderHitTestInfo headerHitTestInfo = this.HeaderControl.HitTest(x, y); + if (headerHitTestInfo != null) + return new OlvListViewHitTestInfo(this, headerHitTestInfo.ColumnIndex, headerHitTestInfo.IsOverCheckBox, headerHitTestInfo.OverDividerIndex); + + // Call the native hit test method, which is a little confusing. + NativeMethods.LVHITTESTINFO lParam = new NativeMethods.LVHITTESTINFO(); + lParam.pt_x = x; + lParam.pt_y = y; + int index = NativeMethods.HitTest(this, ref lParam); + + // Setup the various values we need to make our hit test structure + bool isGroupHit = (lParam.flags & (int)HitTestLocationEx.LVHT_EX_GROUP) != 0; + OLVListItem hitItem = isGroupHit || index == -1 ? null : this.GetItem(index); + OLVListSubItem subItem = (this.View == View.Details && hitItem != null) ? hitItem.GetSubItem(lParam.iSubItem) : null; + + // Figure out which group is involved in the hit test. This is a little complicated: + // If the list is virtual: + // - the returned value is list view item index + // - iGroup is the *index* of the hit group. + // If the list is not virtual: + // - iGroup is always -1. + // - if the point is over a group, the returned value is the *id* of the hit group. + // - if the point is not over a group, the returned value is list view item index. + OLVGroup group = null; + if (this.ShowGroups && this.OLVGroups != null) { + if (this.VirtualMode) { + group = lParam.iGroup >= 0 && lParam.iGroup < this.OLVGroups.Count ? this.OLVGroups[lParam.iGroup] : null; + } else { + if (isGroupHit) { + foreach (OLVGroup olvGroup in this.OLVGroups) { + if (olvGroup.GroupId == index) { + group = olvGroup; + break; + } + } + } + } + } + OlvListViewHitTestInfo olvListViewHitTest = new OlvListViewHitTestInfo(hitItem, subItem, lParam.flags, group, lParam.iSubItem); + // System.Diagnostics.Debug.WriteLine(String.Format("HitTest({0}, {1})=>{2}", x, y, olvListViewHitTest)); + return olvListViewHitTest; + } + + /// + /// What is under the given point? This takes the various parts of a cell into accout, including + /// any custom parts that a custom renderer might use + /// + /// + /// + /// An information block about what is under the point + public virtual OlvListViewHitTestInfo OlvHitTest(int x, int y) { + OlvListViewHitTestInfo hti = this.LowLevelHitTest(x, y); + + // There is a bug/"feature" of the ListView concerning hit testing. + // If FullRowSelect is false and the point is over cell 0 but not on + // the text or icon, HitTest will not register a hit. We could turn + // FullRowSelect on, do the HitTest, and then turn it off again, but + // toggling FullRowSelect in that way messes up the tooltip in the + // underlying control. So we have to find another way. + // + // It's too hard to try to write the hit test from scratch. Grouping (for + // example) makes it just too complicated. So, we have to use HitTest + // but try to get around its limits. + // + // First step is to determine if the point was within column 0. + // If it was, then we only have to determine if there is an actual row + // under the point. If there is, then we know that the point is over cell 0. + // So we try a Battleship-style approach: is there a subcell to the right + // of cell 0? This will return a false negative if column 0 is the rightmost column, + // so we also check for a subcell to the left. But if only column 0 is visible, + // then that will fail too, so we check for something at the very left of the + // control. + // + // This will still fail under pathological conditions. If column 0 fills + // the whole listview and no part of the text column 0 is visible + // (because it is horizontally scrolled offscreen), then the hit test will fail. + + // Are we in the buggy context? Details view, not full row select, and + // failing to find anything + if (hti.Item == null && !this.FullRowSelect && this.View == View.Details) { + // Is the point within the column 0? If it is, maybe it should have been a hit. + // Let's test slightly to the right and then to left of column 0. Hopefully one + // of those will hit a subitem + Point sides = NativeMethods.GetScrolledColumnSides(this, 0); + if (x >= sides.X && x <= sides.Y) { + // We look for: + // - any subitem to the right of cell 0? + // - any subitem to the left of cell 0? + // - cell 0 at the left edge of the screen + hti = this.LowLevelHitTest(sides.Y + 4, y); + if (hti.Item == null) + hti = this.LowLevelHitTest(sides.X - 4, y); + if (hti.Item == null) + hti = this.LowLevelHitTest(4, y); + + if (hti.Item != null) + { + // We hit something! So, the original point must have been in cell 0 + hti.ColumnIndex = 0; + hti.SubItem = hti.Item.GetSubItem(0); + hti.Location = ListViewHitTestLocations.None; + hti.HitTestLocation = HitTestLocation.InCell; + } + } + } + + if (this.OwnerDraw) + this.CalculateOwnerDrawnHitTest(hti, x, y); + else + this.CalculateStandardHitTest(hti, x, y); + + return hti; + } + + /// + /// Perform a hit test when the control is not owner drawn + /// + /// + /// + /// + protected virtual void CalculateStandardHitTest(OlvListViewHitTestInfo hti, int x, int y) { + + // Standard hit test works fine for the primary column + if (this.View != View.Details || hti.ColumnIndex == 0 || + hti.SubItem == null || hti.Column == null) + return; + + Rectangle cellBounds = hti.SubItem.Bounds; + bool hasImage = (this.GetActualImageIndex(hti.SubItem.ImageSelector) != -1); + + // Unless we say otherwise, it was an general incell hit + hti.HitTestLocation = HitTestLocation.InCell; + + // Check if the point is over where an image should be. + // If there is a checkbox or image there, tag it and exit. + Rectangle r = cellBounds; + r.Width = this.SmallImageSize.Width; + if (r.Contains(x, y)) { + if (hti.Column.CheckBoxes) { + hti.HitTestLocation = HitTestLocation.CheckBox; + return; + } + if (hasImage) { + hti.HitTestLocation = HitTestLocation.Image; + return; + } + } + + // Figure out where the text actually is and if the point is in it + // The standard HitTest assumes that any point inside a subitem is + // a hit on Text -- which is clearly not true. + Rectangle textBounds = cellBounds; + textBounds.X += 4; + if (hasImage) + textBounds.X += this.SmallImageSize.Width; + + Size proposedSize = new Size(textBounds.Width, textBounds.Height); + Size textSize = TextRenderer.MeasureText(hti.SubItem.Text, this.Font, proposedSize, TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine | TextFormatFlags.NoPrefix); + textBounds.Width = textSize.Width; + + switch (hti.Column.TextAlign) { + case HorizontalAlignment.Center: + textBounds.X += (cellBounds.Right - cellBounds.Left - textSize.Width) / 2; + break; + case HorizontalAlignment.Right: + textBounds.X = cellBounds.Right - textSize.Width; + break; + } + if (textBounds.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.Text; + } + } + + /// + /// Perform a hit test when the control is owner drawn. This hands off responsibility + /// to the renderer. + /// + /// + /// + /// + protected virtual void CalculateOwnerDrawnHitTest(OlvListViewHitTestInfo hti, int x, int y) { + // If the click wasn't on an item, give up + if (hti.Item == null) + return; + + // If the list is showing column, but they clicked outside the columns, also give up + if (this.View == View.Details && hti.Column == null) + return; + + // Which renderer was responsible for drawing that point + IRenderer renderer = this.View == View.Details ? (hti.Column.Renderer ?? this.DefaultRenderer) : this.ItemRenderer; + + // We can't decide who was responsible. Give up + if (renderer == null) + return; + + // Ask the responsible renderer what is at that point + renderer.HitTest(hti, x, y); + } + + /// + /// Pause (or unpause) all animations in the list + /// + /// true to pause, false to unpause + public virtual void PauseAnimations(bool isPause) { + for (int i = 0; i < this.Columns.Count; i++) { + OLVColumn col = this.GetColumn(i); + ImageRenderer renderer = col.Renderer as ImageRenderer; + if (renderer != null) + renderer.Paused = isPause; + } + } + + /// + /// Rebuild the columns based upon its current view and column visibility settings + /// + public virtual void RebuildColumns() { + this.ChangeToFilteredColumns(this.View); + } + + /// + /// Remove the given model object from the ListView + /// + /// The model to be removed + /// See RemoveObjects() for more details + /// This method is thread-safe. + /// + public virtual void RemoveObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.RemoveObject(modelObject); }); + else + this.RemoveObjects(new object[] { modelObject }); + } + + /// + /// Remove all of the given objects from the control. + /// + /// Collection of objects to be removed + /// + /// Nulls and model objects that are not in the ListView are silently ignored. + /// This method is thread-safe. + /// + public virtual void RemoveObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.RemoveObjects(modelObjects); }); + return; + } + if (modelObjects == null) + return; + + this.BeginUpdate(); + try { + // Give the world a chance to cancel or change the added objects + ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); + this.OnItemsRemoving(args); + if (args.Canceled) + return; + modelObjects = args.ObjectsToRemove; + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { +// ReSharper disable PossibleMultipleEnumeration + int i = ourObjects.IndexOf(modelObject); + if (i >= 0) + ourObjects.RemoveAt(i); +// ReSharper restore PossibleMultipleEnumeration + i = this.IndexOf(modelObject); + if (i >= 0) + this.Items.RemoveAt(i); + } + } + this.PostProcessRows(); + + // Tell the world that the list has changed + this.UnsubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } finally { + this.EndUpdate(); + } + } + + /// + /// Select all rows in the listview + /// + public virtual void SelectAll() { + NativeMethods.SelectAllItems(this); + } + + /// + /// Set the given image to be fixed in the bottom right of the list view. + /// This image will not scroll when the list view scrolls. + /// + /// + /// + /// This method uses ListView's native ability to display a background image. + /// It has a few limitations: + /// + /// + /// It doesn't work well with owner drawn mode. In owner drawn mode, each cell draws itself, + /// including its background, which covers the background image. + /// It doesn't look very good when grid lines are enabled, since the grid lines are drawn over the image. + /// It does not work at all on XP. + /// Obviously, it doesn't look good when alternate row background colors are enabled. + /// + /// + /// If you can live with these limitations, native watermarks are quite neat. They are true backgrounds, not + /// translucent overlays like the OverlayImage uses. They also have the decided advantage over overlays in that + /// they work correctly even in MDI applications. + /// + /// Setting this clears any background image. + /// + /// The image to be drawn. If null, any existing image will be removed. + public void SetNativeBackgroundWatermark(Image image) { + NativeMethods.SetBackgroundImage(this, image, true, false, 0, 0); + } + + /// + /// Set the given image to be background of the ListView so that it appears at the given + /// percentage offsets within the list. + /// + /// + /// This has the same limitations as described in . Make sure those limitations + /// are understood before using the method. + /// This is very similar to setting the property of the standard .NET ListView, except that the standard + /// BackgroundImage does not handle images with transparent areas properly -- it renders transparent areas as black. This + /// method does not have that problem. + /// Setting this clears any background watermark. + /// + /// The image to be drawn. If null, any existing image will be removed. + /// The horizontal percentage where the image will be placed. 0 is absolute left, 100 is absolute right. + /// The vertical percentage where the image will be placed. + public void SetNativeBackgroundImage(Image image, int xOffset, int yOffset) { + NativeMethods.SetBackgroundImage(this, image, false, false, xOffset, yOffset); + } + + /// + /// Set the given image to be the tiled background of the ListView. + /// + /// + /// This has the same limitations as described in and . + /// Make sure those limitations + /// are understood before using the method. + /// + /// The image to be drawn. If null, any existing image will be removed. + public void SetNativeBackgroundTiledImage(Image image) { + NativeMethods.SetBackgroundImage(this, image, false, true, 0, 0); + } + + /// + /// Set the collection of objects that will be shown in this list view. + /// + /// This method can safely be called from background threads. + /// The list is updated immediately + /// The objects to be displayed + public virtual void SetObjects(IEnumerable collection) { + this.SetObjects(collection, false); + } + + /// + /// Set the collection of objects that will be shown in this list view. + /// + /// This method can safely be called from background threads. + /// The list is updated immediately + /// The objects to be displayed + /// Should the state of the list be preserved as far as is possible. + public virtual void SetObjects(IEnumerable collection, bool preserveState) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); + return; + } + + // Give the world a chance to cancel or change the assigned collection + ItemsChangingEventArgs args = new ItemsChangingEventArgs(this.objects, collection); + this.OnItemsChanging(args); + if (args.Canceled) + return; + collection = args.NewObjects; + + // If we own the current list and they change to another list, we don't own it anymore + if (this.isOwnerOfObjects && !ReferenceEquals(this.objects, collection)) + this.isOwnerOfObjects = false; + this.objects = collection; + this.BuildList(preserveState); + + // Tell the world that the list has changed + this.UpdateNotificationSubscriptions(this.objects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } + + /// + /// Update the given model object into the ListView. The model will be added if it doesn't already exist. + /// + /// The model to be updated + /// + /// + /// See for more details + /// + /// This method is thread-safe. + /// This method will cause the list to be resorted. + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void UpdateObject(object modelObject) { + if (this.InvokeRequired) + this.Invoke((MethodInvoker)delegate() { this.UpdateObject(modelObject); }); + else + this.UpdateObjects(new object[] { modelObject }); + } + + /// + /// Update the pre-existing models that are equal to the given objects. If any of the model doesn't + /// already exist in the control, they will be added. + /// + /// Collection of objects to be updated/added + /// + /// This method will cause the list to be resorted. + /// Nulls are silently ignored. + /// This method is thread-safe. + /// This method only works on ObjectListViews and FastObjectListViews. + /// + public virtual void UpdateObjects(ICollection modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate() { this.UpdateObjects(modelObjects); }); + return; + } + if (modelObjects == null || modelObjects.Count == 0) + return; + + this.BeginUpdate(); + try { + this.UnsubscribeNotifications(modelObjects); + + ArrayList objectsToAdd = new ArrayList(); + + this.TakeOwnershipOfObjects(); + ArrayList ourObjects = ObjectListView.EnumerableToArray(this.Objects, false); + foreach (object modelObject in modelObjects) { + if (modelObject != null) { + int i = ourObjects.IndexOf(modelObject); + if (i < 0) + objectsToAdd.Add(modelObject); + else { + ourObjects[i] = modelObject; + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null) { + olvi.RowObject = modelObject; + this.RefreshItem(olvi); + } + } + } + } + this.PostProcessRows(); + + this.AddObjects(objectsToAdd); + + // Tell the world that the list has changed + this.SubscribeNotifications(modelObjects); + this.OnItemsChanged(new ItemsChangedEventArgs()); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Change any subscriptions to INotifyPropertyChanged events on our current + /// model objects so that we no longer listen for events on the old models + /// and do listen for events on the given collection. + /// + /// This does nothing if UseNotifyPropertyChanged is false. + /// + protected virtual void UpdateNotificationSubscriptions(IEnumerable collection) { + if (!this.UseNotifyPropertyChanged) + return; + + // We could calculate a symmetric difference between the old models and the new models + // except that we don't have the previous models at this point. + + this.UnsubscribeNotifications(null); + this.SubscribeNotifications(collection ?? this.Objects); + } + + /// + /// Gets or sets whether or not ObjectListView should subscribe to INotifyPropertyChanged + /// events on the model objects that it is given. + /// + /// + /// + /// This should be set before calling SetObjects(). If you set this to false, + /// ObjectListView will unsubscribe to all current model objects. + /// + /// If you set this to true on a virtual list, the ObjectListView will + /// walk all the objects in the list trying to subscribe to change notifications. + /// If you have 10,000,000 items in your virtual list, this may take some time. + /// + [Category("ObjectListView"), + Description("Should ObjectListView listen for property changed events on the model objects?"), + DefaultValue(false)] + public bool UseNotifyPropertyChanged { + get { return this.useNotifyPropertyChanged; } + set { + if (this.useNotifyPropertyChanged == value) + return; + this.useNotifyPropertyChanged = value; + if (value) + this.SubscribeNotifications(this.Objects); + else + this.UnsubscribeNotifications(null); + } + } + private bool useNotifyPropertyChanged; + + /// + /// Subscribe to INotifyPropertyChanges on the given collection of objects. + /// + /// + protected void SubscribeNotifications(IEnumerable models) { + if (!this.UseNotifyPropertyChanged || models == null) + return; + foreach (object x in models) { + INotifyPropertyChanged notifier = x as INotifyPropertyChanged; + if (notifier != null && !subscribedModels.ContainsKey(notifier)) { + notifier.PropertyChanged += HandleModelOnPropertyChanged; + subscribedModels[notifier] = notifier; + } + } + } + + /// + /// Unsubscribe from INotifyPropertyChanges on the given collection of objects. + /// If the given collection is null, unsubscribe from all current subscriptions + /// + /// + protected void UnsubscribeNotifications(IEnumerable models) { + if (models == null) { + foreach (INotifyPropertyChanged notifier in this.subscribedModels.Keys) { + notifier.PropertyChanged -= HandleModelOnPropertyChanged; + } + subscribedModels = new Hashtable(); + } else { + foreach (object x in models) { + INotifyPropertyChanged notifier = x as INotifyPropertyChanged; + if (notifier != null) { + notifier.PropertyChanged -= HandleModelOnPropertyChanged; + subscribedModels.Remove(notifier); + } + } + } + } + + private void HandleModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs) { + // System.Diagnostics.Debug.WriteLine(String.Format("PropertyChanged: '{0}' on '{1}", propertyChangedEventArgs.PropertyName, sender)); + this.RefreshObject(sender); + } + + private Hashtable subscribedModels = new Hashtable(); + + #endregion + + #region Save/Restore State + + /// + /// Return a byte array that represents the current state of the ObjectListView, such + /// that the state can be restored by RestoreState() + /// + /// + /// The state of an ObjectListView includes the attributes that the user can modify: + /// + /// current view (i.e. Details, Tile, Large Icon...) + /// sort column and direction + /// column order + /// column widths + /// column visibility + /// + /// + /// + /// It does not include selection or the scroll position. + /// + /// + /// A byte array representing the state of the ObjectListView + public virtual byte[] SaveState() { + ObjectListViewState olvState = new ObjectListViewState(); + olvState.VersionNumber = 1; + olvState.NumberOfColumns = this.AllColumns.Count; + olvState.CurrentView = this.View; + + // If we have a sort column, it is possible that it is not currently being shown, in which + // case, it's Index will be -1. So we calculate its index directly. Technically, the sort + // column does not even have to a member of AllColumns, in which case IndexOf will return -1, + // which is works fine since we have no way of restoring such a column anyway. + if (this.PrimarySortColumn != null) + olvState.SortColumn = this.AllColumns.IndexOf(this.PrimarySortColumn); + olvState.LastSortOrder = this.PrimarySortOrder; + olvState.IsShowingGroups = this.ShowGroups; + + if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) + this.RememberDisplayIndicies(); + + foreach (OLVColumn column in this.AllColumns) { + olvState.ColumnIsVisible.Add(column.IsVisible); + olvState.ColumnDisplayIndicies.Add(column.LastDisplayIndex); + olvState.ColumnWidths.Add(column.Width); + } + + // Now that we have stored our state, convert it to a byte array + using (MemoryStream ms = new MemoryStream()) { + BinaryFormatter serializer = new BinaryFormatter(); + serializer.AssemblyFormat = FormatterAssemblyStyle.Simple; + serializer.Serialize(ms, olvState); + return ms.ToArray(); + } + } + + /// + /// Restore the state of the control from the given string, which must have been + /// produced by SaveState() + /// + /// A byte array returned from SaveState() + /// Returns true if the state was restored + public virtual bool RestoreState(byte[] state) { + using (MemoryStream ms = new MemoryStream(state)) { + BinaryFormatter deserializer = new BinaryFormatter(); + ObjectListViewState olvState; + try { + olvState = deserializer.Deserialize(ms) as ObjectListViewState; + } catch (System.Runtime.Serialization.SerializationException) { + return false; + } + // The number of columns has changed. We have no way to match old + // columns to the new ones, so we just give up. + if (olvState == null || olvState.NumberOfColumns != this.AllColumns.Count) + return false; + if (olvState.SortColumn == -1) { + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + } else { + this.PrimarySortColumn = this.AllColumns[olvState.SortColumn]; + this.PrimarySortOrder = olvState.LastSortOrder; + } + for (int i = 0; i < olvState.NumberOfColumns; i++) { + OLVColumn column = this.AllColumns[i]; + column.Width = (int)olvState.ColumnWidths[i]; + column.IsVisible = (bool)olvState.ColumnIsVisible[i]; + column.LastDisplayIndex = (int)olvState.ColumnDisplayIndicies[i]; + } +// ReSharper disable RedundantCheckBeforeAssignment + if (olvState.IsShowingGroups != this.ShowGroups) +// ReSharper restore RedundantCheckBeforeAssignment + this.ShowGroups = olvState.IsShowingGroups; + if (this.View == olvState.CurrentView) + this.RebuildColumns(); + else + this.View = olvState.CurrentView; + } + + return true; + } + + /// + /// Instances of this class are used to store the state of an ObjectListView. + /// + [Serializable] + internal class ObjectListViewState + { +// ReSharper disable NotAccessedField.Global + public int VersionNumber = 1; +// ReSharper restore NotAccessedField.Global + public int NumberOfColumns = 1; + public View CurrentView; + public int SortColumn = -1; + public bool IsShowingGroups; + public SortOrder LastSortOrder = SortOrder.None; +// ReSharper disable FieldCanBeMadeReadOnly.Global + public ArrayList ColumnIsVisible = new ArrayList(); + public ArrayList ColumnDisplayIndicies = new ArrayList(); + public ArrayList ColumnWidths = new ArrayList(); +// ReSharper restore FieldCanBeMadeReadOnly.Global + } + + #endregion + + #region Event handlers + + /// + /// The application is idle. Trigger a SelectionChanged event. + /// + /// + /// + protected virtual void HandleApplicationIdle(object sender, EventArgs e) { + // Remove the handler before triggering the event + Application.Idle -= new EventHandler(HandleApplicationIdle); + this.hasIdleHandler = false; + + this.OnSelectionChanged(new EventArgs()); + } + + /// + /// The application is idle. Trigger a SelectionChanged event. + /// + /// + /// + protected virtual void HandleApplicationIdleResizeColumns(object sender, EventArgs e) { + // Remove the handler before triggering the event + Application.Idle -= new EventHandler(this.HandleApplicationIdleResizeColumns); + this.hasResizeColumnsHandler = false; + + this.ResizeFreeSpaceFillingColumns(); + } + + /// + /// Handle the BeginScroll listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleBeginScroll(ref Message m) { + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + + NativeMethods.NMLVSCROLL nmlvscroll = (NativeMethods.NMLVSCROLL)m.GetLParam(typeof(NativeMethods.NMLVSCROLL)); + if (nmlvscroll.dx != 0) { + int scrollPositionH = NativeMethods.GetScrollPosition(this, true); + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionH - nmlvscroll.dx, scrollPositionH, ScrollOrientation.HorizontalScroll); + this.OnScroll(args); + + // Force any empty list msg to redraw when the list is scrolled horizontally + if (this.GetItemCount() == 0) + this.Invalidate(); + } + if (nmlvscroll.dy != 0) { + int scrollPositionV = NativeMethods.GetScrollPosition(this, false); + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, scrollPositionV - nmlvscroll.dy, scrollPositionV, ScrollOrientation.VerticalScroll); + this.OnScroll(args); + } + + return false; + } + + /// + /// Handle the EndScroll listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleEndScroll(ref Message m) { + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + + // There is a bug in ListView under XP that causes the gridlines to be incorrectly scrolled + // when the left button is clicked to scroll. This is supposedly documented at + // KB 813791, but I couldn't find it anywhere. You can follow this thread to see the discussion + // http://www.ureader.com/msg/1484143.aspx + + if (!ObjectListView.IsVistaOrLater && Control.MouseButtons == MouseButtons.Left && this.GridLines) { + this.Invalidate(); + this.Update(); + } + + return false; + } + + /// + /// Handle the LinkClick listview notification + /// + /// + /// True if the event was completely handled + protected virtual bool HandleLinkClick(ref Message m) { + //System.Diagnostics.Debug.WriteLine("HandleLinkClick"); + + NativeMethods.NMLVLINK nmlvlink = (NativeMethods.NMLVLINK)m.GetLParam(typeof(NativeMethods.NMLVLINK)); + + // Find the group that was clicked and trigger an event + foreach (OLVGroup x in this.OLVGroups) { + if (x.GroupId == nmlvlink.iSubItem) { + this.OnGroupTaskClicked(new GroupTaskClickedEventArgs(x)); + return true; + } + } + + return false; + } + + /// + /// The cell tooltip control wants information about the tool tip that it should show. + /// + /// + /// + protected virtual void HandleCellToolTipShowing(object sender, ToolTipShowingEventArgs e) { + this.BuildCellEvent(e, this.PointToClient(Cursor.Position)); + if (e.Item != null) { + e.Text = this.GetCellToolTip(e.ColumnIndex, e.RowIndex); + this.OnCellToolTip(e); + } + } + + /// + /// Allow the HeaderControl to call back into HandleHeaderToolTipShowing without making that method public + /// + /// + /// + internal void HeaderToolTipShowingCallback(object sender, ToolTipShowingEventArgs e) { + this.HandleHeaderToolTipShowing(sender, e); + } + + /// + /// The header tooltip control wants information about the tool tip that it should show. + /// + /// + /// + protected virtual void HandleHeaderToolTipShowing(object sender, ToolTipShowingEventArgs e) { + e.ColumnIndex = this.HeaderControl.ColumnIndexUnderCursor; + if (e.ColumnIndex < 0) + return; + + e.RowIndex = -1; + e.Model = null; + e.Column = this.GetColumn(e.ColumnIndex); + e.Text = this.GetHeaderToolTip(e.ColumnIndex); + e.ListView = this; + this.OnHeaderToolTip(e); + } + + /// + /// Event handler for the column click event + /// + protected virtual void HandleColumnClick(object sender, ColumnClickEventArgs e) { + if (!this.PossibleFinishCellEditing()) + return; + + // Toggle the sorting direction on successive clicks on the same column + if (this.PrimarySortColumn != null && e.Column == this.PrimarySortColumn.Index) + this.PrimarySortOrder = (this.PrimarySortOrder == SortOrder.Descending ? SortOrder.Ascending : SortOrder.Descending); + else + this.PrimarySortOrder = SortOrder.Ascending; + + this.BeginUpdate(); + try { + this.Sort(e.Column); + } finally { + this.EndUpdate(); + } + } + + #endregion + + #region Low level Windows Message handling + + /// + /// Override the basic message pump for this control + /// + /// + protected override void WndProc(ref Message m) + { + + // System.Diagnostics.Debug.WriteLine(m.Msg); + switch (m.Msg) { + case 2: // WM_DESTROY + if (!this.HandleDestroy(ref m)) + base.WndProc(ref m); + break; + //case 0x14: // WM_ERASEBKGND + // Can't do anything here since, when the control is double buffered, anything + // done here is immediately over-drawn + // break; + case 0x0F: // WM_PAINT + if (!this.HandlePaint(ref m)) + base.WndProc(ref m); + break; + case 0x46: // WM_WINDOWPOSCHANGING + if (this.PossibleFinishCellEditing() && !this.HandleWindowPosChanging(ref m)) + base.WndProc(ref m); + break; + case 0x4E: // WM_NOTIFY + if (!this.HandleNotify(ref m)) + base.WndProc(ref m); + break; + case 0x0100: // WM_KEY_DOWN + if (!this.HandleKeyDown(ref m)) + base.WndProc(ref m); + break; + case 0x0102: // WM_CHAR + if (!this.HandleChar(ref m)) + base.WndProc(ref m); + break; + case 0x0200: // WM_MOUSEMOVE + if (!this.HandleMouseMove(ref m)) + base.WndProc(ref m); + break; + case 0x0201: // WM_LBUTTONDOWN + if (this.PossibleFinishCellEditing() && !this.HandleLButtonDown(ref m)) + base.WndProc(ref m); + break; + case 0x202: // WM_LBUTTONUP + if (this.PossibleFinishCellEditing() && !this.HandleLButtonUp(ref m)) + base.WndProc(ref m); + break; + case 0x0203: // WM_LBUTTONDBLCLK + if (this.PossibleFinishCellEditing() && !this.HandleLButtonDoubleClick(ref m)) + base.WndProc(ref m); + break; + case 0x0204: // WM_RBUTTONDOWN + if (this.PossibleFinishCellEditing() && !this.HandleRButtonDown(ref m)) + base.WndProc(ref m); + break; + case 0x0206: // WM_RBUTTONDBLCLK + if (this.PossibleFinishCellEditing() && !this.HandleRButtonDoubleClick(ref m)) + base.WndProc(ref m); + break; + case 0x204E: // WM_REFLECT_NOTIFY + if (!this.HandleReflectNotify(ref m)) + base.WndProc(ref m); + break; + case 0x114: // WM_HSCROLL: + case 0x115: // WM_VSCROLL: + //System.Diagnostics.Debug.WriteLine("WM_VSCROLL"); + if (this.PossibleFinishCellEditing()) + base.WndProc(ref m); + break; + case 0x20A: // WM_MOUSEWHEEL: + case 0x20E: // WM_MOUSEHWHEEL: + if (this.PossibleFinishCellEditing()) + base.WndProc(ref m); + break; + case 0x7B: // WM_CONTEXTMENU + if (!this.HandleContextMenu(ref m)) + base.WndProc(ref m); + break; + //case 0x1000 + 18: // LVM_HITTEST: + // if (!this.HandleHitTest(ref m)) + // base.WndProc(ref m); + // break; + default: + base.WndProc(ref m); + break; + } + } + + /// + /// Handle the search for item m if possible. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleChar(ref Message m) { + + // Trigger a normal KeyPress event, which listeners can handle if they want. + // Handling the event stops ObjectListView's fancy search-by-typing. + if (this.ProcessKeyEventArgs(ref m)) + return true; + + const int MILLISECONDS_BETWEEN_KEYPRESSES = 1000; + + // What character did the user type and was it part of a longer string? + char character = (char)m.WParam.ToInt32(); //TODO: Will this work on 64 bit or MBCS? + if (character == (char)Keys.Back) { + // Backspace forces the next key to be considered the start of a new search + this.timeLastCharEvent = 0; + return true; + } + + if (System.Environment.TickCount < (this.timeLastCharEvent + MILLISECONDS_BETWEEN_KEYPRESSES)) + this.lastSearchString += character; + else + this.lastSearchString = character.ToString(CultureInfo.InvariantCulture); + + // If this control is showing checkboxes, we want to ignore single space presses, + // since they are used to toggle the selected checkboxes. + if (this.CheckBoxes && this.lastSearchString == " ") { + this.timeLastCharEvent = 0; + return true; + } + + // Where should the search start? + int start = 0; + ListViewItem focused = this.FocusedItem; + if (focused != null) { + start = this.GetDisplayOrderOfItemIndex(focused.Index); + + // If the user presses a single key, we search from after the focused item, + // being careful not to march past the end of the list + if (this.lastSearchString.Length == 1) { + start += 1; + if (start == this.GetItemCount()) + start = 0; + } + } + + // Give the world a chance to fiddle with or completely avoid the searching process + BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(this.lastSearchString, start); + this.OnBeforeSearching(args); + if (args.Canceled) + return true; + + // The parameters of the search may have been changed + string searchString = args.StringToFind; + start = args.StartSearchFrom; + + // Do the actual search + int found = this.FindMatchingRow(searchString, start, SearchDirectionHint.Down); + if (found >= 0) { + // Select and focus on the found item + this.BeginUpdate(); + try { + this.SelectedIndices.Clear(); + OLVListItem lvi = this.GetNthItemInDisplayOrder(found); + if (lvi != null) { + if (lvi.Enabled) + lvi.Selected = true; + lvi.Focused = true; + this.EnsureVisible(lvi.Index); + } + } finally { + this.EndUpdate(); + } + } + + // Tell the world that a search has occurred + AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(searchString, found); + this.OnAfterSearching(args2); + if (!args2.Handled) { + if (found < 0) + System.Media.SystemSounds.Beep.Play(); + } + + // When did this event occur? + this.timeLastCharEvent = System.Environment.TickCount; + return true; + } + private int timeLastCharEvent; + private string lastSearchString; + + /// + /// The user wants to see the context menu. + /// + /// The windows m + /// A bool indicating if this m has been handled + /// + /// We want to ignore context menu requests that are triggered by right clicks on the header + /// + protected virtual bool HandleContextMenu(ref Message m) { + // Don't try to handle context menu commands at design time. + if (this.DesignMode) + return false; + + // If the context menu command was generated by the keyboard, LParam will be -1. + // We don't want to process these. + if (m.LParam == this.minusOne) + return false; + + // If the context menu came from somewhere other than the header control, + // we also don't want to ignore it + if (m.WParam != this.HeaderControl.Handle) + return false; + + // OK. Looks like a right click in the header + if (!this.PossibleFinishCellEditing()) + return true; + + int columnIndex = this.HeaderControl.ColumnIndexUnderCursor; + return this.HandleHeaderRightClick(columnIndex); + } + readonly IntPtr minusOne = new IntPtr(-1); + +#if NOT_USED + /// + /// Intercept the low-level HitTest message to try and trick it in regards to checkboxes + /// + /// + /// + protected virtual bool HandleHitTest(ref Message m) { + System.Diagnostics.Debug.WriteLine("HandleHitTest"); + + // The underlying control knows nothing about checkboxes on virtual lists. + // This throws off the hit testing. We can handle that for functionality that is + // triggered internally, but we need to intercept the hit test message so we + // can fool the underlying control when it does hit testing for itself. + + // Basic condition we are trying to fix: a virtual list with checkboxes. + // Anything else and we can just use the normal hit test + if (!this.VirtualMode || !this.CheckBoxes) + return false; + + // Do the normal hit test and see if we hit something + base.DefWndProc(ref m); + NativeMethods.LVHITTESTINFO lvhittestinfo = (NativeMethods.LVHITTESTINFO) m.GetLParam(typeof (NativeMethods.LVHITTESTINFO)); + + // If we did hit something, we don't need to do anything else + if (lvhittestinfo.iItem != -1) { + Debug.WriteLine(String.Format("Found item {0} normally", lvhittestinfo.iItem)); + return true; + } + // The hit test doesn't take the checkbox into account. So pretend the click happened + // slightly to the left (the width of the checkbox) + int originalX = lvhittestinfo.pt_x; + int checkBoxWidth = this.StateImageList.ImageSize.Width; + lvhittestinfo.pt_x -= checkBoxWidth; // TODO: Handle RTL + Marshal.StructureToPtr(lvhittestinfo, m.LParam, false); + base.DefWndProc(ref m); + + // Put back the original x co-ord so if anything was hit, it will appear + // to have been at the original location + lvhittestinfo = (NativeMethods.LVHITTESTINFO)m.GetLParam(typeof(NativeMethods.LVHITTESTINFO)); + System.Diagnostics.Debug.WriteLine(lvhittestinfo.iItem); + System.Diagnostics.Debug.WriteLine(lvhittestinfo.flags); + lvhittestinfo.pt_x = originalX; + Marshal.StructureToPtr(lvhittestinfo, m.LParam, false); + + return true; + } +#endif + /// + /// Handle the Custom draw series of notifications + /// + /// The message + /// True if the message has been handled + protected virtual bool HandleCustomDraw(ref Message m) { + const int CDDS_PREPAINT = 1; + const int CDDS_POSTPAINT = 2; + const int CDDS_PREERASE = 3; + const int CDDS_POSTERASE = 4; + //const int CDRF_NEWFONT = 2; + //const int CDRF_SKIPDEFAULT = 4; + const int CDDS_ITEM = 0x00010000; + const int CDDS_SUBITEM = 0x00020000; + const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT); + const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT); + const int CDDS_ITEMPREERASE = (CDDS_ITEM | CDDS_PREERASE); + const int CDDS_ITEMPOSTERASE = (CDDS_ITEM | CDDS_POSTERASE); + const int CDDS_SUBITEMPREPAINT = (CDDS_SUBITEM | CDDS_ITEMPREPAINT); + const int CDDS_SUBITEMPOSTPAINT = (CDDS_SUBITEM | CDDS_ITEMPOSTPAINT); + const int CDRF_NOTIFYPOSTPAINT = 0x10; + //const int CDRF_NOTIFYITEMDRAW = 0x20; + //const int CDRF_NOTIFYSUBITEMDRAW = 0x20; // same value as above! + const int CDRF_NOTIFYPOSTERASE = 0x40; + + // There is a bug in owner drawn virtual lists which causes lots of custom draw messages + // to be sent to the control *outside* of a WmPaint event. AFAIK, these custom draw events + // are spurious and only serve to make the control flicker annoyingly. + // So, we ignore messages that are outside of a paint event. + if (!this.isInWmPaintEvent) + return true; + + // One more complication! Sometimes with owner drawn virtual lists, the act of drawing + // the overlays triggers a second attempt to paint the control -- which makes an annoying + // flicker. So, we only do the custom drawing once per WmPaint event. + if (!this.shouldDoCustomDrawing) + return true; + + NativeMethods.NMLVCUSTOMDRAW nmcustomdraw = (NativeMethods.NMLVCUSTOMDRAW)m.GetLParam(typeof(NativeMethods.NMLVCUSTOMDRAW)); + //System.Diagnostics.Debug.WriteLine(String.Format("cd: {0:x}, {1}, {2}", nmcustomdraw.nmcd.dwDrawStage, nmcustomdraw.dwItemType, nmcustomdraw.nmcd.dwItemSpec)); + + // Ignore drawing of group items + if (nmcustomdraw.dwItemType == 1) { + // This is the basis of an idea about how to owner draw group headers + + //nmcustomdraw.clrText = ColorTranslator.ToWin32(Color.DeepPink); + //nmcustomdraw.clrFace = ColorTranslator.ToWin32(Color.DeepPink); + //nmcustomdraw.clrTextBk = ColorTranslator.ToWin32(Color.DeepPink); + //Marshal.StructureToPtr(nmcustomdraw, m.LParam, false); + //using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + // g.DrawRectangle(Pens.Red, Rectangle.FromLTRB(nmcustomdraw.rcText.left, nmcustomdraw.rcText.top, nmcustomdraw.rcText.right, nmcustomdraw.rcText.bottom)); + //} + //m.Result = (IntPtr)((int)m.Result | CDRF_SKIPDEFAULT); + return true; + } + + switch (nmcustomdraw.nmcd.dwDrawStage) { + case CDDS_PREPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_PREPAINT"); + // Remember which items were drawn during this paint cycle + if (this.prePaintLevel == 0) + this.drawnItems = new List(); + + // If there are any items, we have to wait until at least one has been painted + // before we draw the overlays. If there aren't any items, there will never be any + // item paint events, so we can draw the overlays whenever + this.isAfterItemPaint = (this.GetItemCount() == 0); + this.prePaintLevel++; + base.WndProc(ref m); + + // Make sure that we get postpaint notifications + m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); + return true; + + case CDDS_POSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_POSTPAINT"); + this.prePaintLevel--; + + // When in group view, we have two problems. On XP, the control sends + // a whole heap of PREPAINT/POSTPAINT messages before drawing any items. + // We have to wait until after the first item paint before we draw overlays. + // On Vista, we have a different problem. On Vista, the control nests calls + // to PREPAINT and POSTPAINT. We only want to draw overlays on the outermost + // POSTPAINT. + if (this.prePaintLevel == 0 && (this.isMarqueSelecting || this.isAfterItemPaint)) { + this.shouldDoCustomDrawing = false; + + // Draw our overlays after everything has been drawn + using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + this.DrawAllDecorations(g, this.drawnItems); + } + } + break; + + case CDDS_ITEMPREPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREPAINT"); + + // When in group view on XP, the control send a whole heap of PREPAINT/POSTPAINT + // messages before drawing any items. + // We have to wait until after the first item paint before we draw overlays + this.isAfterItemPaint = true; + + // This scheme of catching custom draw msgs works fine, except + // for Tile view. Something in .NET's handling of Tile view causes lots + // of invalidates and erases. So, we just ignore completely + // .NET's handling of Tile view and let the underlying control + // do its stuff. Strangely, if the Tile view is + // completely owner drawn, those erasures don't happen. + if (this.View == View.Tile) { + if (this.OwnerDraw && this.ItemRenderer != null) + base.WndProc(ref m); + } else { + base.WndProc(ref m); + } + + m.Result = (IntPtr)((int)m.Result | CDRF_NOTIFYPOSTPAINT | CDRF_NOTIFYPOSTERASE); + return true; + + case CDDS_ITEMPOSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTPAINT"); + // Remember which items have been drawn so we can draw any decorations for them + // once all other painting is finished + if (this.Columns.Count > 0) { + OLVListItem olvi = this.GetItem((int)nmcustomdraw.nmcd.dwItemSpec); + if (olvi != null) + this.drawnItems.Add(olvi); + } + break; + + case CDDS_SUBITEMPREPAINT: + //System.Diagnostics.Debug.WriteLine(String.Format("CDDS_SUBITEMPREPAINT ({0},{1})", (int)nmcustomdraw.nmcd.dwItemSpec, nmcustomdraw.iSubItem)); + + // There is a bug in the .NET framework which appears when column 0 of an owner drawn listview + // is dragged to another column position. + // The bounds calculation always returns the left edge of column 0 as being 0. + // The effects of this bug become apparent + // when the listview is scrolled horizontally: the control can think that column 0 + // is no longer visible (the horizontal scroll position is subtracted from the bounds, giving a + // rectangle that is offscreen). In those circumstances, column 0 is not redraw because + // the control thinks it is not visible and so does not trigger a DrawSubItem event. + + // To fix this problem, we have to detected the situation -- owner drawing column 0 in any column except 0 -- + // trigger our own DrawSubItem, and then prevent the default processing from occuring. + + // Are we owner drawing column 0 when it's in any column except 0? + if (!this.OwnerDraw) + return false; + + int columnIndex = nmcustomdraw.iSubItem; + if (columnIndex != 0) + return false; + + int displayIndex = this.Columns[0].DisplayIndex; + if (displayIndex == 0) + return false; + + int rowIndex = (int)nmcustomdraw.nmcd.dwItemSpec; + OLVListItem item = this.GetItem(rowIndex); + if (item == null) + return false; + + // OK. We have the error condition, so lets do what the .NET framework should do. + // Trigger an event to draw column 0 when it is not at display index 0 + using (Graphics g = Graphics.FromHdc(nmcustomdraw.nmcd.hdc)) { + + // Correctly calculate the bounds of cell 0 + Rectangle r = item.GetSubItemBounds(0); + + // We can hardcode "0" here since we know we are only doing this for column 0 + DrawListViewSubItemEventArgs args = new DrawListViewSubItemEventArgs(g, r, item, item.SubItems[0], rowIndex, 0, + this.Columns[0], (ListViewItemStates)nmcustomdraw.nmcd.uItemState); + this.OnDrawSubItem(args); + + // If the event handler wants to do the default processing (i.e. DrawDefault = true), we are stuck. + // There is no way we can force the default drawing because of the bug in .NET we are trying to get around. + System.Diagnostics.Trace.Assert(!args.DrawDefault, "Default drawing is impossible in this situation"); + } + m.Result = (IntPtr)4; + + return true; + + case CDDS_SUBITEMPOSTPAINT: + //System.Diagnostics.Debug.WriteLine("CDDS_SUBITEMPOSTPAINT"); + break; + + // I have included these stages, but it doesn't seem that they are sent for ListViews. + // http://www.tech-archive.net/Archive/VC/microsoft.public.vc.mfc/2006-08/msg00220.html + + case CDDS_PREERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_PREERASE"); + break; + + case CDDS_POSTERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_POSTERASE"); + break; + + case CDDS_ITEMPREERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPREERASE"); + break; + + case CDDS_ITEMPOSTERASE: + //System.Diagnostics.Debug.WriteLine("CDDS_ITEMPOSTERASE"); + break; + } + + return false; + } + bool isAfterItemPaint; + List drawnItems; + + /// + /// Handle the underlying control being destroyed + /// + /// + /// + protected virtual bool HandleDestroy(ref Message m) { + //System.Diagnostics.Debug.WriteLine("WM_DESTROY"); + + // Recreate the header control when the listview control is destroyed + this.BeginInvoke((MethodInvoker)delegate { + this.headerControl = null; + this.HeaderControl.WordWrap = this.HeaderWordWrap; + }); + + // When the underlying control is destroyed, we need to recreate + // and reconfigure its tooltip + if (this.cellToolTip == null) + return false; + + this.cellToolTip.PushSettings(); + base.WndProc(ref m); + this.BeginInvoke((MethodInvoker)delegate { + this.UpdateCellToolTipHandle(); + this.cellToolTip.PopSettings(); + }); + return true; + } + + /// + /// Handle the search for item m if possible. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleFindItem(ref Message m) { + // NOTE: As far as I can see, this message is never actually sent to the control, making this + // method redundant! + + const int LVFI_STRING = 0x0002; + + NativeMethods.LVFINDINFO findInfo = (NativeMethods.LVFINDINFO)m.GetLParam(typeof(NativeMethods.LVFINDINFO)); + + // We can only handle string searches + if ((findInfo.flags & LVFI_STRING) != LVFI_STRING) + return false; + + int start = m.WParam.ToInt32(); + m.Result = (IntPtr)this.FindMatchingRow(findInfo.psz, start, SearchDirectionHint.Down); + return true; + } + + /// + /// Find the first row after the given start in which the text value in the + /// comparison column begins with the given text. The comparison column is column 0, + /// unless IsSearchOnSortColumn is true, in which case the current sort column is used. + /// + /// The text to be prefix matched + /// The index of the first row to consider + /// Which direction should be searched? + /// The index of the first row that matched, or -1 + /// The text comparison is a case-insensitive, prefix match. The search will + /// search the every row until a match is found, wrapping at the end if needed. + public virtual int FindMatchingRow(string text, int start, SearchDirectionHint direction) { + // We also can't do anything if we don't have data + int rowCount = this.GetItemCount(); + if (rowCount == 0) + return -1; + + // Which column are we going to use for our comparing? + OLVColumn column = this.GetColumn(0); + if (this.IsSearchOnSortColumn && this.View == View.Details && this.PrimarySortColumn != null) + column = this.PrimarySortColumn; + + // Do two searches if necessary to find a match. The second search is the wrap-around part of searching + int i; + if (direction == SearchDirectionHint.Down) { + i = this.FindMatchInRange(text, start, rowCount - 1, column); + if (i == -1 && start > 0) + i = this.FindMatchInRange(text, 0, start - 1, column); + } else { + i = this.FindMatchInRange(text, start, 0, column); + if (i == -1 && start != rowCount) + i = this.FindMatchInRange(text, rowCount - 1, start + 1, column); + } + + return i; + } + + /// + /// Find the first row in the given range of rows that prefix matches the string value of the given column. + /// + /// + /// + /// + /// + /// The index of the matched row, or -1 + protected virtual int FindMatchInRange(string text, int first, int last, OLVColumn column) { + if (first <= last) { + for (int i = first; i <= last; i++) { + string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } else { + for (int i = first; i >= last; i--) { + string data = column.GetStringValue(this.GetNthItemInDisplayOrder(i).RowObject); + if (data.StartsWith(text, StringComparison.CurrentCultureIgnoreCase)) + return i; + } + } + + return -1; + } + + /// + /// Handle the Group Info series of notifications + /// + /// The message + /// True if the message has been handled + protected virtual bool HandleGroupInfo(ref Message m) + { + NativeMethods.NMLVGROUP nmlvgroup = (NativeMethods.NMLVGROUP)m.GetLParam(typeof(NativeMethods.NMLVGROUP)); + + //System.Diagnostics.Debug.WriteLine(String.Format("group: {0}, old state: {1}, new state: {2}", + // nmlvgroup.iGroupId, OLVGroup.StateToString(nmlvgroup.uOldState), OLVGroup.StateToString(nmlvgroup.uNewState))); + + // Ignore state changes that aren't related to selection, focus or collapsedness + const uint INTERESTING_STATES = (uint) (GroupState.LVGS_COLLAPSED | GroupState.LVGS_FOCUSED | GroupState.LVGS_SELECTED); + if ((nmlvgroup.uOldState & INTERESTING_STATES) == (nmlvgroup.uNewState & INTERESTING_STATES)) + return false; + + foreach (OLVGroup group in this.OLVGroups) { + if (group.GroupId == nmlvgroup.iGroupId) { + GroupStateChangedEventArgs args = new GroupStateChangedEventArgs(group, (GroupState)nmlvgroup.uOldState, (GroupState)nmlvgroup.uNewState); + this.OnGroupStateChanged(args); + break; + } + } + + return false; + } + + //private static string StateToString(uint state) + //{ + // if (state == 0) + // return Enum.GetName(typeof(GroupState), 0); + + // List names = new List(); + // foreach (int value in Enum.GetValues(typeof(GroupState))) + // { + // if (value != 0 && (state & value) == value) + // { + // names.Add(Enum.GetName(typeof(GroupState), value)); + // } + // } + // return names.Count == 0 ? state.ToString("x") : String.Join("|", names.ToArray()); + //} + + /// + /// Handle a key down message + /// + /// + /// True if the msg has been handled + protected virtual bool HandleKeyDown(ref Message m) { + + // If this is a checkbox list, toggle the selected rows when the user presses Space + if (this.CheckBoxes && m.WParam.ToInt32() == (int)Keys.Space && this.SelectedIndices.Count > 0) { + this.ToggleSelectedRowCheckBoxes(); + return true; + } + + // Remember the scroll position so we can decide if the listview has scrolled in the + // handling of the event. + int scrollPositionH = NativeMethods.GetScrollPosition(this, true); + int scrollPositionV = NativeMethods.GetScrollPosition(this, false); + + base.WndProc(ref m); + + // It's possible that the processing in base.WndProc has actually destroyed this control + if (this.IsDisposed) + return true; + + // If the keydown processing changed the scroll position, trigger a Scroll event + int newScrollPositionH = NativeMethods.GetScrollPosition(this, true); + int newScrollPositionV = NativeMethods.GetScrollPosition(this, false); + + if (scrollPositionH != newScrollPositionH) { + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, + scrollPositionH, newScrollPositionH, ScrollOrientation.HorizontalScroll); + this.OnScroll(args); + this.RefreshHotItem(); + } + if (scrollPositionV != newScrollPositionV) { + ScrollEventArgs args = new ScrollEventArgs(ScrollEventType.EndScroll, + scrollPositionV, newScrollPositionV, ScrollOrientation.VerticalScroll); + this.OnScroll(args); + this.RefreshHotItem(); + } + + return true; + } + + private void ToggleSelectedRowCheckBoxes() { + // This doesn't actually toggle all rows. It toggles the first row, and + // all other rows get the check state of that first row. + Object primaryModel = this.GetItem(this.SelectedIndices[0]).RowObject; + this.ToggleCheckObject(primaryModel); + CheckState? state = this.GetCheckState(primaryModel); + if (state.HasValue) { + foreach (Object x in this.SelectedObjects) + this.SetObjectCheckedness(x, state.Value); + } + } + + /// + /// Catch the Left Button down event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonDown(ref Message m) { + // We have to intercept this low level message rather than the more natural + // overridding of OnMouseDown, since ListCtrl's internal mouse down behavior + // is to select (or deselect) rows when the mouse is released. We don't + // want the selection to change when the user checks or unchecks a checkbox, so if the + // mouse down event was to check/uncheck, we have to hide this mouse + // down event from the control. + + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessLButtonDown(this.OlvHitTest(x, y)); + } + + /// + /// Handle a left mouse down at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessLButtonDown(OlvListViewHitTestInfo hti) { + + if (hti.Item == null) + return false; + + // If they didn't click checkbox, we can just return + if (hti.HitTestLocation != HitTestLocation.CheckBox) + return false; + + // Disabled rows cannot change checkboxes + if (!hti.Item.Enabled) + return true; + + // Did they click a sub item checkbox? + if (hti.Column != null && hti.Column.Index > 0) { + if (hti.Column.IsEditable && hti.Item.Enabled) + this.ToggleSubItemCheckBox(hti.RowObject, hti.Column); + return true; + } + + // They must have clicked the primary checkbox + this.ToggleCheckObject(hti.RowObject); + + // If they change the checkbox of a selected row, all the rows in the selection + // should be given the same state + if (hti.Item.Selected) { + CheckState? state = this.GetCheckState(hti.RowObject); + if (state.HasValue) { + foreach (Object x in this.SelectedObjects) + this.SetObjectCheckedness(x, state.Value); + } + } + + return true; + } + + /// + /// Catch the Left Button up event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonUp(ref Message m) { + if (this.MouseMoveHitTest == null) + return false; + + // Are they trying to expand/collapse a group? + if (this.MouseMoveHitTest.HitTestLocation == HitTestLocation.GroupExpander) { + if (this.TriggerGroupExpandCollapse(this.MouseMoveHitTest.Group)) + return true; + } + + if (ObjectListView.IsVistaOrLater && this.HasCollapsibleGroups) + base.DefWndProc(ref m); + + return false; + } + + /// + /// Trigger a GroupExpandCollapse event and return true if the action was cancelled + /// + /// + /// + protected virtual bool TriggerGroupExpandCollapse(OLVGroup group) + { + GroupExpandingCollapsingEventArgs args = new GroupExpandingCollapsingEventArgs(group); + this.OnGroupExpandingCollapsing(args); + return args.Canceled; + } + + /// + /// Catch the Right Button down event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleRButtonDown(ref Message m) { + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessRButtonDown(this.OlvHitTest(x, y)); + } + + /// + /// Handle a left mouse down at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessRButtonDown(OlvListViewHitTestInfo hti) { + if (hti.Item == null) + return false; + + // Ignore clicks on checkboxes + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the Left Button double click event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleLButtonDoubleClick(ref Message m) { + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessLButtonDoubleClick(this.OlvHitTest(x, y)); + } + + /// + /// Handle a mouse double click at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessLButtonDoubleClick(OlvListViewHitTestInfo hti) { + + // If the user double clicked on a checkbox, ignore it + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the right Button double click event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleRButtonDoubleClick(ref Message m) { + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + return this.ProcessRButtonDoubleClick(this.OlvHitTest(x, y)); + } + + /// + /// Handle a right mouse double click at the given hit test location + /// + /// Subclasses can override this to do something unique + /// + /// True if the message has been handled + protected virtual bool ProcessRButtonDoubleClick(OlvListViewHitTestInfo hti) { + + // If the user double clicked on a checkbox, ignore it + return (hti.HitTestLocation == HitTestLocation.CheckBox); + } + + /// + /// Catch the MouseMove event. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleMouseMove(ref Message m) + { + //int x = m.LParam.ToInt32() & 0xFFFF; + //int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + + //this.lastMouseMoveX = x; + //this.lastMouseMoveY = y; + + return false; + } + //private int lastMouseMoveX = -1; + //private int lastMouseMoveY = -1; + + /// + /// Handle notifications that have been reflected back from the parent window + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleReflectNotify(ref Message m) { + const int NM_CLICK = -2; + const int NM_DBLCLK = -3; + const int NM_RDBLCLK = -6; + const int NM_CUSTOMDRAW = -12; + const int NM_RELEASEDCAPTURE = -16; + const int LVN_FIRST = -100; + const int LVN_ITEMCHANGED = LVN_FIRST - 1; + const int LVN_ITEMCHANGING = LVN_FIRST - 0; + const int LVN_HOTTRACK = LVN_FIRST - 21; + const int LVN_MARQUEEBEGIN = LVN_FIRST - 56; + const int LVN_GETINFOTIP = LVN_FIRST - 58; + const int LVN_GETDISPINFO = LVN_FIRST - 77; + const int LVN_BEGINSCROLL = LVN_FIRST - 80; + const int LVN_ENDSCROLL = LVN_FIRST - 81; + const int LVN_LINKCLICK = LVN_FIRST - 84; + const int LVN_GROUPINFO = LVN_FIRST - 88; // undocumented + const int LVIF_STATE = 8; + //const int LVIS_FOCUSED = 1; + const int LVIS_SELECTED = 2; + + bool isMsgHandled = false; + + // TODO: Don't do any logic in this method. Create separate methods for each message + + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + //System.Diagnostics.Debug.WriteLine(String.Format("rn: {0}", nmhdr->code)); + + switch (nmhdr.code) { + case NM_CLICK: + // The standard ListView does some strange stuff here when the list has checkboxes. + // If you shift click on non-primary columns when FullRowSelect is true, the + // checkedness of the selected rows changes. + // We can't just not do the base class stuff because it sets up state that is used to + // determine mouse up events later on. + // However, the actual processing we want is part of a "fall through" in RCLICK event handling. + // So... we solve our dilemma by turning this click event into a RCLICK event, which avoids + // the logic in the base class that we don't want, while including the logic that we do. + //System.Diagnostics.Debug.WriteLine("NM_CLICK"); + this.fakeRightClick = true; + nmhdr.code = -5; // NM_RCLICK; + Marshal.StructureToPtr(nmhdr, m.LParam, false); + break; + + case LVN_BEGINSCROLL: + //System.Diagnostics.Debug.WriteLine("LVN_BEGINSCROLL"); + isMsgHandled = this.HandleBeginScroll(ref m); + break; + + case LVN_ENDSCROLL: + isMsgHandled = this.HandleEndScroll(ref m); + break; + + case LVN_LINKCLICK: + isMsgHandled = this.HandleLinkClick(ref m); + break; + + case LVN_MARQUEEBEGIN: + //System.Diagnostics.Debug.WriteLine("LVN_MARQUEEBEGIN"); + this.isMarqueSelecting = true; + break; + + case LVN_GETINFOTIP: + //System.Diagnostics.Debug.WriteLine("LVN_GETINFOTIP"); + // When virtual lists are in SmallIcon view, they generates tooltip message with invalid item indicies. + NativeMethods.NMLVGETINFOTIP nmGetInfoTip = (NativeMethods.NMLVGETINFOTIP)m.GetLParam(typeof(NativeMethods.NMLVGETINFOTIP)); + isMsgHandled = nmGetInfoTip.iItem >= this.GetItemCount(); + break; + + case NM_RELEASEDCAPTURE: + //System.Diagnostics.Debug.WriteLine("NM_RELEASEDCAPTURE"); + this.isMarqueSelecting = false; + this.Invalidate(); + break; + + case NM_CUSTOMDRAW: + //System.Diagnostics.Debug.WriteLine("NM_CUSTOMDRAW"); + isMsgHandled = this.HandleCustomDraw(ref m); + break; + + case NM_DBLCLK: + // The default behavior of a .NET ListView with checkboxes is to toggle the checkbox on + // double-click. That's just silly, if you ask me :) + if (this.CheckBoxes) { + // How do we make ListView not do that silliness? We could just ignore the message + // but the last part of the base code sets up state information, and without that + // state, the ListView doesn't trigger MouseDoubleClick events. So we fake a + // right button double click event, which sets up the same state, but without + // toggling the checkbox. + nmhdr.code = NM_RDBLCLK; + Marshal.StructureToPtr(nmhdr, m.LParam, false); + } + break; + + case LVN_ITEMCHANGED: + //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGED"); + NativeMethods.NMLISTVIEW nmlistviewPtr2 = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); + if ((nmlistviewPtr2.uChanged & LVIF_STATE) != 0) { + CheckState currentValue = this.CalculateCheckState(nmlistviewPtr2.uOldState); + CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr2.uNewState); + if (currentValue != newCheckValue) + { + // Remove the state indicies so that we don't trigger the OnItemChecked method + // when we call our base method after exiting this method + nmlistviewPtr2.uOldState = (nmlistviewPtr2.uOldState & 0x0FFF); + nmlistviewPtr2.uNewState = (nmlistviewPtr2.uNewState & 0x0FFF); + Marshal.StructureToPtr(nmlistviewPtr2, m.LParam, false); + } + else + { + bool isSelected = (nmlistviewPtr2.uNewState & LVIS_SELECTED) == LVIS_SELECTED; + + if (isSelected) + { + // System.Diagnostics.Debug.WriteLine(String.Format("Selected: {0}", nmlistviewPtr2.iItem)); + bool isShiftDown = (Control.ModifierKeys & Keys.Shift) == Keys.Shift; + + // -1 indicates that all rows are to be selected -- in fact, they already have been. + // We now have to deselect all the disabled objects. + if (nmlistviewPtr2.iItem == -1 || isShiftDown) { + Stopwatch sw = Stopwatch.StartNew(); + foreach (object disabledModel in this.DisabledObjects) + { + int modelIndex = this.IndexOf(disabledModel); + if (modelIndex >= 0) + NativeMethods.DeselectOneItem(this, modelIndex); + } + System.Diagnostics.Debug.WriteLine(String.Format("PERF - Deselecting took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks)); + } + else + { + // If the object just selected is disabled, explicitly de-select it + OLVListItem olvi = this.GetItem(nmlistviewPtr2.iItem); + if (olvi != null && !olvi.Enabled) + NativeMethods.DeselectOneItem(this, nmlistviewPtr2.iItem); + } + } + } + } + break; + + case LVN_ITEMCHANGING: + //System.Diagnostics.Debug.WriteLine("LVN_ITEMCHANGING"); + NativeMethods.NMLISTVIEW nmlistviewPtr = (NativeMethods.NMLISTVIEW)m.GetLParam(typeof(NativeMethods.NMLISTVIEW)); + if ((nmlistviewPtr.uChanged & LVIF_STATE) != 0) { + CheckState currentValue = this.CalculateCheckState(nmlistviewPtr.uOldState); + CheckState newCheckValue = this.CalculateCheckState(nmlistviewPtr.uNewState); + + if (currentValue != newCheckValue) { + // Prevent the base method from seeing the state change, + // since we handled it elsewhere + nmlistviewPtr.uChanged &= ~LVIF_STATE; + Marshal.StructureToPtr(nmlistviewPtr, m.LParam, false); + } + } + break; + + case LVN_HOTTRACK: + break; + + case LVN_GETDISPINFO: + break; + + case LVN_GROUPINFO: + //System.Diagnostics.Debug.WriteLine("reflect notify: GROUP INFO"); + isMsgHandled = this.HandleGroupInfo(ref m); + break; + + //default: + //System.Diagnostics.Debug.WriteLine(String.Format("reflect notify: {0}", nmhdr.code)); + //break; + } + + return isMsgHandled; + } + private bool fakeRightClick = false; + + private CheckState CalculateCheckState(int state) { + switch ((state & 0xf000) >> 12) { + case 1: + return CheckState.Unchecked; + case 2: + return CheckState.Checked; + case 3: + return CheckState.Indeterminate; + default: + return CheckState.Checked; + } + } + + /// + /// In the notification messages, we handle attempts to change the width of our columns + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected bool HandleNotify(ref Message m) { + bool isMsgHandled = false; + + const int NM_CUSTOMDRAW = -12; + + const int HDN_FIRST = (0 - 300); + const int HDN_ITEMCHANGINGA = (HDN_FIRST - 0); + const int HDN_ITEMCHANGINGW = (HDN_FIRST - 20); + const int HDN_ITEMCLICKA = (HDN_FIRST - 2); + const int HDN_ITEMCLICKW = (HDN_FIRST - 22); + const int HDN_DIVIDERDBLCLICKA = (HDN_FIRST - 5); + const int HDN_DIVIDERDBLCLICKW = (HDN_FIRST - 25); + const int HDN_BEGINTRACKA = (HDN_FIRST - 6); + const int HDN_BEGINTRACKW = (HDN_FIRST - 26); + const int HDN_ENDTRACKA = (HDN_FIRST - 7); + const int HDN_ENDTRACKW = (HDN_FIRST - 27); + const int HDN_TRACKA = (HDN_FIRST - 8); + const int HDN_TRACKW = (HDN_FIRST - 28); + + // Handle the notification, remembering to handle both ANSI and Unicode versions + NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); + //System.Diagnostics.Debug.WriteLine(String.Format("not: {0}", nmhdr->code)); + + //if (nmhdr.code < HDN_FIRST) + // System.Diagnostics.Debug.WriteLine(nmhdr.code); + + // In KB Article #183258, MS states that when a header control has the HDS_FULLDRAG style, it will receive + // ITEMCHANGING events rather than TRACK events. Under XP SP2 (at least) this is not always true, which may be + // why MS has withdrawn that particular KB article. It is true that the header is always given the HDS_FULLDRAG + // style. But even while window style set, the control doesn't always received ITEMCHANGING events. + // The controlling setting seems to be the Explorer option "Show Window Contents While Dragging"! + // In the category of "truly bizarre side effects", if the this option is turned on, we will receive + // ITEMCHANGING events instead of TRACK events. But if it is turned off, we receive lots of TRACK events and + // only one ITEMCHANGING event at the very end of the process. + // If we receive HDN_TRACK messages, it's harder to control the resizing process. If we return a result of 1, we + // cancel the whole drag operation, not just that particular track event, which is clearly not what we want. + // If we are willing to compile with unsafe code enabled, we can modify the size of the column in place, using the + // commented out code below. But without unsafe code, the best we can do is allow the user to drag the column to + // any width, and then spring it back to within bounds once they release the mouse button. UI-wise it's very ugly. + switch (nmheader.nhdr.code) { + + case NM_CUSTOMDRAW: + if (!this.OwnerDrawnHeader) + isMsgHandled = this.HeaderControl.HandleHeaderCustomDraw(ref m); + break; + + case HDN_ITEMCLICKA: + case HDN_ITEMCLICKW: + if (!this.PossibleFinishCellEditing()) + { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + break; + + case HDN_DIVIDERDBLCLICKA: + case HDN_DIVIDERDBLCLICKW: + case HDN_BEGINTRACKA: + case HDN_BEGINTRACKW: + if (!this.PossibleFinishCellEditing()) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + break; + } + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + OLVColumn column = this.GetColumn(nmheader.iItem); + // Space filling columns can't be dragged or double-click resized + if (column.FillsFreeSpace) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + } + break; + case HDN_ENDTRACKA: + case HDN_ENDTRACKW: + //if (this.ShowGroups) + // this.ResizeLastGroup(); + break; + case HDN_TRACKA: + case HDN_TRACKW: + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); + OLVColumn column = this.GetColumn(nmheader.iItem); + if (hditem.cxy < column.MinimumWidth) + hditem.cxy = column.MinimumWidth; + else if (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth) + hditem.cxy = column.MaximumWidth; + Marshal.StructureToPtr(hditem, nmheader.pHDITEM, false); + } + break; + + case HDN_ITEMCHANGINGA: + case HDN_ITEMCHANGINGW: + nmheader = (NativeMethods.NMHEADER)m.GetLParam(typeof(NativeMethods.NMHEADER)); + if (nmheader.iItem >= 0 && nmheader.iItem < this.Columns.Count) { + NativeMethods.HDITEM hditem = (NativeMethods.HDITEM)Marshal.PtrToStructure(nmheader.pHDITEM, typeof(NativeMethods.HDITEM)); + OLVColumn column = this.GetColumn(nmheader.iItem); + // Check the mask to see if the width field is valid, and if it is, make sure it's within range + if ((hditem.mask & 1) == 1) { + if (hditem.cxy < column.MinimumWidth || + (column.MaximumWidth != -1 && hditem.cxy > column.MaximumWidth)) { + m.Result = (IntPtr)1; // prevent the change from happening + isMsgHandled = true; + } + } + } + break; + + case ToolTipControl.TTN_SHOW: + //System.Diagnostics.Debug.WriteLine("olv TTN_SHOW"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandleShow(ref m); + break; + + case ToolTipControl.TTN_POP: + //System.Diagnostics.Debug.WriteLine("olv TTN_POP"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandlePop(ref m); + break; + + case ToolTipControl.TTN_GETDISPINFO: + //System.Diagnostics.Debug.WriteLine("olv TTN_GETDISPINFO"); + if (this.CellToolTip.Handle == nmheader.nhdr.hwndFrom) + isMsgHandled = this.CellToolTip.HandleGetDispInfo(ref m); + break; + +// default: +// System.Diagnostics.Debug.WriteLine(String.Format("notify: {0}", nmheader.nhdr.code)); +// break; + } + + return isMsgHandled; + } + + /// + /// Create a ToolTipControl to manage the tooltip control used by the listview control + /// + protected virtual void CreateCellToolTip() { + this.cellToolTip = new ToolTipControl(); + this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); + this.cellToolTip.Showing += new EventHandler(HandleCellToolTipShowing); + this.cellToolTip.SetMaxWidth(); + NativeMethods.MakeTopMost(this.cellToolTip); + } + + /// + /// Update the handle used by our cell tooltip to be the tooltip used by + /// the underlying Windows listview control. + /// + protected virtual void UpdateCellToolTipHandle() { + if (this.cellToolTip != null && this.cellToolTip.Handle == IntPtr.Zero) + this.cellToolTip.AssignHandle(NativeMethods.GetTooltipControl(this)); + } + + /// + /// Handle the WM_PAINT event + /// + /// + /// Return true if the msg has been handled and nothing further should be done + protected virtual bool HandlePaint(ref Message m) { + //System.Diagnostics.Debug.WriteLine("> WMPAINT"); + + // We only want to custom draw the control within WmPaint message and only + // once per paint event. We use these bools to insure this. + this.isInWmPaintEvent = true; + this.shouldDoCustomDrawing = true; + this.prePaintLevel = 0; + + this.ShowOverlays(); + + this.HandlePrePaint(); + base.WndProc(ref m); + this.HandlePostPaint(); + this.isInWmPaintEvent = false; + //System.Diagnostics.Debug.WriteLine("< WMPAINT"); + return true; + } + private int prePaintLevel; + + /// + /// Perform any steps needed before painting the control + /// + protected virtual void HandlePrePaint() { + // When we get a WM_PAINT msg, remember the rectangle that is being updated. + // We can't get this information later, since the BeginPaint call wipes it out. + // this.lastUpdateRectangle = NativeMethods.GetUpdateRect(this); // we no longer need this, but keep the code so we can see it later + + //// When the list is empty, we want to handle the drawing of the control by ourselves. + //// Unfortunately, there is no easy way to tell our superclass that we want to do this. + //// So we resort to guile and deception. We validate the list area of the control, which + //// effectively tells our superclass that this area does not need to be painted. + //// Our superclass will then not paint the control, leaving us free to do so ourselves. + //// Without doing this trickery, the superclass will draw the list as empty, + //// and then moments later, we will draw the empty list msg, giving a nasty flicker + //if (this.GetItemCount() == 0 && this.HasEmptyListMsg) + // NativeMethods.ValidateRect(this, this.ClientRectangle); + } + + /// + /// Perform any steps needed after painting the control + /// + protected virtual void HandlePostPaint() { + // This message is no longer necessary, but we keep it for compatibility + } + + /// + /// Handle the window position changing. + /// + /// The m to be processed + /// bool to indicate if the msg has been handled + protected virtual bool HandleWindowPosChanging(ref Message m) { + const int SWP_NOSIZE = 1; + + NativeMethods.WINDOWPOS pos = (NativeMethods.WINDOWPOS)m.GetLParam(typeof(NativeMethods.WINDOWPOS)); + if ((pos.flags & SWP_NOSIZE) == 0) { + if (pos.cx < this.Bounds.Width) // only when shrinking + // pos.cx is the window width, not the client area width, so we have to subtract the border widths + this.ResizeFreeSpaceFillingColumns(pos.cx - (this.Bounds.Width - this.ClientSize.Width)); + } + + return false; + } + + #endregion + + #region Column header clicking, column hiding and resizing + + /// + /// The user has right clicked on the column headers. Do whatever is required + /// + /// Return true if this event has been handle + protected virtual bool HandleHeaderRightClick(int columnIndex) { + ColumnClickEventArgs eventArgs = new ColumnClickEventArgs(columnIndex); + this.OnColumnRightClick(eventArgs); + + // TODO: Allow users to say they have handled this event + + return this.ShowHeaderRightClickMenu(columnIndex, Cursor.Position); + } + + /// + /// Show a menu that is appropriate when the given column header is clicked. + /// + /// The index of the header that was clicked. This + /// can be -1, indicating that the header was clicked outside of a column + /// Where should the menu be shown + /// True if a menu was displayed + protected virtual bool ShowHeaderRightClickMenu(int columnIndex, Point pt) { + ToolStripDropDown m = this.MakeHeaderRightClickMenu(columnIndex); + if (m.Items.Count > 0) { + m.Show(pt); + return true; + } + + return false; + } + + /// + /// Create the menu that should be displayed when the user right clicks + /// on the given column header. + /// + /// Index of the column that was right clicked. + /// This can be negative, which indicates a click outside of any header. + /// The toolstrip that should be displayed + protected virtual ToolStripDropDown MakeHeaderRightClickMenu(int columnIndex) { + ToolStripDropDown m = new ContextMenuStrip(); + + if (columnIndex >= 0 && this.UseFiltering && this.ShowFilterMenuOnRightClick) + m = this.MakeFilteringMenu(m, columnIndex); + + if (columnIndex >= 0 && this.ShowCommandMenuOnRightClick) + m = this.MakeColumnCommandMenu(m, columnIndex); + + if (this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None) { + m = this.MakeColumnSelectMenu(m); + } + + return m; + } + + /// + /// The user has right clicked on the column headers. Do whatever is required + /// + /// Return true if this event has been handle + [Obsolete("Use HandleHeaderRightClick(int) instead")] + protected virtual bool HandleHeaderRightClick() { + return false; + } + + /// + /// Show a popup menu at the given point which will allow the user to choose which columns + /// are visible on this listview + /// + /// Where should the menu be placed + [Obsolete("Use ShowHeaderRightClickMenu instead")] + protected virtual void ShowColumnSelectMenu(Point pt) { + ToolStripDropDown m = this.MakeColumnSelectMenu(new ContextMenuStrip()); + m.Show(pt); + } + + /// + /// Show a popup menu at the given point which will allow the user to choose which columns + /// are visible on this listview + /// + /// + /// Where should the menu be placed + [Obsolete("Use ShowHeaderRightClickMenu instead")] + protected virtual void ShowColumnCommandMenu(int columnIndex, Point pt) { + ToolStripDropDown m = this.MakeColumnCommandMenu(new ContextMenuStrip(), columnIndex); + if (this.SelectColumnsOnRightClick) { + if (m.Items.Count > 0) + m.Items.Add(new ToolStripSeparator()); + this.MakeColumnSelectMenu(m); + } + m.Show(pt); + } + + /// + /// Gets or set the text to be used for the sorting ascending command + /// + [Category("Labels - ObjectListView"), DefaultValue("Sort ascending by '{0}'"), Localizable(true)] + public string MenuLabelSortAscending { + get { return this.menuLabelSortAscending; } + set { this.menuLabelSortAscending = value; } + } + private string menuLabelSortAscending = "Sort ascending by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Sort descending by '{0}'"), Localizable(true)] + public string MenuLabelSortDescending { + get { return this.menuLabelSortDescending; } + set { this.menuLabelSortDescending = value; } + } + private string menuLabelSortDescending = "Sort descending by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Group by '{0}'"), Localizable(true)] + public string MenuLabelGroupBy { + get { return this.menuLabelGroupBy; } + set { this.menuLabelGroupBy = value; } + } + private string menuLabelGroupBy = "Group by '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Lock grouping on '{0}'"), Localizable(true)] + public string MenuLabelLockGroupingOn { + get { return this.menuLabelLockGroupingOn; } + set { this.menuLabelLockGroupingOn = value; } + } + private string menuLabelLockGroupingOn = "Lock grouping on '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Unlock grouping from '{0}'"), Localizable(true)] + public string MenuLabelUnlockGroupingOn { + get { return this.menuLabelUnlockGroupingOn; } + set { this.menuLabelUnlockGroupingOn = value; } + } + private string menuLabelUnlockGroupingOn = "Unlock grouping from '{0}'"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Turn off groups"), Localizable(true)] + public string MenuLabelTurnOffGroups { + get { return this.menuLabelTurnOffGroups; } + set { this.menuLabelTurnOffGroups = value; } + } + private string menuLabelTurnOffGroups = "Turn off groups"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Unsort"), Localizable(true)] + public string MenuLabelUnsort { + get { return this.menuLabelUnsort; } + set { this.menuLabelUnsort = value; } + } + private string menuLabelUnsort = "Unsort"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Columns"), Localizable(true)] + public string MenuLabelColumns { + get { return this.menuLabelColumns; } + set { this.menuLabelColumns = value; } + } + private string menuLabelColumns = "Columns"; + + /// + /// + /// + [Category("Labels - ObjectListView"), DefaultValue("Select Columns..."), Localizable(true)] + public string MenuLabelSelectColumns { + get { return this.menuLabelSelectColumns; } + set { this.menuLabelSelectColumns = value; } + } + private string menuLabelSelectColumns = "Select Columns..."; + + /// + /// Gets or sets the image that will be place next to the Sort Ascending command + /// + static public Bitmap SortAscendingImage = BrightIdeasSoftware.Properties.Resources.SortAscending; + + /// + /// Gets or sets the image that will be placed next to the Sort Descending command + /// + static public Bitmap SortDescendingImage = BrightIdeasSoftware.Properties.Resources.SortDescending; + + /// + /// Append the column selection menu items to the given menu strip. + /// + /// The menu to which the items will be added. + /// + /// Return the menu to which the items were added + public virtual ToolStripDropDown MakeColumnCommandMenu(ToolStripDropDown strip, int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return strip; + + if (strip.Items.Count > 0) + strip.Items.Add(new ToolStripSeparator()); + + string label = String.Format(this.MenuLabelSortAscending, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, ObjectListView.SortAscendingImage, (EventHandler)delegate(object sender, EventArgs args) { + this.Sort(column, SortOrder.Ascending); + }); + } + label = String.Format(this.MenuLabelSortDescending, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, ObjectListView.SortDescendingImage, (EventHandler)delegate(object sender, EventArgs args) { + this.Sort(column, SortOrder.Descending); + }); + } + if (this.CanShowGroups) { + label = String.Format(this.MenuLabelGroupBy, column.Text); + if (column.Groupable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = true; + this.PrimarySortColumn = column; + this.PrimarySortOrder = SortOrder.Ascending; + this.BuildList(); + }); + } + } + if (this.ShowGroups) { + if (this.AlwaysGroupByColumn == column) { + label = String.Format(this.MenuLabelUnlockGroupingOn, column.Text); + if (!String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.AlwaysGroupByColumn = null; + this.AlwaysGroupBySortOrder = SortOrder.None; + this.BuildList(); + }); + } + } else { + label = String.Format(this.MenuLabelLockGroupingOn, column.Text); + if (column.Groupable && !String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = true; + this.AlwaysGroupByColumn = column; + this.AlwaysGroupBySortOrder = SortOrder.Ascending; + this.BuildList(); + }); + } + } + label = String.Format(this.MenuLabelTurnOffGroups, column.Text); + if (!String.IsNullOrEmpty(label)) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.ShowGroups = false; + this.BuildList(); + }); + } + } else { + label = String.Format(this.MenuLabelUnsort, column.Text); + if (column.Sortable && !String.IsNullOrEmpty(label) && this.PrimarySortOrder != SortOrder.None) { + strip.Items.Add(label, null, (EventHandler)delegate(object sender, EventArgs args) { + this.Unsort(); + }); + } + } + + return strip; + } + + /// + /// Append the column selection menu items to the given menu strip. + /// + /// The menu to which the items will be added. + /// Return the menu to which the items were added + public virtual ToolStripDropDown MakeColumnSelectMenu(ToolStripDropDown strip) { + + System.Diagnostics.Debug.Assert(this.SelectColumnsOnRightClickBehaviour != ColumnSelectBehaviour.None); + + // Append a separator if the menu isn't empty and the last item isn't already a separator + if (strip.Items.Count > 0 && (!(strip.Items[strip.Items.Count-1] is ToolStripSeparator))) + strip.Items.Add(new ToolStripSeparator()); + + if (this.AllColumns.Count > 0 && this.AllColumns[0].LastDisplayIndex == -1) + this.RememberDisplayIndicies(); + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.ModelDialog) { + strip.Items.Add(this.MenuLabelSelectColumns, null, delegate(object sender, EventArgs args) { + (new ColumnSelectionForm()).OpenOn(this); + }); + } + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.Submenu) { + ToolStripMenuItem menu = new ToolStripMenuItem(this.MenuLabelColumns); + menu.DropDownItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); + strip.Items.Add(menu); + this.AddItemsToColumnSelectMenu(menu.DropDownItems); + } + + if (this.SelectColumnsOnRightClickBehaviour == ColumnSelectBehaviour.InlineMenu) { + strip.ItemClicked += new ToolStripItemClickedEventHandler(this.ColumnSelectMenuItemClicked); + strip.Closing += new ToolStripDropDownClosingEventHandler(this.ColumnSelectMenuClosing); + this.AddItemsToColumnSelectMenu(strip.Items); + } + + return strip; + } + + /// + /// Create the menu items that will allow columns to be choosen and add them to the + /// given collection + /// + /// + protected void AddItemsToColumnSelectMenu(ToolStripItemCollection items) { + + // Sort columns by display order + List columns = new List(this.AllColumns); + columns.Sort(delegate(OLVColumn x, OLVColumn y) { return (x.LastDisplayIndex - y.LastDisplayIndex); }); + + // Build menu from sorted columns + foreach (OLVColumn col in columns) { + ToolStripMenuItem mi = new ToolStripMenuItem(col.Text); + mi.Checked = col.IsVisible; + mi.Tag = col; + + // The 'Index' property returns -1 when the column is not visible, so if the + // column isn't visible we have to enable the item. Also the first column can't be turned off + mi.Enabled = !col.IsVisible || col.CanBeHidden; + items.Add(mi); + } + } + + private void ColumnSelectMenuItemClicked(object sender, ToolStripItemClickedEventArgs e) { + this.contextMenuStaysOpen = false; + ToolStripMenuItem menuItemClicked = e.ClickedItem as ToolStripMenuItem; + if (menuItemClicked == null) + return; + OLVColumn col = menuItemClicked.Tag as OLVColumn; + if (col == null) + return; + menuItemClicked.Checked = !menuItemClicked.Checked; + col.IsVisible = menuItemClicked.Checked; + this.contextMenuStaysOpen = this.SelectColumnsMenuStaysOpen; + this.BeginInvoke(new MethodInvoker(this.RebuildColumns)); + } + private bool contextMenuStaysOpen; + + private void ColumnSelectMenuClosing(object sender, ToolStripDropDownClosingEventArgs e) { + e.Cancel = this.contextMenuStaysOpen && e.CloseReason == ToolStripDropDownCloseReason.ItemClicked; + this.contextMenuStaysOpen = false; + } + + /// + /// Create a Filtering menu + /// + /// + /// + /// + public virtual ToolStripDropDown MakeFilteringMenu(ToolStripDropDown strip, int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return strip; + + FilterMenuBuilder strategy = this.FilterMenuBuildStrategy; + if (strategy == null) + return strip; + + return strategy.MakeFilterMenu(strip, this, column); + } + + /// + /// Override the OnColumnReordered method to do what we want + /// + /// + protected override void OnColumnReordered(ColumnReorderedEventArgs e) { + base.OnColumnReordered(e); + + // The internal logic of the .NET code behind a ENDDRAG event means that, + // at this point, the DisplayIndex's of the columns are not yet as they are + // going to be. So we have to invoke a method to run later that will remember + // what the real DisplayIndex's are. + this.BeginInvoke(new MethodInvoker(this.RememberDisplayIndicies)); + } + + private void RememberDisplayIndicies() { + // Remember the display indexes so we can put them back at a later date + foreach (OLVColumn x in this.AllColumns) + x.LastDisplayIndex = x.DisplayIndex; + } + + /// + /// When the column widths are changing, resize the space filling columns + /// + /// + /// + protected virtual void HandleColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e) { + if (this.UpdateSpaceFillingColumnsWhenDraggingColumnDivider && !this.GetColumn(e.ColumnIndex).FillsFreeSpace) { + // If the width of a column is increasing, resize any space filling columns allowing the extra + // space that the new column width is going to consume + int oldWidth = this.GetColumn(e.ColumnIndex).Width; + if (e.NewWidth > oldWidth) + this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width - (e.NewWidth - oldWidth)); + else + this.ResizeFreeSpaceFillingColumns(); + } + } + + /// + /// When the column widths change, resize the space filling columns + /// + /// + /// + protected virtual void HandleColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e) { + if (!this.GetColumn(e.ColumnIndex).FillsFreeSpace) + this.ResizeFreeSpaceFillingColumns(); + } + + /// + /// When the size of the control changes, we have to resize our space filling columns. + /// + /// + /// + protected virtual void HandleLayout(object sender, LayoutEventArgs e) { + // We have to delay executing the recalculation of the columns, since virtual lists + // get terribly confused if we resize the column widths during this event. + if (!this.hasResizeColumnsHandler) { + this.hasResizeColumnsHandler = true; + this.RunWhenIdle(this.HandleApplicationIdleResizeColumns); + } + } + + private void RunWhenIdle(EventHandler eventHandler) { + Application.Idle += eventHandler; + if (!this.CanUseApplicationIdle) { + SynchronizationContext.Current.Post(delegate(object x) { Application.RaiseIdle(EventArgs.Empty); }, null); + } + } + + /// + /// Resize our space filling columns so they fill any unoccupied width in the control + /// + protected virtual void ResizeFreeSpaceFillingColumns() { + this.ResizeFreeSpaceFillingColumns(this.ClientSize.Width); + } + + /// + /// Resize our space filling columns so they fill any unoccupied width in the control + /// + protected virtual void ResizeFreeSpaceFillingColumns(int freeSpace) { + // It's too confusing to dynamically resize columns at design time. + if (this.DesignMode) + return; + + if (this.Frozen) + return; + + // Calculate the free space available + int totalProportion = 0; + List spaceFillingColumns = new List(); + for (int i = 0; i < this.Columns.Count; i++) { + OLVColumn col = this.GetColumn(i); + if (col.FillsFreeSpace) { + spaceFillingColumns.Add(col); + totalProportion += col.FreeSpaceProportion; + } else + freeSpace -= col.Width; + } + freeSpace = Math.Max(0, freeSpace); + + // Any space filling column that would hit it's Minimum or Maximum + // width must be treated as a fixed column. + foreach (OLVColumn col in spaceFillingColumns.ToArray()) { + int newWidth = (freeSpace * col.FreeSpaceProportion) / totalProportion; + + if (col.MinimumWidth != -1 && newWidth < col.MinimumWidth) + newWidth = col.MinimumWidth; + else if (col.MaximumWidth != -1 && newWidth > col.MaximumWidth) + newWidth = col.MaximumWidth; + else + newWidth = 0; + + if (newWidth > 0) { + col.Width = newWidth; + freeSpace -= newWidth; + totalProportion -= col.FreeSpaceProportion; + spaceFillingColumns.Remove(col); + } + } + + // Distribute the free space between the columns + foreach (OLVColumn col in spaceFillingColumns) { + col.Width = (freeSpace*col.FreeSpaceProportion)/totalProportion; + } + } + + #endregion + + #region Checkboxes + + /// + /// Check all rows + /// + public virtual void CheckAll() + { + this.CheckedObjects = EnumerableToArray(this.Objects, false); + } + + /// + /// Check the checkbox in the given column header + /// + /// If the given columns header check box is linked to the cell check boxes, + /// then checkboxes in all cells will also be checked. + /// + public virtual void CheckHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Unchecked); + } + + /// + /// Mark the checkbox in the given column header as having an indeterminate value + /// + /// + public virtual void CheckIndeterminateHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Indeterminate); + } + + /// + /// Mark the given object as indeterminate check state + /// + /// The model object to be marked indeterminate + public virtual void CheckIndeterminateObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Indeterminate); + } + + /// + /// Mark the given object as checked in the list + /// + /// The model object to be checked + public virtual void CheckObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Checked); + } + + /// + /// Mark the given objects as checked in the list + /// + /// The model object to be checked + public virtual void CheckObjects(IEnumerable modelObjects) { + foreach (object model in modelObjects) + this.CheckObject(model); + } + + /// + /// Put a check into the check box at the given cell + /// + /// + /// + public virtual void CheckSubItem(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Checked); + this.RefreshObject(rowObject); + } + + /// + /// Put an indeterminate check into the check box at the given cell + /// + /// + /// + public virtual void CheckIndeterminateSubItem(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Indeterminate); + this.RefreshObject(rowObject); + } + + /// + /// Return true of the given object is checked + /// + /// The model object whose checkedness is returned + /// Is the given object checked? + /// If the given object is not in the list, this method returns false. + public virtual bool IsChecked(object modelObject) { + return this.GetCheckState(modelObject) == CheckState.Checked; + } + + /// + /// Return true of the given object is indeterminately checked + /// + /// The model object whose checkedness is returned + /// Is the given object indeterminately checked? + /// If the given object is not in the list, this method returns false. + public virtual bool IsCheckedIndeterminate(object modelObject) { + return this.GetCheckState(modelObject) == CheckState.Indeterminate; + } + + /// + /// Is there a check at the check box at the given cell + /// + /// + /// + public virtual bool IsSubItemChecked(object rowObject, OLVColumn column) { + if (column == null || rowObject == null || !column.CheckBoxes) + return false; + return (column.GetCheckState(rowObject) == CheckState.Checked); + } + + /// + /// Get the checkedness of an object from the model. Returning null means the + /// model does not know and the value from the control will be used. + /// + /// + /// + protected virtual CheckState? GetCheckState(Object modelObject) { + if (this.CheckStateGetter != null) + return this.CheckStateGetter(modelObject); + return this.PersistentCheckBoxes ? this.GetPersistentCheckState(modelObject) : (CheckState?)null; + } + + /// + /// Record the change of checkstate for the given object in the model. + /// This does not update the UI -- only the model + /// + /// + /// + /// The check state that was recorded and that should be used to update + /// the control. + protected virtual CheckState PutCheckState(Object modelObject, CheckState state) { + if (this.CheckStatePutter != null) + return this.CheckStatePutter(modelObject, state); + return this.PersistentCheckBoxes ? this.SetPersistentCheckState(modelObject, state) : state; + } + + /// + /// Change the check state of the given object to be the given state. + /// + /// + /// If the given model object isn't in the list, we still try to remember + /// its state, in case it is referenced in the future. + /// + /// + /// True if the checkedness of the model changed + protected virtual bool SetObjectCheckedness(object modelObject, CheckState state) { + + if (GetCheckState(modelObject) == state) + return false; + + OLVListItem olvi = this.ModelToItem(modelObject); + + // If we didn't find the given, we still try to record the check state. + if (olvi == null) { + this.PutCheckState(modelObject, state); + return true; + } + + // Trigger checkbox changing event + ItemCheckEventArgs ice = new ItemCheckEventArgs(olvi.Index, state, olvi.CheckState); + this.OnItemCheck(ice); + if (ice.NewValue == olvi.CheckState) + return false; + + olvi.CheckState = this.PutCheckState(modelObject, state); + this.RefreshItem(olvi); + + // Trigger check changed event + this.OnItemChecked(new ItemCheckedEventArgs(olvi)); + return true; + } + + /// + /// Toggle the checkedness of the given object. A checked object becomes + /// unchecked; an unchecked or indeterminate object becomes checked. + /// If the list has tristate checkboxes, the order is: + /// unchecked -> checked -> indeterminate -> unchecked ... + /// + /// The model object to be checked + public virtual void ToggleCheckObject(object modelObject) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi == null) + return; + + CheckState newState = CheckState.Checked; + + if (olvi.CheckState == CheckState.Checked) { + newState = this.TriStateCheckBoxes ? CheckState.Indeterminate : CheckState.Unchecked; + } else { + if (olvi.CheckState == CheckState.Indeterminate && this.TriStateCheckBoxes) + newState = CheckState.Unchecked; + } + this.SetObjectCheckedness(modelObject, newState); + } + + /// + /// Toggle the checkbox in the header of the given column + /// + /// Obviously, this is only useful if the column actually has a header checkbox. + /// + public virtual void ToggleHeaderCheckBox(OLVColumn column) { + if (column == null) + return; + + CheckState newState = CalculateToggledCheckState(column.HeaderCheckState, column.HeaderTriStateCheckBox, column.HeaderCheckBoxDisabled); + ChangeHeaderCheckBoxState(column, newState); + } + + private void ChangeHeaderCheckBoxState(OLVColumn column, CheckState newState) { + // Tell the world the checkbox was clicked + HeaderCheckBoxChangingEventArgs args = new HeaderCheckBoxChangingEventArgs(); + args.Column = column; + args.NewCheckState = newState; + + this.OnHeaderCheckBoxChanging(args); + if (args.Cancel || column.HeaderCheckState == args.NewCheckState) + return; + + Stopwatch sw = Stopwatch.StartNew(); + + column.HeaderCheckState = args.NewCheckState; + this.HeaderControl.Invalidate(column); + + if (column.HeaderCheckBoxUpdatesRowCheckBoxes) { + if (column.Index == 0) + this.UpdateAllPrimaryCheckBoxes(column); + else + this.UpdateAllSubItemCheckBoxes(column); + } + + Debug.WriteLine(String.Format("PERF - Changing row checkboxes on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + + private void UpdateAllPrimaryCheckBoxes(OLVColumn column) { + if (!this.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) + return; + + if (column.HeaderCheckState == CheckState.Checked) + CheckAll(); + else + UncheckAll(); + } + + private void UpdateAllSubItemCheckBoxes(OLVColumn column) { + if (!column.CheckBoxes || column.HeaderCheckState == CheckState.Indeterminate) + return; + + foreach (object model in this.Objects) + column.PutCheckState(model, column.HeaderCheckState); + this.BuildList(true); + } + + /// + /// Toggle the check at the check box of the given cell + /// + /// + /// + public virtual void ToggleSubItemCheckBox(object rowObject, OLVColumn column) { + CheckState currentState = column.GetCheckState(rowObject); + CheckState newState = CalculateToggledCheckState(currentState, column.TriStateCheckBoxes, false); + + SubItemCheckingEventArgs args = new SubItemCheckingEventArgs(column, this.ModelToItem(rowObject), column.Index, currentState, newState); + this.OnSubItemChecking(args); + if (args.Canceled) + return; + + switch (args.NewValue) { + case CheckState.Checked: + this.CheckSubItem(rowObject, column); + break; + case CheckState.Indeterminate: + this.CheckIndeterminateSubItem(rowObject, column); + break; + case CheckState.Unchecked: + this.UncheckSubItem(rowObject, column); + break; + } + } + + /// + /// Uncheck all rows + /// + public virtual void UncheckAll() + { + this.CheckedObjects = null; + } + + /// + /// Mark the given object as unchecked in the list + /// + /// The model object to be unchecked + public virtual void UncheckObject(object modelObject) { + this.SetObjectCheckedness(modelObject, CheckState.Unchecked); + } + + /// + /// Mark the given objects as unchecked in the list + /// + /// The model object to be checked + public virtual void UncheckObjects(IEnumerable modelObjects) { + foreach (object model in modelObjects) + this.UncheckObject(model); + } + + /// + /// Uncheck the checkbox in the given column header + /// + /// + public virtual void UncheckHeaderCheckBox(OLVColumn column) + { + if (column == null) + return; + + ChangeHeaderCheckBoxState(column, CheckState.Unchecked); + } + + /// + /// Uncheck the check at the given cell + /// + /// + /// + public virtual void UncheckSubItem(object rowObject, OLVColumn column) + { + if (column == null || rowObject == null || !column.CheckBoxes) + return; + + column.PutCheckState(rowObject, CheckState.Unchecked); + this.RefreshObject(rowObject); + } + + #endregion + + #region OLV accessing + + /// + /// Return the column at the given index + /// + /// Index of the column to be returned + /// An OLVColumn + public virtual OLVColumn GetColumn(int index) { + return (OLVColumn)this.Columns[index]; + } + + /// + /// Return the column at the given title. + /// + /// Name of the column to be returned + /// An OLVColumn + public virtual OLVColumn GetColumn(string name) { + foreach (ColumnHeader column in this.Columns) { + if (column.Text == name) + return (OLVColumn)column; + } + return null; + } + + /// + /// Return a collection of columns that are visible to the given view. + /// Only Tile and Details have columns; all other views have 0 columns. + /// + /// Which view are the columns being calculate for? + /// A list of columns + public virtual List GetFilteredColumns(View view) { + // For both detail and tile view, the first column must be included. Normally, we would + // use the ColumnHeader.Index property, but if the header is not currently part of a ListView + // that property returns -1. So, we track the index of + // the column header, and always include the first header. + + int index = 0; + return this.AllColumns.FindAll(delegate(OLVColumn x) { + return (index++ == 0) || x.IsVisible; + }); + } + + /// + /// Return the number of items in the list + /// + /// the number of items in the list + /// If a filter is installed, this will return the number of items that match the filter. + public virtual int GetItemCount() { + return this.Items.Count; + } + + /// + /// Return the item at the given index + /// + /// Index of the item to be returned + /// An OLVListItem + public virtual OLVListItem GetItem(int index) { + if (index < 0 || index >= this.GetItemCount()) + return null; + + return (OLVListItem)this.Items[index]; + } + + /// + /// Return the model object at the given index + /// + /// Index of the model object to be returned + /// A model object + public virtual object GetModelObject(int index) { + OLVListItem item = this.GetItem(index); + return item == null ? null : item.RowObject; + } + + /// + /// Find the item and column that are under the given co-ords + /// + /// X co-ord + /// Y co-ord + /// The column under the given point + /// The item under the given point. Can be null. + public virtual OLVListItem GetItemAt(int x, int y, out OLVColumn hitColumn) { + hitColumn = null; + ListViewHitTestInfo info = this.HitTest(x, y); + if (info.Item == null) + return null; + + if (info.SubItem != null) { + int subItemIndex = info.Item.SubItems.IndexOf(info.SubItem); + hitColumn = this.GetColumn(subItemIndex); + } + + return (OLVListItem)info.Item; + } + + /// + /// Return the sub item at the given index/column + /// + /// Index of the item to be returned + /// Index of the subitem to be returned + /// An OLVListSubItem + public virtual OLVListSubItem GetSubItem(int index, int columnIndex) { + OLVListItem olvi = this.GetItem(index); + return olvi == null ? null : olvi.GetSubItem(columnIndex); + } + + #endregion + + #region Object manipulation + + /// + /// Scroll the listview so that the given group is at the top. + /// + /// The group to be revealed + /// + /// If the group is already visible, the list will still be scrolled to move + /// the group to the top, if that is possible. + /// + /// This only works when the list is showing groups (obviously). + /// This does not work on virtual lists, since virtual lists don't use ListViewGroups + /// for grouping. Use instead. + /// + public virtual void EnsureGroupVisible(ListViewGroup lvg) { + if (!this.ShowGroups || lvg == null) + return; + + int groupIndex = this.Groups.IndexOf(lvg); + if (groupIndex <= 0) { + // There is no easy way to scroll back to the beginning of the list + int delta = 0 - NativeMethods.GetScrollPosition(this, false); + NativeMethods.Scroll(this, 0, delta); + } else { + // Find the display rectangle of the last item in the previous group + ListViewGroup previousGroup = this.Groups[groupIndex - 1]; + ListViewItem lastItemInGroup = previousGroup.Items[previousGroup.Items.Count - 1]; + Rectangle r = this.GetItemRect(lastItemInGroup.Index); + + // Scroll so that the last item of the previous group is just out of sight, + // which will make the desired group header visible. + int delta = r.Y + r.Height / 2; + NativeMethods.Scroll(this, 0, delta); + } + } + + /// + /// Ensure that the given model object is visible + /// + /// The model object to be revealed + public virtual void EnsureModelVisible(Object modelObject) { + int index = this.IndexOf(modelObject); + if (index >= 0) + this.EnsureVisible(index); + } + + /// + /// Return the model object of the row that is selected or null if there is no selection or more than one selection + /// + /// Model object or null + [Obsolete("Use SelectedObject property instead of this method")] + public virtual object GetSelectedObject() { + return this.SelectedObject; + } + + /// + /// Return the model objects of the rows that are selected or an empty collection if there is no selection + /// + /// ArrayList + [Obsolete("Use SelectedObjects property instead of this method")] + public virtual ArrayList GetSelectedObjects() { + return ObjectListView.EnumerableToArray(this.SelectedObjects, false); + } + + /// + /// Return the model object of the row that is checked or null if no row is checked + /// or more than one row is checked + /// + /// Model object or null + /// Use CheckedObject property instead of this method + [Obsolete("Use CheckedObject property instead of this method")] + public virtual object GetCheckedObject() { + return this.CheckedObject; + } + + /// + /// Get the collection of model objects that are checked. + /// + /// Use CheckedObjects property instead of this method + [Obsolete("Use CheckedObjects property instead of this method")] + public virtual ArrayList GetCheckedObjects() { + return ObjectListView.EnumerableToArray(this.CheckedObjects, false); + } + + /// + /// Find the given model object within the listview and return its index + /// + /// The model object to be found + /// The index of the object. -1 means the object was not present + public virtual int IndexOf(Object modelObject) { + for (int i = 0; i < this.GetItemCount(); i++) { + if (this.GetModelObject(i).Equals(modelObject)) + return i; + } + return -1; + } + + /// + /// Rebuild the given ListViewItem with the data from its associated model. + /// + /// This method does not resort or regroup the view. It simply updates + /// the displayed data of the given item + public virtual void RefreshItem(OLVListItem olvi) { + olvi.UseItemStyleForSubItems = true; + olvi.SubItems.Clear(); + this.FillInValues(olvi, olvi.RowObject); + this.PostProcessOneRow(olvi.Index, this.GetDisplayOrderOfItemIndex(olvi.Index), olvi); + } + + /// + /// Rebuild the data on the row that is showing the given object. + /// + /// + /// + /// This method does not resort or regroup the view. + /// + /// + /// The given object is *not* used as the source of data for the rebuild. + /// It is only used to locate the matching model in the collection, + /// then that matching model is used as the data source. This distinction is + /// only important in model classes that have overridden the Equals() method. + /// + /// + /// If you want the given model object to replace the pre-existing model, + /// use . + /// + /// + public virtual void RefreshObject(object modelObject) { + this.RefreshObjects(new object[] { modelObject }); + } + + /// + /// Update the rows that are showing the given objects + /// + /// + /// This method does not resort or regroup the view. + /// This method can safely be called from background threads. + /// + public virtual void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); + return; + } + foreach (object modelObject in modelObjects) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null) { + this.ReplaceModel(olvi, modelObject); + this.RefreshItem(olvi); + } + } + } + + private void ReplaceModel(OLVListItem olvi, object newModel) { + if (ReferenceEquals(olvi.RowObject, newModel)) + return; + + this.TakeOwnershipOfObjects(); + ArrayList array = ObjectListView.EnumerableToArray(this.Objects, false); + int i = array.IndexOf(olvi.RowObject); + if (i >= 0) + array[i] = newModel; + + olvi.RowObject = newModel; + } + + /// + /// Update the rows that are selected + /// + /// This method does not resort or regroup the view. + public virtual void RefreshSelectedObjects() { + foreach (ListViewItem lvi in this.SelectedItems) + this.RefreshItem((OLVListItem)lvi); + } + + /// + /// Select the row that is displaying the given model object, in addition to any current selection. + /// + /// The object to be selected + /// Use the property to deselect all other rows + public virtual void SelectObject(object modelObject) { + this.SelectObject(modelObject, false); + } + + /// + /// Select the row that is displaying the given model object, in addition to any current selection. + /// + /// The object to be selected + /// Should the object be focused as well? + /// Use the property to deselect all other rows + public virtual void SelectObject(object modelObject, bool setFocus) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null && olvi.Enabled) { + olvi.Selected = true; + if (setFocus) + olvi.Focused = true; + } + } + + /// + /// Select the rows that is displaying any of the given model object. All other rows are deselected. + /// + /// A collection of model objects + public virtual void SelectObjects(IList modelObjects) { + this.SelectedIndices.Clear(); + + if (modelObjects == null) + return; + + foreach (object modelObject in modelObjects) { + OLVListItem olvi = this.ModelToItem(modelObject); + if (olvi != null && olvi.Enabled) + olvi.Selected = true; + } + } + + #endregion + + #region Freezing/Suspending + + /// + /// Get or set whether or not the listview is frozen. When the listview is + /// frozen, it will not update itself. + /// + /// The Frozen property is similar to the methods Freeze()/Unfreeze() + /// except that setting Frozen property to false immediately unfreezes the control + /// regardless of the number of Freeze() calls outstanding. + /// objectListView1.Frozen = false; // unfreeze the control now! + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual bool Frozen { + get { return freezeCount > 0; } + set { + if (value) + Freeze(); + else if (freezeCount > 0) { + freezeCount = 1; + Unfreeze(); + } + } + } + private int freezeCount; + + /// + /// Freeze the listview so that it no longer updates itself. + /// + /// Freeze()/Unfreeze() calls nest correctly + public virtual void Freeze() { + if (freezeCount == 0) + DoFreeze(); + + freezeCount++; + this.OnFreezing(new FreezeEventArgs(freezeCount)); + } + + /// + /// Unfreeze the listview. If this call is the outermost Unfreeze(), + /// the contents of the listview will be rebuilt. + /// + /// Freeze()/Unfreeze() calls nest correctly + public virtual void Unfreeze() { + if (freezeCount <= 0) + return; + + freezeCount--; + if (freezeCount == 0) + DoUnfreeze(); + + this.OnFreezing(new FreezeEventArgs(freezeCount)); + } + + /// + /// Do the actual work required when the listview is frozen + /// + protected virtual void DoFreeze() { + this.BeginUpdate(); + } + + /// + /// Do the actual work required when the listview is unfrozen + /// + protected virtual void DoUnfreeze() + { + this.EndUpdate(); + this.ResizeFreeSpaceFillingColumns(); + this.BuildList(); + } + + /// + /// Returns true if selection events are currently suspended. + /// While selection events are suspended, neither SelectedIndexChanged + /// or SelectionChanged events will be raised. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + protected bool SelectionEventsSuspended { + get { return this.suspendSelectionEventCount > 0; } + } + + /// + /// Suspend selection events until a matching ResumeSelectionEvents() + /// is called. + /// + /// Calls to this method nest correctly. Every call to SuspendSelectionEvents() + /// must have a matching ResumeSelectionEvents(). + protected void SuspendSelectionEvents() { + this.suspendSelectionEventCount++; + } + + /// + /// Resume raising selection events. + /// + protected void ResumeSelectionEvents() { + Debug.Assert(this.SelectionEventsSuspended, "Mismatched called to ResumeSelectionEvents()"); + this.suspendSelectionEventCount--; + } + + /// + /// Returns a disposable that will disable selection events + /// during a using() block. + /// + /// + protected IDisposable SuspendSelectionEventsDuring() { + return new SuspendSelectionDisposable(this); + } + + /// + /// Implementation only class that suspends and resumes selection + /// events on instance creation and disposal. + /// + private class SuspendSelectionDisposable : IDisposable { + public SuspendSelectionDisposable(ObjectListView objectListView) { + this.objectListView = objectListView; + this.objectListView.SuspendSelectionEvents(); + } + + public void Dispose() { + this.objectListView.ResumeSelectionEvents(); + } + + private readonly ObjectListView objectListView; + } + + #endregion + + #region Column sorting + + /// + /// Sort the items by the last sort column and order + /// + new public void Sort() { + this.Sort(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The name of the column whose values will be used for the sorting + public virtual void Sort(string columnToSortName) { + this.Sort(this.GetColumn(columnToSortName), this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The index of the column whose values will be used for the sorting + public virtual void Sort(int columnToSortIndex) { + if (columnToSortIndex >= 0 && columnToSortIndex < this.Columns.Count) + this.Sort(this.GetColumn(columnToSortIndex), this.PrimarySortOrder); + } + + /// + /// Sort the items in the list view by the values in the given column and the last sort order + /// + /// The column whose values will be used for the sorting + public virtual void Sort(OLVColumn columnToSort) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort); }); + } else { + this.Sort(columnToSort, this.PrimarySortOrder); + } + } + + /// + /// Sort the items in the list view by the values in the given column and by the given order. + /// + /// The column whose values will be used for the sorting. + /// If null, the first column will be used. + /// The ordering to be used for sorting. If this is None, + /// this.Sorting and then SortOrder.Ascending will be used + /// If ShowGroups is true, the rows will be grouped by the given column. + /// If AlwaysGroupsByColumn is not null, the rows will be grouped by that column, + /// and the rows within each group will be sorted by the given column. + public virtual void Sort(OLVColumn columnToSort, SortOrder order) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.Sort(columnToSort, order); }); + } else { + this.DoSort(columnToSort, order); + this.PostProcessRows(); + } + } + + private void DoSort(OLVColumn columnToSort, SortOrder order) { + // Sanity checks + if (this.GetItemCount() == 0 || this.Columns.Count == 0) + return; + + // Fill in default values, if the parameters don't make sense + if (this.ShowGroups) { + columnToSort = columnToSort ?? this.GetColumn(0); + if (order == SortOrder.None) { + order = this.Sorting; + if (order == SortOrder.None) + order = SortOrder.Ascending; + } + } + + // Give the world a chance to fiddle with or completely avoid the sorting process + BeforeSortingEventArgs args = this.BuildBeforeSortingEventArgs(columnToSort, order); + this.OnBeforeSorting(args); + if (args.Canceled) + return; + + // Virtual lists don't preserve selection, so we have to do it specifically + // THINK: Do we need to preserve focus too? + IList selection = this.VirtualMode ? this.SelectedObjects : null; + this.SuspendSelectionEvents(); + + this.ClearHotItem(); + + // Finally, do the work of sorting, unless an event handler has already done the sorting for us + if (!args.Handled) { + // Sanity checks + if (args.ColumnToSort != null && args.SortOrder != SortOrder.None) { + if (this.ShowGroups) + this.BuildGroups(args.ColumnToGroupBy, args.GroupByOrder, args.ColumnToSort, args.SortOrder, + args.SecondaryColumnToSort, args.SecondarySortOrder); + else if (this.CustomSorter != null) + this.CustomSorter(args.ColumnToSort, args.SortOrder); + else + this.ListViewItemSorter = new ColumnComparer(args.ColumnToSort, args.SortOrder, + args.SecondaryColumnToSort, args.SecondarySortOrder); + } + } + + if (this.ShowSortIndicators) + this.ShowSortIndicator(args.ColumnToSort, args.SortOrder); + + this.PrimarySortColumn = args.ColumnToSort; + this.PrimarySortOrder = args.SortOrder; + + if (selection != null && selection.Count > 0) + this.SelectedObjects = selection; + this.ResumeSelectionEvents(); + + this.RefreshHotItem(); + + this.OnAfterSorting(new AfterSortingEventArgs(args)); + } + + /// + /// Put a sort indicator next to the text of the sort column + /// + public virtual void ShowSortIndicator() { + if (this.ShowSortIndicators && this.PrimarySortOrder != SortOrder.None) + this.ShowSortIndicator(this.PrimarySortColumn, this.PrimarySortOrder); + } + + /// + /// Put a sort indicator next to the text of the given given column + /// + /// The column to be marked + /// The sort order in effect on that column + protected virtual void ShowSortIndicator(OLVColumn columnToSort, SortOrder sortOrder) { + int imageIndex = -1; + + if (!NativeMethods.HasBuiltinSortIndicators()) { + // If we can't use builtin image, we have to make and then locate the index of the + // sort indicator we want to use. SortOrder.None doesn't show an image. + if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(SORT_INDICATOR_UP_KEY)) + MakeSortIndicatorImages(); + + if (this.SmallImageList != null) + { + string key = sortOrder == SortOrder.Ascending ? SORT_INDICATOR_UP_KEY : SORT_INDICATOR_DOWN_KEY; + imageIndex = this.SmallImageList.Images.IndexOfKey(key); + } + } + + // Set the image for each column + for (int i = 0; i < this.Columns.Count; i++) { + if (columnToSort != null && i == columnToSort.Index) + NativeMethods.SetColumnImage(this, i, sortOrder, imageIndex); + else + NativeMethods.SetColumnImage(this, i, SortOrder.None, -1); + } + } + + /// + /// The name of the image used when a column is sorted ascending + /// + /// This image is only used on pre-XP systems. System images are used for XP and later + public const string SORT_INDICATOR_UP_KEY = "sort-indicator-up"; + + /// + /// The name of the image used when a column is sorted descending + /// + /// This image is only used on pre-XP systems. System images are used for XP and later + public const string SORT_INDICATOR_DOWN_KEY = "sort-indicator-down"; + + /// + /// If the sort indicator images don't already exist, this method will make and install them + /// + protected virtual void MakeSortIndicatorImages() { + // Don't mess with the image list in design mode + if (this.DesignMode) + return; + + ImageList il = this.SmallImageList; + if (il == null) { + il = new ImageList(); + il.ImageSize = new Size(16, 16); + il.ColorDepth = ColorDepth.Depth32Bit; + } + + // This arrangement of points works well with (16,16) images, and OK with others + int midX = il.ImageSize.Width / 2; + int midY = (il.ImageSize.Height / 2) - 1; + int deltaX = midX - 2; + int deltaY = deltaX / 2; + + if (il.Images.IndexOfKey(SORT_INDICATOR_UP_KEY) == -1) { + Point pt1 = new Point(midX - deltaX, midY + deltaY); + Point pt2 = new Point(midX, midY - deltaY - 1); + Point pt3 = new Point(midX + deltaX, midY + deltaY); + il.Images.Add(SORT_INDICATOR_UP_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); + } + + if (il.Images.IndexOfKey(SORT_INDICATOR_DOWN_KEY) == -1) { + Point pt1 = new Point(midX - deltaX, midY - deltaY); + Point pt2 = new Point(midX, midY + deltaY); + Point pt3 = new Point(midX + deltaX, midY - deltaY); + il.Images.Add(SORT_INDICATOR_DOWN_KEY, this.MakeTriangleBitmap(il.ImageSize, new Point[] { pt1, pt2, pt3 })); + } + + this.SmallImageList = il; + } + + private Bitmap MakeTriangleBitmap(Size sz, Point[] pts) { + Bitmap bm = new Bitmap(sz.Width, sz.Height); + Graphics g = Graphics.FromImage(bm); + g.FillPolygon(new SolidBrush(Color.Gray), pts); + return bm; + } + + /// + /// Remove any sorting and revert to the given order of the model objects + /// + public virtual void Unsort() { + this.ShowGroups = false; + this.PrimarySortColumn = null; + this.PrimarySortOrder = SortOrder.None; + this.BuildList(); + } + + #endregion + + #region Utilities + + private static CheckState CalculateToggledCheckState(CheckState currentState, bool isTriState, bool isDisabled) + { + if (isDisabled) + return currentState; + switch (currentState) + { + case CheckState.Checked: return isTriState ? CheckState.Indeterminate : CheckState.Unchecked; + case CheckState.Indeterminate: return CheckState.Unchecked; + default: return CheckState.Checked; + } + } + + /// + /// Do the actual work of creating the given list of groups + /// + /// + protected virtual void CreateGroups(IEnumerable groups) { + this.Groups.Clear(); + // The group must be added before it is given items, otherwise an exception is thrown (is this documented?) + foreach (OLVGroup group in groups) { + group.InsertGroupOldStyle(this); + group.SetItemsOldStyle(); + } + } + + /// + /// For some reason, UseItemStyleForSubItems doesn't work for the colors + /// when owner drawing the list, so we have to specifically give each subitem + /// the desired colors + /// + /// The item whose subitems are to be corrected + /// Cells drawn via BaseRenderer don't need this, but it is needed + /// when an owner drawn cell uses DrawDefault=true + protected virtual void CorrectSubItemColors(ListViewItem olvi) { + } + + /// + /// Fill in the given OLVListItem with values of the given row + /// + /// the OLVListItem that is to be stuff with values + /// the model object from which values will be taken + protected virtual void FillInValues(OLVListItem lvi, object rowObject) { + if (this.Columns.Count == 0) + return; + + OLVListSubItem subItem = this.MakeSubItem(rowObject, this.GetColumn(0)); + lvi.SubItems[0] = subItem; + lvi.ImageSelector = subItem.ImageSelector; + + // Give the item the same font/colors as the control + lvi.Font = this.Font; + lvi.BackColor = this.BackColor; + lvi.ForeColor = this.ForeColor; + + // Should the row be selectable? + lvi.Enabled = !this.IsDisabled(rowObject); + + // Only Details and Tile views have subitems + switch (this.View) { + case View.Details: + for (int i = 1; i < this.Columns.Count; i++) { + lvi.SubItems.Add(this.MakeSubItem(rowObject, this.GetColumn(i))); + } + break; + case View.Tile: + for (int i = 1; i < this.Columns.Count; i++) { + OLVColumn column = this.GetColumn(i); + if (column.IsTileViewColumn) + lvi.SubItems.Add(this.MakeSubItem(rowObject, column)); + } + break; + } + + // Should the row be selectable? + if (!lvi.Enabled) + { + lvi.UseItemStyleForSubItems = false; + ApplyRowStyle(lvi, this.DisabledItemStyle ?? ObjectListView.DefaultDisabledItemStyle, false); + } + + // Set the check state of the row, if we are showing check boxes + if (this.CheckBoxes) { + CheckState? state = this.GetCheckState(lvi.RowObject); + lvi.CheckState = state ?? CheckState.Unchecked; + } + + // Give the RowFormatter a chance to mess with the item + if (this.RowFormatter != null) { + this.RowFormatter(lvi); + } + } + + private OLVListSubItem MakeSubItem(object rowObject, OLVColumn column) { + object cellValue = column.GetValue(rowObject); + OLVListSubItem subItem = new OLVListSubItem(cellValue, + column.ValueToString(cellValue), + column.GetImage(rowObject)); + if (this.UseHyperlinks && column.Hyperlink) { + IsHyperlinkEventArgs args = new IsHyperlinkEventArgs(); + args.ListView = this; + args.Model = rowObject; + args.Column = column; + args.Text = subItem.Text; + args.Url = subItem.Text; + args.IsHyperlink = !this.IsDisabled(rowObject); + this.OnIsHyperlink(args); + subItem.Url = args.IsHyperlink ? args.Url : null; + } + + return subItem; + } + + private void ApplyHyperlinkStyle(OLVListItem olvi) { + olvi.UseItemStyleForSubItems = false; + + // If subitem 0 is given a back color, the item back color is changed too. + // So we have to remember it here so we can used it even if subitem 0 is changed. + Color itemBackColor = olvi.BackColor; + + for (int i = 0; i < this.Columns.Count; i++) { + OLVListSubItem subItem = olvi.GetSubItem(i); + if (subItem == null) + continue; + OLVColumn column = this.GetColumn(i); + subItem.BackColor = itemBackColor; + if (column.Hyperlink && !String.IsNullOrEmpty(subItem.Url)) { + this.ApplyCellStyle(olvi, i, this.IsUrlVisited(subItem.Url) ? this.HyperlinkStyle.Visited : this.HyperlinkStyle.Normal); + } + } + } + + + /// + /// Make sure the ListView has the extended style that says to display subitem images. + /// + /// This method must be called after any .NET call that update the extended styles + /// since they seem to erase this setting. + protected virtual void ForceSubItemImagesExStyle() { + // Virtual lists can't show subitem images natively, so don't turn on this flag + if (!this.VirtualMode) + NativeMethods.ForceSubItemImagesExStyle(this); + } + + /// + /// Convert the given image selector to an index into our image list. + /// Return -1 if that's not possible + /// + /// + /// Index of the image in the imageList, or -1 + protected virtual int GetActualImageIndex(Object imageSelector) { + if (imageSelector == null) + return -1; + + if (imageSelector is Int32) + return (int)imageSelector; + + String imageSelectorAsString = imageSelector as String; + if (imageSelectorAsString != null && this.SmallImageList != null) + return this.SmallImageList.Images.IndexOfKey(imageSelectorAsString); + + return -1; + } + + /// + /// Return the tooltip that should be shown when the mouse is hovered over the given column + /// + /// The column index whose tool tip is to be fetched + /// A string or null if no tool tip is to be shown + public virtual String GetHeaderToolTip(int columnIndex) { + OLVColumn column = this.GetColumn(columnIndex); + if (column == null) + return null; + String tooltip = column.ToolTipText; + if (this.HeaderToolTipGetter != null) + tooltip = this.HeaderToolTipGetter(column); + return tooltip; + } + + /// + /// Return the tooltip that should be shown when the mouse is hovered over the given cell + /// + /// The column index whose tool tip is to be fetched + /// The row index whose tool tip is to be fetched + /// A string or null if no tool tip is to be shown + public virtual String GetCellToolTip(int columnIndex, int rowIndex) { + if (this.CellToolTipGetter != null) + return this.CellToolTipGetter(this.GetColumn(columnIndex), this.GetModelObject(rowIndex)); + + // Show the URL in the tooltip if it's different to the text + if (columnIndex >= 0) { + OLVListSubItem subItem = this.GetSubItem(rowIndex, columnIndex); + if (subItem != null && !String.IsNullOrEmpty(subItem.Url) && subItem.Url != subItem.Text && + this.HotCellHitLocation == HitTestLocation.Text) + return subItem.Url; + } + + return null; + } + + /// + /// Return the OLVListItem that displays the given model object + /// + /// The modelObject whose item is to be found + /// The OLVListItem that displays the model, or null + /// This method has O(n) performance. + public virtual OLVListItem ModelToItem(object modelObject) { + if (modelObject == null) + return null; + + foreach (OLVListItem olvi in this.Items) { + if (olvi.RowObject != null && olvi.RowObject.Equals(modelObject)) + return olvi; + } + return null; + } + + /// + /// Do the work required after the items in a listview have been created + /// + protected virtual void PostProcessRows() { + // If this method is called during a BeginUpdate/EndUpdate pair, changes to the + // Items collection are cached. Getting the Count flushes that cache. +#pragma warning disable 168 +// ReSharper disable once UnusedVariable + int count = this.Items.Count; +#pragma warning restore 168 + + int i = 0; + if (this.ShowGroups) { + foreach (ListViewGroup group in this.Groups) { + foreach (OLVListItem olvi in group.Items) { + this.PostProcessOneRow(olvi.Index, i, olvi); + i++; + } + } + } else { + foreach (OLVListItem olvi in this.Items) { + this.PostProcessOneRow(olvi.Index, i, olvi); + i++; + } + } + } + + /// + /// Do the work required after one item in a listview have been created + /// + protected virtual void PostProcessOneRow(int rowIndex, int displayIndex, OLVListItem olvi) { + if (this.UseAlternatingBackColors && this.View == View.Details && olvi.Enabled) { + olvi.BackColor = displayIndex % 2 == 1 ? this.AlternateRowBackColorOrDefault : this.BackColor; + } + if (this.ShowImagesOnSubItems && !this.VirtualMode) { + this.SetSubItemImages(rowIndex, olvi); + } + if (this.UseHyperlinks) { + this.ApplyHyperlinkStyle(olvi); + } + this.TriggerFormatRowEvent(rowIndex, displayIndex, olvi); + } + + /// + /// Prepare the listview to show alternate row backcolors + /// + /// We cannot rely on lvi.Index in this method. + /// In a straight list, lvi.Index is the display index, and can be used to determine + /// whether the row should be colored. But when organised by groups, lvi.Index is not + /// useable because it still refers to the position in the overall list, not the display order. + /// + [Obsolete("This method is no longer used. Override PostProcessOneRow() to achieve a similar result")] + protected virtual void PrepareAlternateBackColors() { + } + + /// + /// Setup all subitem images on all rows + /// + [Obsolete("This method is not longer maintained and will be removed", false)] + protected virtual void SetAllSubItemImages() { + //if (!this.ShowImagesOnSubItems || this.OwnerDraw) + // return; + + //this.ForceSubItemImagesExStyle(); + + //for (int rowIndex = 0; rowIndex < this.GetItemCount(); rowIndex++) + // SetSubItemImages(rowIndex, this.GetItem(rowIndex)); + } + + /// + /// Tell the underlying list control which images to show against the subitems + /// + /// the index at which the item occurs + /// the item whose subitems are to be set + protected virtual void SetSubItemImages(int rowIndex, OLVListItem item) { + this.SetSubItemImages(rowIndex, item, false); + } + + /// + /// Tell the underlying list control which images to show against the subitems + /// + /// the index at which the item occurs + /// the item whose subitems are to be set + /// will existing images be cleared if no new image is provided? + protected virtual void SetSubItemImages(int rowIndex, OLVListItem item, bool shouldClearImages) { + if (!this.ShowImagesOnSubItems || this.OwnerDraw) + return; + + for (int i = 1; i < item.SubItems.Count; i++) { + this.SetSubItemImage(rowIndex, i, item.GetSubItem(i), shouldClearImages); + } + } + + /// + /// Set the subitem image natively + /// + /// + /// + /// + /// + public virtual void SetSubItemImage(int rowIndex, int subItemIndex, OLVListSubItem subItem, bool shouldClearImages) { + int imageIndex = this.GetActualImageIndex(subItem.ImageSelector); + if (shouldClearImages || imageIndex != -1) + NativeMethods.SetSubItemImage(this, rowIndex, subItemIndex, imageIndex); + } + + /// + /// Take ownership of the 'objects' collection. This separats our collection from the source. + /// + /// + /// + /// This method + /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject + /// calls will modify our collection and not the original colleciton. + /// + /// + /// This method has the intentional side-effect of converting our list of objects to an ArrayList. + /// + /// + protected virtual void TakeOwnershipOfObjects() { + if (this.isOwnerOfObjects) + return; + + this.isOwnerOfObjects = true; + + this.objects = ObjectListView.EnumerableToArray(this.objects, true); + } + + /// + /// Trigger FormatRow and possibly FormatCell events for the given item + /// + /// + /// + /// + protected virtual void TriggerFormatRowEvent(int rowIndex, int displayIndex, OLVListItem olvi) { + FormatRowEventArgs args = new FormatRowEventArgs(); + args.ListView = this; + args.RowIndex = rowIndex; + args.DisplayIndex = displayIndex; + args.Item = olvi; + args.UseCellFormatEvents = this.UseCellFormatEvents; + this.OnFormatRow(args); + + if (args.UseCellFormatEvents && this.View == View.Details) { + // If a cell isn't given its own color, it should use the color of the item. + // However, there is a bug in the .NET framework where the cell are given + // the color of the ListView instead. So we have to explicitly give each + // cell the fore and back colors that it should have. + olvi.UseItemStyleForSubItems = false; + Color backColor = olvi.BackColor; + Color foreColor = olvi.ForeColor; + foreach (OLVListSubItem subitem in olvi.SubItems) { + subitem.BackColor = backColor; + subitem.ForeColor = foreColor; + } + + // Fire one event per cell + FormatCellEventArgs args2 = new FormatCellEventArgs(); + args2.ListView = this; + args2.RowIndex = rowIndex; + args2.DisplayIndex = displayIndex; + args2.Item = olvi; + for (int i = 0; i < this.Columns.Count; i++) { + args2.ColumnIndex = i; + args2.Column = this.GetColumn(i); + args2.SubItem = olvi.GetSubItem(i); + this.OnFormatCell(args2); + } + } + } + + /// + /// Make the list forget everything -- all rows and all columns + /// + /// Use if you want to remove just the rows. + public virtual void Reset() { + this.Clear(); + this.AllColumns.Clear(); + this.ClearObjects(); + this.PrimarySortColumn = null; + this.SecondarySortColumn = null; + this.ClearDisabledObjects(); + this.ClearPersistentCheckState(); + this.ClearUrlVisited(); + this.ClearHotItem(); + } + + + #endregion + + #region ISupportInitialize Members + + void ISupportInitialize.BeginInit() { + this.Frozen = true; + } + + void ISupportInitialize.EndInit() { + if (this.RowHeight != -1) { + this.SmallImageList = this.SmallImageList; + if (this.CheckBoxes) + this.InitializeStateImageList(); + } + + if (this.UseCustomSelectionColors) + this.EnableCustomSelectionColors(); + + if (this.UseSubItemCheckBoxes || (this.VirtualMode && this.CheckBoxes)) + this.SetupSubItemCheckBoxes(); + + this.Frozen = false; + } + + #endregion + + #region Image list manipulation + + /// + /// Update our externally visible image list so it holds the same images as our shadow list, but sized correctly + /// + private void SetupBaseImageList() { + // If a row height hasn't been set, or an image list has been give which is the required size, just assign it + if (rowHeight == -1 || + this.View != View.Details || + (this.shadowedImageList != null && this.shadowedImageList.ImageSize.Height == rowHeight)) + this.BaseSmallImageList = this.shadowedImageList; + else { + int width = (this.shadowedImageList == null ? 16 : this.shadowedImageList.ImageSize.Width); + this.BaseSmallImageList = this.MakeResizedImageList(width, rowHeight, shadowedImageList); + } + } + + /// + /// Return a copy of the given source image list, where each image has been resized to be height x height in size. + /// If source is null, an empty image list of the given size is returned + /// + /// Height and width of the new images + /// Height and width of the new images + /// Source of the images (can be null) + /// A new image list + private ImageList MakeResizedImageList(int width, int height, ImageList source) { + ImageList il = new ImageList(); + il.ImageSize = new Size(width, height); + + // If there's nothing to copy, just return the new list + if (source == null) + return il; + + il.TransparentColor = source.TransparentColor; + il.ColorDepth = source.ColorDepth; + + // Fill the imagelist with resized copies from the source + for (int i = 0; i < source.Images.Count; i++) { + Bitmap bm = this.MakeResizedImage(width, height, source.Images[i], source.TransparentColor); + il.Images.Add(bm); + } + + // Give each image the same key it has in the original + foreach (String key in source.Images.Keys) { + il.Images.SetKeyName(source.Images.IndexOfKey(key), key); + } + + return il; + } + + /// + /// Return a bitmap of the given height x height, which shows the given image, centred. + /// + /// Height and width of new bitmap + /// Height and width of new bitmap + /// Image to be centred + /// The background color + /// A new bitmap + private Bitmap MakeResizedImage(int width, int height, Image image, Color transparent) { + Bitmap bm = new Bitmap(width, height); + Graphics g = Graphics.FromImage(bm); + g.Clear(transparent); + int x = Math.Max(0, (bm.Size.Width - image.Size.Width) / 2); + int y = Math.Max(0, (bm.Size.Height - image.Size.Height) / 2); + g.DrawImage(image, x, y, image.Size.Width, image.Size.Height); + return bm; + } + + /// + /// Initialize the state image list with the required checkbox images + /// + protected virtual void InitializeStateImageList() { + if (this.DesignMode) + return; + + if (!this.CheckBoxes) + return; + + if (this.StateImageList == null) { + this.StateImageList = new ImageList(); + this.StateImageList.ImageSize = new Size(16, this.RowHeight == -1 ? 16 : this.RowHeight); + this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; + } + + if (this.RowHeight != -1 && + this.View == View.Details && + this.StateImageList.ImageSize.Height != this.RowHeight) { + this.StateImageList = new ImageList(); + this.StateImageList.ImageSize = new Size(16, this.RowHeight); + this.StateImageList.ColorDepth = ColorDepth.Depth32Bit; + } + + // The internal logic of ListView cycles through the state images when the primary + // checkbox is clicked. So we have to get exactly the right number of images in the + // image list. + if (this.StateImageList.Images.Count == 0) + this.AddCheckStateBitmap(this.StateImageList, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); + if (this.StateImageList.Images.Count <= 1) + this.AddCheckStateBitmap(this.StateImageList, CHECKED_KEY, CheckBoxState.CheckedNormal); + if (this.TriStateCheckBoxes && this.StateImageList.Images.Count <= 2) + this.AddCheckStateBitmap(this.StateImageList, INDETERMINATE_KEY, CheckBoxState.MixedNormal); + else { + if (this.StateImageList.Images.ContainsKey(INDETERMINATE_KEY)) + this.StateImageList.Images.RemoveByKey(INDETERMINATE_KEY); + } + } + + /// + /// The name of the image used when a check box is checked + /// + public const string CHECKED_KEY = "checkbox-checked"; + + /// + /// The name of the image used when a check box is unchecked + /// + public const string UNCHECKED_KEY = "checkbox-unchecked"; + + /// + /// The name of the image used when a check box is Indeterminate + /// + public const string INDETERMINATE_KEY = "checkbox-indeterminate"; + + /// + /// Setup this control so it can display check boxes on subitems + /// (or primary checkboxes in virtual mode) + /// + /// This gives the ListView a small image list, if it doesn't already have one. + public virtual void SetupSubItemCheckBoxes() { + this.ShowImagesOnSubItems = true; + if (this.SmallImageList == null || !this.SmallImageList.Images.ContainsKey(CHECKED_KEY)) + this.InitializeSubItemCheckBoxImages(); + } + + /// + /// Make sure the small image list for this control has checkbox images + /// (used for sub-item checkboxes). + /// + /// + /// + /// This gives the ListView a small image list, if it doesn't already have one. + /// + /// + /// ObjectListView has to manage checkboxes on subitems separate from the checkboxes on each row. + /// The underlying ListView knows about the per-row checkboxes, and to make them work, OLV has to + /// correctly configure the StateImageList. However, the ListView cannot do checkboxes in subitems, + /// so ObjectListView has to handle them in a differnt fashion. So, per-row checkboxes are controlled + /// by images in the StateImageList, but per-cell checkboxes are handled by images in the SmallImageList. + /// + /// + protected virtual void InitializeSubItemCheckBoxImages() { + // Don't mess with the image list in design mode + if (this.DesignMode) + return; + + ImageList il = this.SmallImageList; + if (il == null) { + il = new ImageList(); + il.ImageSize = new Size(16, 16); + il.ColorDepth = ColorDepth.Depth32Bit; + } + + this.AddCheckStateBitmap(il, CHECKED_KEY, CheckBoxState.CheckedNormal); + this.AddCheckStateBitmap(il, UNCHECKED_KEY, CheckBoxState.UncheckedNormal); + this.AddCheckStateBitmap(il, INDETERMINATE_KEY, CheckBoxState.MixedNormal); + + this.SmallImageList = il; + } + + private void AddCheckStateBitmap(ImageList il, string key, CheckBoxState boxState) { + Bitmap b = new Bitmap(il.ImageSize.Width, il.ImageSize.Height); + Graphics g = Graphics.FromImage(b); + g.Clear(il.TransparentColor); + Point location = new Point(b.Width / 2 - 5, b.Height / 2 - 6); + CheckBoxRenderer.DrawCheckBox(g, location, boxState); + il.Images.Add(key, b); + } + + #endregion + + #region Owner drawing + + /// + /// Owner draw the column header + /// + /// + protected override void OnDrawColumnHeader(DrawListViewColumnHeaderEventArgs e) { + e.DrawDefault = true; + base.OnDrawColumnHeader(e); + } + + /// + /// Owner draw the item + /// + /// + protected override void OnDrawItem(DrawListViewItemEventArgs e) { + if (this.View == View.Details) + e.DrawDefault = false; + else { + if (this.ItemRenderer == null) + e.DrawDefault = true; + else { + Object row = ((OLVListItem)e.Item).RowObject; + e.DrawDefault = !this.ItemRenderer.RenderItem(e, e.Graphics, e.Bounds, row); + } + } + + if (e.DrawDefault) + base.OnDrawItem(e); + } + + /// + /// Owner draw a single subitem + /// + /// + protected override void OnDrawSubItem(DrawListViewSubItemEventArgs e) { + //System.Diagnostics.Debug.WriteLine(String.Format("OnDrawSubItem ({0}, {1})", e.ItemIndex, e.ColumnIndex)); + // Don't try to do owner drawing at design time + if (this.DesignMode) { + e.DrawDefault = true; + return; + } + + // Calculate where the subitem should be drawn + Rectangle r = e.Bounds; + + // Get the special renderer for this column. If there isn't one, use the default draw mechanism. + OLVColumn column = this.GetColumn(e.ColumnIndex); + IRenderer renderer = column.Renderer ?? this.DefaultRenderer; + + // Get a graphics context for the renderer to use. + // But we have more complications. Virtual lists have a nasty habit of drawing column 0 + // whenever there is any mouse move events over a row, and doing it in an un-double-buffered manner, + // which results in nasty flickers! There are also some unbuffered draw when a mouse is first + // hovered over column 0 of a normal row. So, to avoid all complications, + // we always manually double-buffer the drawing. + // Except with Mono, which doesn't seem to handle double buffering at all :-( + BufferedGraphics buffer = BufferedGraphicsManager.Current.Allocate(e.Graphics, r); + Graphics g = buffer.Graphics; + + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + + // Finally, give the renderer a chance to draw something + e.DrawDefault = !renderer.RenderSubItem(e, g, r, ((OLVListItem)e.Item).RowObject); + + if (!e.DrawDefault) + buffer.Render(); + buffer.Dispose(); + } + + #endregion + + #region OnEvent Handling + + /// + /// We need the click count in the mouse up event, but that is always 1. + /// So we have to remember the click count from the preceding mouse down event. + /// + /// + protected override void OnMouseDown(MouseEventArgs e) { + this.lastMouseDownClickCount = e.Clicks; + base.OnMouseDown(e); + } + private int lastMouseDownClickCount; + + /// + /// When the mouse leaves the control, remove any hot item highlighting + /// + /// + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + + if (!this.Created) + return; + + this.UpdateHotItem(new Point(-1,-1)); + } + + // We could change the hot item on the mouse hover event, but it looks wrong. + + //protected override void OnMouseHover(EventArgs e) { + // System.Diagnostics.Debug.WriteLine(String.Format("OnMouseHover")); + // base.OnMouseHover(e); + // this.UpdateHotItem(this.PointToClient(Cursor.Position)); + //} + + /// + /// When the mouse moves, we might need to change the hot item. + /// + /// + protected override void OnMouseMove(MouseEventArgs e) { + base.OnMouseMove(e); + + if (this.Created) + HandleMouseMove(e.Location); + } + + internal void HandleMouseMove(Point pt) { + + // System.Diagnostics.Debug.WriteLine(String.Format("HandleMouseMove: {0}", pt)); + + CellOverEventArgs args = new CellOverEventArgs(); + this.BuildCellEvent(args, pt); + this.OnCellOver(args); + this.MouseMoveHitTest = args.HitTest; + + if (!args.Handled) + this.UpdateHotItem(args.HitTest); + } + + /// + /// Check to see if we need to start editing a cell + /// + /// + protected override void OnMouseUp(MouseEventArgs e) { + base.OnMouseUp(e); + + if (!this.Created) + return; + + if (e.Button == MouseButtons.Right && !this.fakeRightClick) { + this.OnRightMouseUp(e); + return; + } + + this.fakeRightClick = false; + + // What event should we listen for to start cell editing? + // ------------------------------------------------------ + // + // We can't use OnMouseClick, OnMouseDoubleClick, OnClick, or OnDoubleClick + // since they are not triggered for clicks on subitems without Full Row Select. + // + // We could use OnMouseDown, but selecting rows is done in OnMouseUp. This means + // that if we start the editing during OnMouseDown, the editor will automatically + // lose focus when mouse up happens. + // + + // Tell the world about a cell click. If someone handles it, don't do anything else + CellClickEventArgs args = new CellClickEventArgs(); + this.BuildCellEvent(args, e.Location); + args.ClickCount = this.lastMouseDownClickCount; + this.OnCellClick(args); + if (args.Handled) + return; + + // Did the user click a hyperlink? + if (this.UseHyperlinks && + args.HitTest.HitTestLocation == HitTestLocation.Text && + args.SubItem != null && + !String.IsNullOrEmpty(args.SubItem.Url)) { + // We have to delay the running of this process otherwise we can generate + // a series of MouseUp events (don't ask me why) + this.BeginInvoke((MethodInvoker)delegate { this.ProcessHyperlinkClicked(args); }); + } + + // No one handled it so check to see if we should start editing. + if (!this.ShouldStartCellEdit(e)) + return; + + // We only start the edit if the user clicked on the image or text. + if (args.HitTest.HitTestLocation == HitTestLocation.Nothing) + return; + + // We don't edit the primary column by single clicks -- only subitems. + if (this.CellEditActivation == CellEditActivateMode.SingleClick && args.ColumnIndex <= 0) + return; + + // Don't start a cell edit operation when the user clicks on the background of a checkbox column -- it just looks wrong. + // If the user clicks on the actual checkbox, changing the checkbox state is handled elsewhere. + if (args.Column != null && args.Column.CheckBoxes) + return; + + this.EditSubItem(args.Item, args.ColumnIndex); + } + + /// + /// Tell the world that a hyperlink was clicked and if the event isn't handled, + /// do the default processing. + /// + /// + protected virtual void ProcessHyperlinkClicked(CellClickEventArgs e) { + HyperlinkClickedEventArgs args = new HyperlinkClickedEventArgs(); + args.HitTest = e.HitTest; + args.ListView = this; + args.Location = new Point(-1, -1); + args.Item = e.Item; + args.SubItem = e.SubItem; + args.Model = e.Model; + args.ColumnIndex = e.ColumnIndex; + args.Column = e.Column; + args.RowIndex = e.RowIndex; + args.ModifierKeys = Control.ModifierKeys; + args.Url = e.SubItem.Url; + this.OnHyperlinkClicked(args); + if (!args.Handled) { + this.StandardHyperlinkClickedProcessing(args); + } + } + + /// + /// Do the default processing for a hyperlink clicked event, which + /// is to try and open the url. + /// + /// + protected virtual void StandardHyperlinkClickedProcessing(HyperlinkClickedEventArgs args) { + Cursor originalCursor = this.Cursor; + try { + this.Cursor = Cursors.WaitCursor; + System.Diagnostics.Process.Start(args.Url); + } catch (Win32Exception) { + System.Media.SystemSounds.Beep.Play(); + // ignore it + } finally { + this.Cursor = originalCursor; + } + this.MarkUrlVisited(args.Url); + this.RefreshHotItem(); + } + + /// + /// The user right clicked on the control + /// + /// + protected virtual void OnRightMouseUp(MouseEventArgs e) { + CellRightClickEventArgs args = new CellRightClickEventArgs(); + this.BuildCellEvent(args, e.Location); + this.OnCellRightClick(args); + if (!args.Handled) { + if (args.MenuStrip != null) { + args.MenuStrip.Show(this, args.Location); + } + } + } + + internal void BuildCellEvent(CellEventArgs args, Point location) { + OlvListViewHitTestInfo hitTest = this.OlvHitTest(location.X, location.Y); + args.HitTest = hitTest; + args.ListView = this; + args.Location = location; + args.Item = hitTest.Item; + args.SubItem = hitTest.SubItem; + args.Model = hitTest.RowObject; + args.ColumnIndex = hitTest.ColumnIndex; + args.Column = hitTest.Column; + if (hitTest.Item != null) + args.RowIndex = hitTest.Item.Index; + args.ModifierKeys = Control.ModifierKeys; + + // In non-details view, we want any hit on an item to act as if it was a hit + // on column 0 -- which, effectively, it was. + if (args.Item != null && args.ListView.View != View.Details) { + args.ColumnIndex = 0; + args.Column = args.ListView.GetColumn(0); + args.SubItem = args.Item.GetSubItem(0); + } + } + + /// + /// This method is called every time a row is selected or deselected. This can be + /// a pain if the user shift-clicks 100 rows. We override this method so we can + /// trigger one event for any number of select/deselects that come from one user action + /// + /// + protected override void OnSelectedIndexChanged(EventArgs e) { + if (this.SelectionEventsSuspended) + return; + + base.OnSelectedIndexChanged(e); + + // If we haven't already scheduled an event, schedule it to be triggered + // By using idle time, we will wait until all select events for the same + // user action have finished before triggering the event. + if (!this.hasIdleHandler) { + this.hasIdleHandler = true; + this.RunWhenIdle(HandleApplicationIdle); + } + } + + /// + /// Called when the handle of the underlying control is created + /// + /// + protected override void OnHandleCreated(EventArgs e) { + base.OnHandleCreated(e); + + this.Invoke((MethodInvoker)this.OnControlCreated); + } + + /// + /// This method is called after the control has been fully created. + /// + protected virtual void OnControlCreated() { + + // Force the header control to be created when the listview handle is + HeaderControl hc = this.HeaderControl; + hc.WordWrap = this.HeaderWordWrap; + + // Make sure any overlays that are set on the hot item style take effect + this.HotItemStyle = this.HotItemStyle; + + // Arrange for any group images to be installed after the control is created + NativeMethods.SetGroupImageList(this, this.GroupImageList); + + this.UseExplorerTheme = this.UseExplorerTheme; + + this.RememberDisplayIndicies(); + this.SetGroupSpacing(); + + if (this.VirtualMode) + this.ApplyExtendedStyles(); + } + + #endregion + + #region Cell editing + + /// + /// Should we start editing the cell in response to the given mouse button event? + /// + /// + /// + protected virtual bool ShouldStartCellEdit(MouseEventArgs e) { + if (this.IsCellEditing) + return false; + + if (e.Button != MouseButtons.Left && !(e.Button == MouseButtons.Right && this.fakeRightClick)) + return false; + + if ((Control.ModifierKeys & (Keys.Shift | Keys.Control | Keys.Alt)) != 0) + return false; + + if (this.lastMouseDownClickCount == 1 && this.CellEditActivation == CellEditActivateMode.SingleClick) + return true; + + return (this.lastMouseDownClickCount == 2 && this.CellEditActivation == CellEditActivateMode.DoubleClick); + } + + /// + /// Handle a key press on this control. We specifically look for F2 which edits the primary column, + /// or a Tab character during an edit operation, which tries to start editing on the next (or previous) cell. + /// + /// + /// + protected override bool ProcessDialogKey(Keys keyData) { + + if (this.IsCellEditing) + return this.CellEditKeyEngine.HandleKey(this, keyData); + + // Treat F2 as a request to edit the primary column + if (keyData == Keys.F2) { + this.EditSubItem((OLVListItem)this.FocusedItem, 0); + return base.ProcessDialogKey(keyData); + } + + // Treat Ctrl-C as Copy To Clipboard. + if (this.CopySelectionOnControlC && keyData == (Keys.C | Keys.Control)) { + this.CopySelectionToClipboard(); + return true; + } + + // Treat Ctrl-A as Select All. + if (this.SelectAllOnControlA && keyData == (Keys.A | Keys.Control)) { + this.SelectAll(); + return true; + } + + return base.ProcessDialogKey(keyData); + } + + /// + /// Start an editing operation on the first editable column of the given model. + /// + /// + /// + /// + /// If the model doesn't exist, or there are no editable columns, this method + /// will do nothing. + /// + /// This will start an edit operation regardless of CellActivationMode. + /// + /// + public virtual void EditModel(object rowModel) { + OLVListItem olvItem = this.ModelToItem(rowModel); + if (olvItem == null) + return; + + for (int i = 0; i < olvItem.SubItems.Count; i++) { + if (this.GetColumn(i).IsEditable) { + this.StartCellEdit(olvItem, i); + return; + } + } + } + + /// + /// Begin an edit operation on the given cell. + /// + /// This performs various sanity checks and passes off the real work to StartCellEdit(). + /// The row to be edited + /// The index of the cell to be edited + public virtual void EditSubItem(OLVListItem item, int subItemIndex) { + if (item == null) + return; + + if (subItemIndex < 0 && subItemIndex >= item.SubItems.Count) + return; + + if (this.CellEditActivation == CellEditActivateMode.None) + return; + + if (!this.GetColumn(subItemIndex).IsEditable) + return; + + if (!item.Enabled) + return; + + this.StartCellEdit(item, subItemIndex); + } + + /// + /// Really start an edit operation on a given cell. The parameters are assumed to be sane. + /// + /// The row to be edited + /// The index of the cell to be edited + public virtual void StartCellEdit(OLVListItem item, int subItemIndex) { + OLVColumn column = this.GetColumn(subItemIndex); + Control c = this.GetCellEditor(item, subItemIndex); + Rectangle r = this.CalculateCellEditorBounds(item, subItemIndex, c.PreferredSize); + c.Bounds = r; + + // Try to align the control as the column is aligned. Not all controls support this property + Munger.PutProperty(c, "TextAlign", column.TextAlign); + + // Give the control the value from the model + this.SetControlValue(c, column.GetValue(item.RowObject), column.GetStringValue(item.RowObject)); + + // Give the outside world the chance to munge with the process + this.CellEditEventArgs = new CellEditEventArgs(column, c, r, item, subItemIndex); + this.OnCellEditStarting(this.CellEditEventArgs); + if (this.CellEditEventArgs.Cancel) + return; + + // The event handler may have completely changed the control, so we need to remember it + this.cellEditor = this.CellEditEventArgs.Control; + + // If the control isn't the height of the cell, centre it vertically. + // We don't do this in OwnerDrawn mode since the renderer already aligns the control correctly. + // We also dont need to do this when in Tile view. + if (this.View != View.Tile && !this.OwnerDraw && this.cellEditor.Height != r.Height) { + this.cellEditor.Top += (r.Height - this.cellEditor.Height) / 2; + } + this.Invalidate(); + this.Controls.Add(this.cellEditor); + this.ConfigureControl(); + this.PauseAnimations(true); + } + private Control cellEditor; + internal CellEditEventArgs CellEditEventArgs; + + /// + /// Calculate the bounds of the edit control for the given item/column + /// + /// + /// + /// + /// + public Rectangle CalculateCellEditorBounds(OLVListItem item, int subItemIndex, Size preferredSize) { + Rectangle r = this.View == View.Details + ? item.GetSubItemBounds(subItemIndex) + : this.GetItemRect(item.Index, ItemBoundsPortion.Label); + return this.OwnerDraw + ? CalculateCellEditorBoundsOwnerDrawn(item, subItemIndex, r, preferredSize) + : CalculateCellEditorBoundsStandard(item, subItemIndex, r, preferredSize); + } + + /// + /// Calculate the bounds of the edit control for the given item/column, when the listview + /// is being owner drawn. + /// + /// + /// + /// + /// + /// A rectangle that is the bounds of the cell editor + protected Rectangle CalculateCellEditorBoundsOwnerDrawn(OLVListItem item, int subItemIndex, Rectangle r, Size preferredSize) { + IRenderer renderer = this.View == View.Details + ? (this.GetColumn(subItemIndex).Renderer ?? this.DefaultRenderer) + : this.ItemRenderer; + + if (renderer == null) + return r; + + using (Graphics g = this.CreateGraphics()) { + return renderer.GetEditRectangle(g, r, item, subItemIndex, preferredSize); + } + } + + /// + /// Calculate the bounds of the edit control for the given item/column, when the listview + /// is not being owner drawn. + /// + /// + /// + /// + /// + /// A rectangle that is the bounds of the cell editor + protected Rectangle CalculateCellEditorBoundsStandard(OLVListItem item, int subItemIndex, Rectangle cellBounds, Size preferredSize) { + if (this.View != View.Details) + return cellBounds; + + // Allow for image (if there is one). + int offset = 0; + object imageSelector = null; + if (subItemIndex == 0) + imageSelector = item.ImageSelector; + else { + // We only check for subitem images if we are owner drawn or showing subitem images + if (this.OwnerDraw || this.ShowImagesOnSubItems) + imageSelector = item.GetSubItem(subItemIndex).ImageSelector; + } + if (this.GetActualImageIndex(imageSelector) != -1) { + offset += this.SmallImageSize.Width + 2; + } + + // Allow for checkbox + if (this.CheckBoxes && this.StateImageList != null && subItemIndex == 0) { + offset += this.StateImageList.ImageSize.Width + 2; + } + + // Allow for indent (first column only) + if (subItemIndex == 0 && item.IndentCount > 0) { + offset += (this.SmallImageSize.Width * item.IndentCount); + } + + // Do the adjustment + if (offset > 0) { + cellBounds.X += offset; + cellBounds.Width -= offset; + } + + return cellBounds; + } + + /// + /// Try to give the given value to the provided control. Fall back to assigning a string + /// if the value assignment fails. + /// + /// A control + /// The value to be given to the control + /// The string to be given if the value doesn't work + protected virtual void SetControlValue(Control control, Object value, String stringValue) { + // Handle combobox explicitly + ComboBox cb = control as ComboBox; + if (cb != null) { + if (cb.Created) + cb.SelectedValue = value; + else + this.BeginInvoke(new MethodInvoker(delegate { + cb.SelectedValue = value; + })); + return; + } + + if (Munger.PutProperty(control, "Value", value)) + return; + + // There wasn't a Value property, or we couldn't set it, so set the text instead + try + { + String valueAsString = value as String; + control.Text = valueAsString ?? stringValue; + } + catch (ArgumentOutOfRangeException) { + // The value couldn't be set via the Text property. + } + } + + /// + /// Setup the given control to be a cell editor + /// + protected virtual void ConfigureControl() { + this.cellEditor.Validating += new CancelEventHandler(CellEditor_Validating); + this.cellEditor.Select(); + } + + /// + /// Return the value that the given control is showing + /// + /// + /// + protected virtual Object GetControlValue(Control control) { + if (control == null) + return null; + + TextBox box = control as TextBox; + if (box != null) + return box.Text; + + ComboBox comboBox = control as ComboBox; + if (comboBox != null) + return comboBox.SelectedValue; + + CheckBox checkBox = control as CheckBox; + if (checkBox != null) + return checkBox.Checked; + + try { + return control.GetType().InvokeMember("Value", BindingFlags.GetProperty, null, control, null); + } catch (MissingMethodException) { // Microsoft throws this + return control.Text; + } catch (MissingFieldException) { // Mono throws this + return control.Text; + } + } + + /// + /// Called when the cell editor could be about to lose focus. Time to commit the change + /// + /// + /// + protected virtual void CellEditor_Validating(object sender, CancelEventArgs e) { + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditorValidating(this.CellEditEventArgs); + + if (this.CellEditEventArgs.Cancel) { + this.CellEditEventArgs.Control.Select(); + e.Cancel = true; + } else + FinishCellEdit(); + } + + /// + /// Return the bounds of the given cell + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Rectangle + public virtual Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex) { + + // TODO: Check if this is the same thing as OLVListItem.GetSubItemBounds() ? + + // We use ItemBoundsPortion.Label rather than ItemBoundsPortion.Item + // since Label extends to the right edge of the cell, whereas Item gives just the + // current text width. + return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.Label); + } + + /// + /// Return the bounds of the given cell only until the edge of the current text + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Rectangle + public virtual Rectangle CalculateCellTextBounds(OLVListItem item, int subItemIndex) { + return this.CalculateCellBounds(item, subItemIndex, ItemBoundsPortion.ItemOnly); + } + + private Rectangle CalculateCellBounds(OLVListItem item, int subItemIndex, ItemBoundsPortion portion) { + // SubItem.Bounds works for every subitem, except the first. + if (subItemIndex > 0) + return item.SubItems[subItemIndex].Bounds; + + // For non detail views, we just use the requested portion + Rectangle r = this.GetItemRect(item.Index, portion); + if (r.Y < -10000000 || r.Y > 10000000) { + r.Y = item.Bounds.Y; + } + if (this.View != View.Details) + return r; + + // Finding the bounds of cell 0 should not be a difficult task, but it is. Problems: + // 1) item.SubItem[0].Bounds is always the full bounds of the entire row, not just cell 0. + // 2) if column 0 has been dragged to some other position, the bounds always has a left edge of 0. + + // We avoid both these problems by using the position of sides the column header to calculate + // the sides of the cell + Point sides = NativeMethods.GetScrolledColumnSides(this, 0); + r.X = sides.X + 4; + r.Width = sides.Y - sides.X - 5; + + return r; + } + + /// + /// Calculate the visible bounds of the given column. The column's bottom edge is + /// either the bottom of the last row or the bottom of the control. + /// + /// The bounds of the control itself + /// The column + /// A Rectangle + /// This returns an empty rectnage if the control isn't in Details mode, + /// OR has doesn't have any rows, OR if the given column is hidden. + public virtual Rectangle CalculateColumnVisibleBounds(Rectangle bounds, OLVColumn column) + { + // Sanity checks + if (column == null || + this.View != System.Windows.Forms.View.Details || + this.GetItemCount() == 0 || + !column.IsVisible) + return Rectangle.Empty; + + Point sides = NativeMethods.GetScrolledColumnSides(this, column.Index); + if (sides.X == -1) + return Rectangle.Empty; + + Rectangle columnBounds = new Rectangle(sides.X, bounds.Top, sides.Y - sides.X, bounds.Bottom); + + // Find the bottom of the last item. The column only extends to there. + OLVListItem lastItem = this.GetLastItemInDisplayOrder(); + if (lastItem != null) + { + Rectangle lastItemBounds = lastItem.Bounds; + if (!lastItemBounds.IsEmpty && lastItemBounds.Bottom < columnBounds.Bottom) + columnBounds.Height = lastItemBounds.Bottom - columnBounds.Top; + } + + return columnBounds; + } + + /// + /// Return a control that can be used to edit the value of the given cell. + /// + /// The row to be edited + /// The index of the cell to be edited + /// A Control to edit the given cell + protected virtual Control GetCellEditor(OLVListItem item, int subItemIndex) { + OLVColumn column = this.GetColumn(subItemIndex); + Object value = column.GetValue(item.RowObject) ?? this.GetFirstNonNullValue(column); + + // TODO: What do we do if value is still null here? + + // Ask the registry for an instance of the appropriate editor. + // Use a default editor if the registry can't create one for us. + Control editor = ObjectListView.EditorRegistry.GetEditor(item.RowObject, column, value) ?? + this.MakeDefaultCellEditor(column); + + return editor; + } + + /// + /// Get the first non-null value of the given column. + /// At most 1000 rows will be considered. + /// + /// + /// The first non-null value, or null if no non-null values were found + internal object GetFirstNonNullValue(OLVColumn column) { + for (int i = 0; i < Math.Min(this.GetItemCount(), 1000); i++) { + object value = column.GetValue(this.GetModelObject(i)); + if (value != null) + return value; + } + return null; + } + + /// + /// Return a TextBox that can be used as a default cell editor. + /// + /// What column does the cell belong to? + /// + protected virtual Control MakeDefaultCellEditor(OLVColumn column) { + TextBox tb = new TextBox(); + if (column.AutoCompleteEditor) + this.ConfigureAutoComplete(tb, column); + return tb; + } + + /// + /// Configure the given text box to autocomplete unique values + /// from the given column. At most 1000 rows will be considered. + /// + /// The textbox to configure + /// The column used to calculate values + public void ConfigureAutoComplete(TextBox tb, OLVColumn column) { + this.ConfigureAutoComplete(tb, column, 1000); + } + + + /// + /// Configure the given text box to autocomplete unique values + /// from the given column. At most 1000 rows will be considered. + /// + /// The textbox to configure + /// The column used to calculate values + /// Consider only this many rows + public void ConfigureAutoComplete(TextBox tb, OLVColumn column, int maxRows) { + // Don't consider more rows than we actually have + maxRows = Math.Min(this.GetItemCount(), maxRows); + + // Reset any existing autocomplete + tb.AutoCompleteCustomSource.Clear(); + + // CONSIDER: Should we use ClusteringStrategy here? + + // Build a list of unique values, to be used as autocomplete on the editor + Dictionary alreadySeen = new Dictionary(); + List values = new List(); + for (int i = 0; i < maxRows; i++) { + string valueAsString = column.GetStringValue(this.GetModelObject(i)); + if (!String.IsNullOrEmpty(valueAsString) && !alreadySeen.ContainsKey(valueAsString)) { + values.Add(valueAsString); + alreadySeen[valueAsString] = true; + } + } + + tb.AutoCompleteCustomSource.AddRange(values.ToArray()); + tb.AutoCompleteSource = AutoCompleteSource.CustomSource; + tb.AutoCompleteMode = column.AutoCompleteEditorMode; + } + + /// + /// Stop editing a cell and throw away any changes. + /// + public virtual void CancelCellEdit() { + if (!this.IsCellEditing) + return; + + // Let the world know that the user has cancelled the edit operation + this.CellEditEventArgs.Cancel = true; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditFinishing(this.CellEditEventArgs); + + // Now cleanup the editing process + this.CleanupCellEdit(false, this.CellEditEventArgs.AutoDispose); + } + + /// + /// If a cell edit is in progress, finish the edit. + /// + /// Returns false if the finishing process was cancelled + /// (i.e. the cell editor is still on screen) + /// This method does not guarantee that the editing will finish. The validation + /// process can cause the finishing to be aborted. Developers should check the return value + /// or use IsCellEditing property after calling this method to see if the user is still + /// editing a cell. + public virtual bool PossibleFinishCellEditing() { + return this.PossibleFinishCellEditing(false); + } + + /// + /// If a cell edit is in progress, finish the edit. + /// + /// Returns false if the finishing process was cancelled + /// (i.e. the cell editor is still on screen) + /// This method does not guarantee that the editing will finish. The validation + /// process can cause the finishing to be aborted. Developers should check the return value + /// or use IsCellEditing property after calling this method to see if the user is still + /// editing a cell. + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + public virtual bool PossibleFinishCellEditing(bool expectingCellEdit) { + if (!this.IsCellEditing) + return true; + + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditorValidating(this.CellEditEventArgs); + + if (this.CellEditEventArgs.Cancel) + return false; + + this.FinishCellEdit(expectingCellEdit); + + return true; + } + + /// + /// Finish the cell edit operation, writing changed data back to the model object + /// + /// This method does not trigger a Validating event, so it always finishes + /// the cell edit. + public virtual void FinishCellEdit() { + this.FinishCellEdit(false); + } + + /// + /// Finish the cell edit operation, writing changed data back to the model object + /// + /// This method does not trigger a Validating event, so it always finishes + /// the cell edit. + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + public virtual void FinishCellEdit(bool expectingCellEdit) { + if (!this.IsCellEditing) + return; + + this.CellEditEventArgs.Cancel = false; + this.CellEditEventArgs.NewValue = this.GetControlValue(this.cellEditor); + this.OnCellEditFinishing(this.CellEditEventArgs); + + // If someone doesn't cancel the editing process, write the value back into the model + if (!this.CellEditEventArgs.Cancel) { + this.CellEditEventArgs.Column.PutValue(this.CellEditEventArgs.RowObject, this.CellEditEventArgs.NewValue); + this.RefreshItem(this.CellEditEventArgs.ListViewItem); + } + + this.CleanupCellEdit(expectingCellEdit, this.CellEditEventArgs.AutoDispose); + } + + /// + /// Remove all trace of any existing cell edit operation + /// + /// True if it is likely that another cell is going to be + /// edited immediately after this cell finishes editing + /// True if the cell editor should be disposed + protected virtual void CleanupCellEdit(bool expectingCellEdit, bool disposeOfCellEditor) { + if (this.cellEditor == null) + return; + + this.cellEditor.Validating -= new CancelEventHandler(CellEditor_Validating); + + Control soonToBeOldCellEditor = this.cellEditor; + this.cellEditor = null; + + // Delay cleaning up the cell editor so that if we are immediately going to + // start a new cell edit (because the user pressed Tab) the new cell editor + // has a chance to grab the focus. Without this, the ListView gains focus + // momentarily (after the cell editor is remove and before the new one is created) + // causing the list's selection to flash momentarily. + EventHandler toBeRun = null; + toBeRun = delegate(object sender, EventArgs e) { + Application.Idle -= toBeRun; + this.Controls.Remove(soonToBeOldCellEditor); + if (disposeOfCellEditor) + soonToBeOldCellEditor.Dispose(); + this.Invalidate(); + + if (!this.IsCellEditing) { + if (this.Focused) + this.Select(); + this.PauseAnimations(false); + } + }; + + // We only want to delay the removal of the control if we are expecting another cell + // to be edited. Otherwise, we remove the control immediately. + if (expectingCellEdit) + this.RunWhenIdle(toBeRun); + else + toBeRun(null, null); + } + + #endregion + + #region Hot row and cell handling + + /// + /// Force the hot item to be recalculated + /// + public virtual void ClearHotItem() { + this.UpdateHotItem(new Point(-1, -1)); + } + + /// + /// Force the hot item to be recalculated + /// + public virtual void RefreshHotItem() { + this.UpdateHotItem(this.PointToClient(Cursor.Position)); + } + + /// + /// The mouse has moved to the given pt. See if the hot item needs to be updated + /// + /// Where is the mouse? + /// This is the main entry point for hot item handling + protected virtual void UpdateHotItem(Point pt) { + this.UpdateHotItem(this.OlvHitTest(pt.X, pt.Y)); + } + /// + /// The mouse has moved to the given pt. See if the hot item needs to be updated + /// + /// + /// This is the main entry point for hot item handling + protected virtual void UpdateHotItem(OlvListViewHitTestInfo hti) { + + if (!this.UseHotItem && !this.UseHyperlinks) + return; + + int newHotRow = hti.RowIndex; + int newHotColumn = hti.ColumnIndex; + HitTestLocation newHotCellHitLocation = hti.HitTestLocation; + HitTestLocationEx newHotCellHitLocationEx = hti.HitTestLocationEx; + OLVGroup newHotGroup = hti.Group; + + // In non-details view, we treat any hit on a row as if it were a hit + // on column 0 -- which (effectively) it is! + if (newHotRow >= 0 && this.View != View.Details) + newHotColumn = 0; + + if (this.HotRowIndex == newHotRow && + this.HotColumnIndex == newHotColumn && + this.HotCellHitLocation == newHotCellHitLocation && + this.HotCellHitLocationEx == newHotCellHitLocationEx && + this.HotGroup == newHotGroup) + return; + + // Trigger the hotitem changed event + HotItemChangedEventArgs args = new HotItemChangedEventArgs(); + args.HotCellHitLocation = newHotCellHitLocation; + args.HotCellHitLocationEx = newHotCellHitLocationEx; + args.HotColumnIndex = newHotColumn; + args.HotRowIndex = newHotRow; + args.HotGroup = newHotGroup; + args.OldHotCellHitLocation = this.HotCellHitLocation; + args.OldHotCellHitLocationEx = this.HotCellHitLocationEx; + args.OldHotColumnIndex = this.HotColumnIndex; + args.OldHotRowIndex = this.HotRowIndex; + args.OldHotGroup = this.HotGroup; + this.OnHotItemChanged(args); + + // Update the state of the control + this.HotRowIndex = newHotRow; + this.HotColumnIndex = newHotColumn; + this.HotCellHitLocation = newHotCellHitLocation; + this.HotCellHitLocationEx = newHotCellHitLocationEx; + this.HotGroup = newHotGroup; + + // If the event handler handled it complete, don't do anything else + if (args.Handled) + return; + + this.BeginUpdate(); + try { + this.Invalidate(); + if (args.OldHotRowIndex != -1) + this.UnapplyHotItem(args.OldHotRowIndex); + + if (this.HotRowIndex != -1) { + // Virtual lists apply hot item style when fetching their rows + if (this.VirtualMode) + { + this.ClearCachedInfo(); + this.RedrawItems(this.HotRowIndex, this.HotRowIndex, true); + } + else + this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, hti.Item); + } + + if (this.UseHotItem && this.HotItemStyle != null && this.HotItemStyle.Overlay != null) { + this.RefreshOverlays(); + } + } finally { + this.EndUpdate(); + } + } + + /// + /// Update the given row using the current hot item information + /// + /// + protected virtual void UpdateHotRow(OLVListItem olvi) { + this.UpdateHotRow(this.HotRowIndex, this.HotColumnIndex, this.HotCellHitLocation, olvi); + } + + /// + /// Update the given row using the given hot item information + /// + /// + /// + /// + /// + protected virtual void UpdateHotRow(int rowIndex, int columnIndex, HitTestLocation hitLocation, OLVListItem olvi) { + if (rowIndex < 0 || columnIndex < 0) + return; + + if (this.UseHyperlinks) { + OLVColumn column = this.GetColumn(columnIndex); + OLVListSubItem subItem = olvi.GetSubItem(columnIndex); + if (column.Hyperlink && hitLocation == HitTestLocation.Text && !String.IsNullOrEmpty(subItem.Url)) { + this.ApplyCellStyle(olvi, columnIndex, this.HyperlinkStyle.Over); + this.Cursor = this.HyperlinkStyle.OverCursor ?? Cursors.Default; + } else { + this.Cursor = Cursors.Default; + } + } + + if (this.UseHotItem) { + if (!olvi.Selected && olvi.Enabled) { + this.ApplyRowStyle(olvi, this.HotItemStyle, !this.FullRowSelect); + } + } + } + + /// + /// Apply a style to the given row + /// + /// + /// + /// + protected virtual void ApplyRowStyle(OLVListItem olvi, IItemStyle style, bool primaryColumnOnly) { + if (style == null) + return; + + if (!primaryColumnOnly || this.View != View.Details) { + Font font = style.Font ?? olvi.Font; + + if (style.FontStyle != FontStyle.Regular) + font = new Font(font ?? this.Font, style.FontStyle); + + if (!Equals(font, olvi.Font)) { + if (olvi.UseItemStyleForSubItems) + olvi.Font = font; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) { + x.Font = font; + } + } + } + + if (!style.ForeColor.IsEmpty) { + if (olvi.UseItemStyleForSubItems) + olvi.ForeColor = style.ForeColor; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) { + x.ForeColor = style.ForeColor; + } + } + } + + if (!style.BackColor.IsEmpty) { + if (olvi.UseItemStyleForSubItems) + olvi.BackColor = style.BackColor; + else { + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) { + x.BackColor = style.BackColor; + } + } + } + } else { + olvi.UseItemStyleForSubItems = false; + + foreach (ListViewItem.ListViewSubItem x in olvi.SubItems) { + x.BackColor = style.BackColor.IsEmpty ? olvi.BackColor : style.BackColor; + x.ForeColor = style.ForeColor.IsEmpty ? olvi.ForeColor : style.ForeColor; + } + + this.ApplyCellStyle(olvi, 0, style); + } + } + + /// + /// Apply a style to a cell + /// + /// + /// + /// + protected virtual void ApplyCellStyle(OLVListItem olvi, int columnIndex, IItemStyle style) { + if (style == null) + return; + + // Don't apply formatting to subitems when not in Details view + if (this.View != View.Details && columnIndex > 0) + return; + + olvi.UseItemStyleForSubItems = false; + + ListViewItem.ListViewSubItem subItem = olvi.SubItems[columnIndex]; + if (style.Font != null) + subItem.Font = style.Font; + + if (style.FontStyle != FontStyle.Regular) + subItem.Font = new Font(subItem.Font ?? olvi.Font ?? this.Font, style.FontStyle); + + if (!style.ForeColor.IsEmpty) + subItem.ForeColor = style.ForeColor; + + if (!style.BackColor.IsEmpty) + subItem.BackColor = style.BackColor; + } + + /// + /// Remove hot item styling from the given row + /// + /// + protected virtual void UnapplyHotItem(int index) { + this.Cursor = Cursors.Default; + // Virtual lists will apply the appropriate formatting when the row is fetched + if (this.VirtualMode) { + if (index < this.VirtualListSize) + this.RedrawItems(index, index, true); + } else { + OLVListItem olvi = this.GetItem(index); + if (olvi != null) { + //this.PostProcessOneRow(index, index, olvi); + this.RefreshItem(olvi); + } + } + } + + + #endregion + + #region Drag and drop + + /// + /// + /// + /// + protected override void OnItemDrag(ItemDragEventArgs e) { + base.OnItemDrag(e); + + if (this.DragSource == null) + return; + + Object data = this.DragSource.StartDrag(this, e.Button, (OLVListItem)e.Item); + if (data != null) { + DragDropEffects effect = this.DoDragDrop(data, this.DragSource.GetAllowedEffects(data)); + this.DragSource.EndDrag(data, effect); + } + } + + /// + /// + /// + /// + protected override void OnDragEnter(DragEventArgs args) { + base.OnDragEnter(args); + + if (this.DropSink != null) + this.DropSink.Enter(args); + } + + /// + /// + /// + /// + protected override void OnDragOver(DragEventArgs args) { + base.OnDragOver(args); + + if (this.DropSink != null) + this.DropSink.Over(args); + } + + /// + /// + /// + /// + protected override void OnDragDrop(DragEventArgs args) { + base.OnDragDrop(args); + + if (this.DropSink != null) + this.DropSink.Drop(args); + } + + /// + /// + /// + /// + protected override void OnDragLeave(EventArgs e) { + base.OnDragLeave(e); + + if (this.DropSink != null) + this.DropSink.Leave(); + } + + /// + /// + /// + /// + protected override void OnGiveFeedback(GiveFeedbackEventArgs args) { + base.OnGiveFeedback(args); + + if (this.DropSink != null) + this.DropSink.GiveFeedback(args); + } + + /// + /// + /// + /// + protected override void OnQueryContinueDrag(QueryContinueDragEventArgs args) { + base.OnQueryContinueDrag(args); + + if (this.DropSink != null) + this.DropSink.QueryContinue(args); + } + + #endregion + + #region Decorations and Overlays + + /// + /// Add the given decoration to those on this list and make it appear + /// + /// The decoration + /// + /// A decoration scrolls with the listview. An overlay stays fixed in place. + /// + public virtual void AddDecoration(IDecoration decoration) { + if (decoration == null) + return; + this.Decorations.Add(decoration); + this.Invalidate(); + } + + /// + /// Add the given overlay to those on this list and make it appear + /// + /// The overlay + public virtual void AddOverlay(IOverlay overlay) { + if (overlay == null) + return; + this.Overlays.Add(overlay); + this.Invalidate(); + } + + /// + /// Draw all the decorations + /// + /// A Graphics + /// The items that were redrawn and whose decorations should also be redrawn + protected virtual void DrawAllDecorations(Graphics g, List itemsThatWereRedrawn) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + + Rectangle contentRectangle = this.ContentRectangle; + + if (this.HasEmptyListMsg && this.GetItemCount() == 0) { + this.EmptyListMsgOverlay.Draw(this, g, contentRectangle); + } + + // Let the drop sink draw whatever feedback it likes + if (this.DropSink != null) { + this.DropSink.DrawFeedback(g, contentRectangle); + } + + // Draw our item and subitem decorations + foreach (OLVListItem olvi in itemsThatWereRedrawn) { + if (olvi.HasDecoration) { + foreach (IDecoration d in olvi.Decorations) { + d.ListItem = olvi; + d.SubItem = null; + d.Draw(this, g, contentRectangle); + } + } + foreach (OLVListSubItem subItem in olvi.SubItems) { + if (subItem.HasDecoration) { + foreach (IDecoration d in subItem.Decorations) { + d.ListItem = olvi; + d.SubItem = subItem; + d.Draw(this, g, contentRectangle); + } + } + } + if (this.SelectedRowDecoration != null && olvi.Selected && olvi.Enabled) { + this.SelectedRowDecoration.ListItem = olvi; + this.SelectedRowDecoration.SubItem = null; + this.SelectedRowDecoration.Draw(this, g, contentRectangle); + } + } + + // Now draw the specifically registered decorations + foreach (IDecoration decoration in this.Decorations) { + decoration.ListItem = null; + decoration.SubItem = null; + decoration.Draw(this, g, contentRectangle); + } + + // Finally, draw any hot item decoration + if (this.UseHotItem && this.HotItemStyle != null && this.HotItemStyle.Decoration != null) { + IDecoration hotItemDecoration = this.HotItemStyle.Decoration; + hotItemDecoration.ListItem = this.GetItem(this.HotRowIndex); + if (hotItemDecoration.ListItem == null || hotItemDecoration.ListItem.Enabled) + { + hotItemDecoration.SubItem = hotItemDecoration.ListItem == null ? null : hotItemDecoration.ListItem.GetSubItem(this.HotColumnIndex); + hotItemDecoration.Draw(this, g, contentRectangle); + } + } + + // If we are in design mode, we don't want to use the glass panels, + // so we draw the background overlays here + if (this.DesignMode) { + foreach (IOverlay overlay in this.Overlays) { + overlay.Draw(this, g, contentRectangle); + } + } + } + + /// + /// Is the given decoration shown on this list + /// + /// The overlay + public virtual bool HasDecoration(IDecoration decoration) { + return this.Decorations.Contains(decoration); + } + + /// + /// Is the given overlay shown on this list? + /// + /// The overlay + public virtual bool HasOverlay(IOverlay overlay) { + return this.Overlays.Contains(overlay); + } + + /// + /// Hide any overlays. + /// + /// + /// This is only a temporary hiding -- the overlays will be shown + /// the next time the ObjectListView redraws. + /// + public virtual void HideOverlays() { + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.HideGlass(); + } + } + + /// + /// Create and configure the empty list msg overlay + /// + protected virtual void InitializeEmptyListMsgOverlay() { + TextOverlay overlay = new TextOverlay(); + overlay.Alignment = System.Drawing.ContentAlignment.MiddleCenter; + overlay.TextColor = SystemColors.ControlDarkDark; + overlay.BackColor = Color.BlanchedAlmond; + overlay.BorderColor = SystemColors.ControlDark; + overlay.BorderWidth = 2.0f; + this.EmptyListMsgOverlay = overlay; + } + + /// + /// Initialize the standard image and text overlays + /// + protected virtual void InitializeStandardOverlays() { + this.OverlayImage = new ImageOverlay(); + this.AddOverlay(this.OverlayImage); + this.OverlayText = new TextOverlay(); + this.AddOverlay(this.OverlayText); + } + + /// + /// Make sure that any overlays are visible. + /// + public virtual void ShowOverlays() { + // If we shouldn't show overlays, then don't create glass panels + if (!this.ShouldShowOverlays()) + return; + + // Make sure that each overlay has its own glass panels + if (this.Overlays.Count != this.glassPanels.Count) { + foreach (IOverlay overlay in this.Overlays) { + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel == null) { + glassPanel = new GlassPanelForm(); + glassPanel.Bind(this, overlay); + this.glassPanels.Add(glassPanel); + } + } + } + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.ShowGlass(); + } + } + + private bool ShouldShowOverlays() { + // If we are in design mode, we dont show the overlays + if (this.DesignMode) + return false; + + // If we are explicitly not using overlays, also don't show them + if (!this.UseOverlays) + return false; + + // If there are no overlays, guess... + if (!this.HasOverlays) + return false; + + // If we don't have 32-bit display, alpha blending doesn't work, so again, no overlays + // TODO: This should actually figure out which screen(s) the control is on, and make sure + // that each one is 32-bit. + if (Screen.PrimaryScreen.BitsPerPixel < 32) + return false; + + // Finally, we can show the overlays + return true; + } + + private GlassPanelForm FindGlassPanelForOverlay(IOverlay overlay) { + return this.glassPanels.Find(delegate(GlassPanelForm x) { return x.Overlay == overlay; }); + } + + /// + /// Refresh the display of the overlays + /// + public virtual void RefreshOverlays() { + foreach (GlassPanelForm glassPanel in this.glassPanels) { + glassPanel.Invalidate(); + } + } + + /// + /// Refresh the display of just one overlays + /// + public virtual void RefreshOverlay(IOverlay overlay) { + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel != null) + glassPanel.Invalidate(); + } + + /// + /// Remove the given decoration from this list + /// + /// The decoration to remove + public virtual void RemoveDecoration(IDecoration decoration) { + if (decoration == null) + return; + this.Decorations.Remove(decoration); + this.Invalidate(); + } + + /// + /// Remove the given overlay to those on this list + /// + /// The overlay + public virtual void RemoveOverlay(IOverlay overlay) { + if (overlay == null) + return; + this.Overlays.Remove(overlay); + GlassPanelForm glassPanel = this.FindGlassPanelForOverlay(overlay); + if (glassPanel != null) { + this.glassPanels.Remove(glassPanel); + glassPanel.Unbind(); + glassPanel.Dispose(); + } + } + + #endregion + + #region Filtering + + /// + /// Create a filter that will enact all the filtering currently installed + /// on the visible columns. + /// + public virtual IModelFilter CreateColumnFilter() { + List filters = new List(); + foreach (OLVColumn column in this.Columns) { + IModelFilter filter = column.ValueBasedFilter; + if (filter != null) + filters.Add(filter); + } + return (filters.Count == 0) ? null : new CompositeAllFilter(filters); + } + + /// + /// Do the actual work of filtering + /// + /// + /// + /// + /// + virtual protected IEnumerable FilterObjects(IEnumerable originalObjects, IModelFilter aModelFilter, IListFilter aListFilter) { + // Being cautious + originalObjects = originalObjects ?? new ArrayList(); + + // Tell the world to filter the objects. If they do so, don't do anything else +// ReSharper disable PossibleMultipleEnumeration + FilterEventArgs args = new FilterEventArgs(originalObjects); + this.OnFilter(args); + if (args.FilteredObjects != null) + return args.FilteredObjects; + + // Apply a filter to the list as a whole + if (aListFilter != null) + originalObjects = aListFilter.Filter(originalObjects); + + // Apply the object filter if there is one + if (aModelFilter != null) { + ArrayList filteredObjects = new ArrayList(); + foreach (object model in originalObjects) { + if (aModelFilter.Filter(model)) + filteredObjects.Add(model); + } + originalObjects = filteredObjects; + } + + return originalObjects; +// ReSharper restore PossibleMultipleEnumeration + } + + /// + /// Remove all column filtering. + /// + public virtual void ResetColumnFiltering() { + foreach (OLVColumn column in this.Columns) { + column.ValuesChosenForFiltering.Clear(); + } + this.UpdateColumnFiltering(); + } + + /// + /// Update the filtering of this ObjectListView based on the value filtering + /// defined in each column + /// + public virtual void UpdateColumnFiltering() { + //List filters = new List(); + //IModelFilter columnFilter = this.CreateColumnFilter(); + //if (columnFilter != null) + // filters.Add(columnFilter); + //if (this.AdditionalFilter != null) + // filters.Add(this.AdditionalFilter); + //this.ModelFilter = filters.Count == 0 ? null : new CompositeAllFilter(filters); + + if (this.AdditionalFilter == null) + this.ModelFilter = this.CreateColumnFilter(); + else { + IModelFilter columnFilter = this.CreateColumnFilter(); + if (columnFilter == null) + this.ModelFilter = this.AdditionalFilter; + else { + List filters = new List(); + filters.Add(columnFilter); + filters.Add(this.AdditionalFilter); + this.ModelFilter = new CompositeAllFilter(filters); + } + } + } + + /// + /// When some setting related to filtering changes, this method is called. + /// + protected virtual void UpdateFiltering() { + this.BuildList(true); + } + + #endregion + + #region Persistent check state + + /// + /// Gets the checkedness of the given model. + /// + /// The model + /// The checkedness of the model. Defaults to unchecked. + protected virtual CheckState GetPersistentCheckState(object model) { + CheckState state; + if (model != null && this.CheckStateMap.TryGetValue(model, out state)) + return state; + return CheckState.Unchecked; + } + + /// + /// Remember the check state of the given model object + /// + /// The model to be remembered + /// The model's checkedness + /// The state given to the method + protected virtual CheckState SetPersistentCheckState(object model, CheckState state) { + if (model == null) + return CheckState.Unchecked; + + this.CheckStateMap[model] = state; + return state; + } + + /// + /// Forget any persistent checkbox state + /// + protected virtual void ClearPersistentCheckState() { + this.CheckStateMap = null; + } + + #endregion + + #region Implementation variables + + private bool isOwnerOfObjects; // does this ObjectListView own the Objects collection? + private bool hasIdleHandler; // has an Idle handler already been installed? + private bool hasResizeColumnsHandler; // has an idle handler been installed which will handle column resizing? + private bool isInWmPaintEvent; // is a WmPaint event currently being handled? + private bool shouldDoCustomDrawing; // should the list do its custom drawing? + private bool isMarqueSelecting; // Is a marque selection in progress? + private int suspendSelectionEventCount; // How many unmatched SuspendSelectionEvents() calls have been made? + + private readonly List glassPanels = new List(); // The transparent panel that draws overlays + private Dictionary visitedUrlMap = new Dictionary(); // Which urls have been visited? + + // TODO + //private CheckBoxSettings checkBoxSettings = new CheckBoxSettings(); + + #endregion + } +} diff --git a/ObjectListView/ObjectListView.shfb b/ObjectListView/ObjectListView.shfb new file mode 100644 index 0000000..2b89efc --- /dev/null +++ b/ObjectListView/ObjectListView.shfb @@ -0,0 +1,47 @@ + + + + + + + All ObjectListView appears in this namespace + + + ObjectListViewDemo demonstrates helpful techniques when using an ObjectListView + Summary, Parameter, Returns, AutoDocumentCtors, Namespace + InheritedMembers, Protected, SealedProtected + + + .\Help\ + + + True + True + HtmlHelp1x + True + False + 2.0.50727 + True + False + True + False + + ObjectListView Reference + Documentation + en-US + + (c) Copyright 2006-2008 Phillip Piper All Rights Reserved + phillip_piper@bigfoot.com + + + Local + Msdn + Blank + Prototype + Guid + CSharp + False + AboveNamespaces + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2005.csproj b/ObjectListView/ObjectListView2005.csproj new file mode 100644 index 0000000..cfae568 --- /dev/null +++ b/ObjectListView/ObjectListView2005.csproj @@ -0,0 +1,178 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + true + olv-keyfile.snk + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + DEBUG;TRACE + prompt + 4 + false + + + + + + + + + + + + + + Component + + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2008.csproj b/ObjectListView/ObjectListView2008.csproj new file mode 100644 index 0000000..fb038d2 --- /dev/null +++ b/ObjectListView/ObjectListView2008.csproj @@ -0,0 +1,188 @@ + + + Debug + AnyCPU + 9.0.21022 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 2.0 + v2.0 + true + olv-keyfile.snk + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + + + + + pdbonly + true + bin\Release\ + DEBUG;TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + Component + + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2008.ncrunchproject b/ObjectListView/ObjectListView2008.ncrunchproject new file mode 100644 index 0000000..17f8118 --- /dev/null +++ b/ObjectListView/ObjectListView2008.ncrunchproject @@ -0,0 +1,16 @@ + + false + false + false + false + false + true + true + false + true + true + 60000 + + + AutoDetect + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2010.csproj b/ObjectListView/ObjectListView2010.csproj new file mode 100644 index 0000000..f478fa7 --- /dev/null +++ b/ObjectListView/ObjectListView2010.csproj @@ -0,0 +1,188 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 3.5 + v2.0 + true + olv-keyfile.snk + + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + bin\Debug\ObjectListView.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + Component + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + Component + + + + Component + + + + + + + Component + + + Component + + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2010.ncrunchproject b/ObjectListView/ObjectListView2010.ncrunchproject new file mode 100644 index 0000000..b4ca671 --- /dev/null +++ b/ObjectListView/ObjectListView2010.ncrunchproject @@ -0,0 +1,27 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + + + .* + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.csproj b/ObjectListView/ObjectListView2012.csproj new file mode 100644 index 0000000..94bfbca --- /dev/null +++ b/ObjectListView/ObjectListView2012.csproj @@ -0,0 +1,201 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {18FEDA0C-D147-4286-B39A-01204808106A} + Library + Properties + BrightIdeasSoftware + ObjectListView + + + + + 3.5 + v2.0 + true + olv-keyfile.snk + + + + + + + + + + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + false + bin\Debug\ObjectListView.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + bin\Release\ObjectListView.XML + + + bin\Remote Debug\ + + + + + + + + + + + + + + Component + + + + + + + + Component + + + + + + + + + Component + + + True + True + Resources.resx + + + + + + + Component + + + + + + + Component + + + Component + + + + + + Component + + + + + + + Component + + + Component + + + Form + + + ColumnSelectionForm.cs + + + + Form + + + + Code + + + + + + + + Component + + + + + + Component + + + Component + + + + Component + + + + + + + Component + + + + + + + Designer + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + ColumnSelectionForm.cs + Designer + + + + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.csproj.DotSettings b/ObjectListView/ObjectListView2012.csproj.DotSettings new file mode 100644 index 0000000..9794177 --- /dev/null +++ b/ObjectListView/ObjectListView2012.csproj.DotSettings @@ -0,0 +1,2 @@ + + False \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.ncrunchproject b/ObjectListView/ObjectListView2012.ncrunchproject new file mode 100644 index 0000000..896f219 --- /dev/null +++ b/ObjectListView/ObjectListView2012.ncrunchproject @@ -0,0 +1,22 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.nuspec b/ObjectListView/ObjectListView2012.nuspec new file mode 100644 index 0000000..7dc78e9 --- /dev/null +++ b/ObjectListView/ObjectListView2012.nuspec @@ -0,0 +1,19 @@ + + + + ObjectListView.Official + ObjectListView (Official) + 2.8.0 + Phillip Piper + Phillip Piper + http://www.gnu.org/licenses/gpl.html + http://objectlistview.sourceforge.net + http://objectlistview.sourceforge.net/cs/_static/index-icon.png + true + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. + ObjectListView is a .NET ListView wired on caffeine, guarana and steroids. More calmly, it is a C# wrapper around a .NET ListView, which makes the ListView much easier to use and teaches it lots of neat new tricks. + V2.8 added the ability to disable rows, and to have checkboxes in column headers + Copyright 2006-2014 Bright Ideas Software + .Net WinForms Net20 Net40 ListView Controls + + \ No newline at end of file diff --git a/ObjectListView/ObjectListView2012.v2.ncrunchproject b/ObjectListView/ObjectListView2012.v2.ncrunchproject new file mode 100644 index 0000000..896f219 --- /dev/null +++ b/ObjectListView/ObjectListView2012.v2.ncrunchproject @@ -0,0 +1,22 @@ + + false + false + false + true + false + false + false + false + true + true + false + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/ObjectListView/Properties/AssemblyInfo.cs b/ObjectListView/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ea40d7b --- /dev/null +++ b/ObjectListView/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ObjectListView")] +[assembly: AssemblyDescription("A much easier to use ListView and friends")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Bright Ideas Software")] +[assembly: AssemblyProduct("ObjectListView")] +[assembly: AssemblyCopyright("Copyright © 2006-2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("ef28c7a8-77ae-442d-abc3-bb023fa31e57")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("2.8.0.*")] +[assembly: AssemblyFileVersion("2.8.0.0")] +[assembly: System.CLSCompliant(true)] diff --git a/ObjectListView/Properties/Resources.Designer.cs b/ObjectListView/Properties/Resources.Designer.cs new file mode 100644 index 0000000..3e76360 --- /dev/null +++ b/ObjectListView/Properties/Resources.Designer.cs @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.1 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BrightIdeasSoftware.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BrightIdeasSoftware.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static System.Drawing.Bitmap ClearFiltering { + get { + object obj = ResourceManager.GetObject("ClearFiltering", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + internal static System.Drawing.Bitmap ColumnFilterIndicator { + get { + object obj = ResourceManager.GetObject("ColumnFilterIndicator", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + internal static System.Drawing.Bitmap Filtering { + get { + object obj = ResourceManager.GetObject("Filtering", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + internal static System.Drawing.Bitmap SortAscending { + get { + object obj = ResourceManager.GetObject("SortAscending", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + internal static System.Drawing.Bitmap SortDescending { + get { + object obj = ResourceManager.GetObject("SortDescending", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/ObjectListView/Properties/Resources.resx b/ObjectListView/Properties/Resources.resx new file mode 100644 index 0000000..b017d6a --- /dev/null +++ b/ObjectListView/Properties/Resources.resx @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\clear-filter.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + + ..\Resources\filter-icons3.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\filter.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\sort-ascending.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\sort-descending.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/ObjectListView/Rendering/Adornments.cs b/ObjectListView/Rendering/Adornments.cs new file mode 100644 index 0000000..e6bb92b --- /dev/null +++ b/ObjectListView/Rendering/Adornments.cs @@ -0,0 +1,743 @@ +/* + * Adornments - Adornments are the basis for overlays and decorations -- things that can be rendered over the top of a ListView + * + * Author: Phillip Piper + * Date: 16/08/2009 1:02 AM + * + * Change log: + * v2.6 + * 2012-08-18 JPP - Correctly dispose of brush and pen resources + * v2.3 + * 2009-09-22 JPP - Added Wrap property to TextAdornment, to allow text wrapping to be disabled + * - Added ShrinkToWidth property to ImageAdornment + * 2009-08-17 JPP - Initial version + * + * To do: + * - Use IPointLocator rather than Corners + * - Add RotationCenter property ratherr than always using middle center + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; + +namespace BrightIdeasSoftware +{ + /// + /// An adorment is the common base for overlays and decorations. + /// + public class GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the corner of the adornment that will be positioned at the reference corner + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public System.Drawing.ContentAlignment AdornmentCorner { + get { return this.adornmentCorner; } + set { this.adornmentCorner = value; } + } + private System.Drawing.ContentAlignment adornmentCorner = System.Drawing.ContentAlignment.MiddleCenter; + + /// + /// Gets or sets location within the reference rectange where the adornment will be drawn + /// + /// This is a simplied interface to ReferenceCorner and AdornmentCorner + [Category("ObjectListView"), + Description("How will the adornment be aligned"), + DefaultValue(System.Drawing.ContentAlignment.BottomRight), + NotifyParentProperty(true)] + public System.Drawing.ContentAlignment Alignment { + get { return this.alignment; } + set { + this.alignment = value; + this.ReferenceCorner = value; + this.AdornmentCorner = value; + } + } + private System.Drawing.ContentAlignment alignment = System.Drawing.ContentAlignment.BottomRight; + + /// + /// Gets or sets the offset by which the position of the adornment will be adjusted + /// + [Category("ObjectListView"), + Description("The offset by which the position of the adornment will be adjusted"), + DefaultValue(typeof(Size), "0,0")] + public Size Offset { + get { return this.offset; } + set { this.offset = value; } + } + private Size offset = new Size(); + + /// + /// Gets or sets the point of the reference rectangle to which the adornment will be aligned. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public System.Drawing.ContentAlignment ReferenceCorner { + get { return this.referenceCorner; } + set { this.referenceCorner = value; } + } + private System.Drawing.ContentAlignment referenceCorner = System.Drawing.ContentAlignment.MiddleCenter; + + /// + /// Gets or sets the degree of rotation by which the adornment will be transformed. + /// The centre of rotation will be the center point of the adornment. + /// + [Category("ObjectListView"), + Description("The degree of rotation that will be applied to the adornment."), + DefaultValue(0), + NotifyParentProperty(true)] + public int Rotation { + get { return this.rotation; } + set { this.rotation = value; } + } + private int rotation; + + /// + /// Gets or sets the transparency of the overlay. + /// 0 is completely transparent, 255 is completely opaque. + /// + [Category("ObjectListView"), + Description("The transparency of this adornment. 0 is completely transparent, 255 is completely opaque."), + DefaultValue(128)] + public int Transparency { + get { return this.transparency; } + set { this.transparency = Math.Min(255, Math.Max(0, value)); } + } + private int transparency = 128; + + #endregion + + #region Calculations + + /// + /// Calculate the location of rectangle of the given size, + /// so that it's indicated corner would be at the given point. + /// + /// The point + /// + /// Which corner will be positioned at the reference point + /// + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.TopLeft) -> Point(50, 100) + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.MiddleCenter) -> Point(45, 90) + /// CalculateAlignedPosition(new Point(50, 100), new Size(10, 20), System.Drawing.ContentAlignment.BottomRight) -> Point(40, 80) + public virtual Point CalculateAlignedPosition(Point pt, Size size, System.Drawing.ContentAlignment corner) { + switch (corner) { + case System.Drawing.ContentAlignment.TopLeft: + return pt; + case System.Drawing.ContentAlignment.TopCenter: + return new Point(pt.X - (size.Width / 2), pt.Y); + case System.Drawing.ContentAlignment.TopRight: + return new Point(pt.X - size.Width, pt.Y); + case System.Drawing.ContentAlignment.MiddleLeft: + return new Point(pt.X, pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.MiddleCenter: + return new Point(pt.X - (size.Width / 2), pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.MiddleRight: + return new Point(pt.X - size.Width, pt.Y - (size.Height / 2)); + case System.Drawing.ContentAlignment.BottomLeft: + return new Point(pt.X, pt.Y - size.Height); + case System.Drawing.ContentAlignment.BottomCenter: + return new Point(pt.X - (size.Width / 2), pt.Y - size.Height); + case System.Drawing.ContentAlignment.BottomRight: + return new Point(pt.X - size.Width, pt.Y - size.Height); + } + + // Should never reach here + return pt; + } + + /// + /// Calculate a rectangle that has the given size which is positioned so that + /// its alignment point is at the reference location of the given rect. + /// + /// + /// + /// + public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz) { + return this.CreateAlignedRectangle(r, sz, this.ReferenceCorner, this.AdornmentCorner, this.Offset); + } + + /// + /// Create a rectangle of the given size which is positioned so that + /// its indicated corner is at the indicated corner of the reference rect. + /// + /// + /// + /// + /// + /// + /// + /// + /// Creates a rectangle so that its bottom left is at the centre of the reference: + /// corner=BottomLeft, referenceCorner=MiddleCenter + /// This is a powerful concept that takes some getting used to, but is + /// very neat once you understand it. + /// + public virtual Rectangle CreateAlignedRectangle(Rectangle r, Size sz, + System.Drawing.ContentAlignment corner, System.Drawing.ContentAlignment referenceCorner, Size offset) { + Point referencePt = this.CalculateCorner(r, referenceCorner); + Point topLeft = this.CalculateAlignedPosition(referencePt, sz, corner); + return new Rectangle(topLeft + offset, sz); + } + + /// + /// Return the point at the indicated corner of the given rectangle (it doesn't + /// have to be a corner, but a named location) + /// + /// The reference rectangle + /// Which point of the rectangle should be returned? + /// A point + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.TopLeft) -> Point(0, 0) + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.MiddleCenter) -> Point(25, 50) + /// CalculateReferenceLocation(new Rectangle(0, 0, 50, 100), System.Drawing.ContentAlignment.BottomRight) -> Point(50, 100) + public virtual Point CalculateCorner(Rectangle r, System.Drawing.ContentAlignment corner) { + switch (corner) { + case System.Drawing.ContentAlignment.TopLeft: + return new Point(r.Left, r.Top); + case System.Drawing.ContentAlignment.TopCenter: + return new Point(r.X + (r.Width / 2), r.Top); + case System.Drawing.ContentAlignment.TopRight: + return new Point(r.Right, r.Top); + case System.Drawing.ContentAlignment.MiddleLeft: + return new Point(r.Left, r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.MiddleCenter: + return new Point(r.X + (r.Width / 2), r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.MiddleRight: + return new Point(r.Right, r.Top + (r.Height / 2)); + case System.Drawing.ContentAlignment.BottomLeft: + return new Point(r.Left, r.Bottom); + case System.Drawing.ContentAlignment.BottomCenter: + return new Point(r.X + (r.Width / 2), r.Bottom); + case System.Drawing.ContentAlignment.BottomRight: + return new Point(r.Right, r.Bottom); + } + + // Should never reach here + return r.Location; + } + + /// + /// Given the item and the subitem, calculate its bounds. + /// + /// + /// + /// + public virtual Rectangle CalculateItemBounds(OLVListItem item, OLVListSubItem subItem) { + if (item == null) + return Rectangle.Empty; + + if (subItem == null) + return item.Bounds; + + return item.GetSubItemBounds(item.SubItems.IndexOf(subItem)); + } + + #endregion + + #region Commands + + /// + /// Apply any specified rotation to the Graphic content. + /// + /// The Graphics to be transformed + /// The rotation will be around the centre of this rect + protected virtual void ApplyRotation(Graphics g, Rectangle r) { + if (this.Rotation == 0) + return; + + // THINK: Do we want to reset the transform? I think we want to push a new transform + g.ResetTransform(); + Matrix m = new Matrix(); + m.RotateAt(this.Rotation, new Point(r.Left + r.Width / 2, r.Top + r.Height / 2)); + g.Transform = m; + } + + /// + /// Reverse the rotation created by ApplyRotation() + /// + /// + protected virtual void UnapplyRotation(Graphics g) { + if (this.Rotation != 0) + g.ResetTransform(); + } + + #endregion + } + + /// + /// An overlay that will draw an image over the top of the ObjectListView + /// + public class ImageAdornment : GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the image that will be drawn + /// + [Category("ObjectListView"), + Description("The image that will be drawn"), + DefaultValue(null), + NotifyParentProperty(true)] + public Image Image { + get { return this.image; } + set { this.image = value; } + } + private Image image; + + /// + /// Gets or sets if the image will be shrunk to fit with its horizontal bounds + /// + [Category("ObjectListView"), + Description("Will the image be shrunk to fit within its width?"), + DefaultValue(false)] + public bool ShrinkToWidth { + get { return this.shrinkToWidth; } + set { this.shrinkToWidth = value; } + } + private bool shrinkToWidth; + + #endregion + + #region Commands + + /// + /// Draw the image in its specified location + /// + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void DrawImage(Graphics g, Rectangle r) { + if (this.ShrinkToWidth) + this.DrawScaledImage(g, r, this.Image, this.Transparency); + else + this.DrawImage(g, r, this.Image, this.Transparency); + } + + /// + /// Draw the image in its specified location + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawImage(Graphics g, Rectangle r, Image image, int transparency) { + if (image != null) + this.DrawImage(g, r, image, image.Size, transparency); + } + + /// + /// Draw the image in its specified location + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How big should the image be? + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawImage(Graphics g, Rectangle r, Image image, Size sz, int transparency) { + if (image == null) + return; + + Rectangle adornmentBounds = this.CreateAlignedRectangle(r, sz); + try { + this.ApplyRotation(g, adornmentBounds); + this.DrawTransparentBitmap(g, adornmentBounds, image, transparency); + } + finally { + this.UnapplyRotation(g); + } + } + + /// + /// Draw the image in its specified location, scaled so that it is not wider + /// than the given rectangle. Height is scaled proportional to the width. + /// + /// The image to be drawn + /// The Graphics used for drawing + /// The bounds of the rendering + /// How transparent should the image be (0 is completely transparent, 255 is opaque) + public virtual void DrawScaledImage(Graphics g, Rectangle r, Image image, int transparency) { + if (image == null) + return; + + // If the image is too wide to be drawn in the space provided, proportionally scale it down. + // Too tall images are not scaled. + Size size = image.Size; + if (image.Width > r.Width) { + float scaleRatio = (float)r.Width / (float)image.Width; + size.Height = (int)((float)image.Height * scaleRatio); + size.Width = r.Width - 1; + } + + this.DrawImage(g, r, image, size, transparency); + } + + /// + /// Utility to draw a bitmap transparenly. + /// + /// + /// + /// + /// + protected virtual void DrawTransparentBitmap(Graphics g, Rectangle r, Image image, int transparency) { + ImageAttributes imageAttributes = null; + if (transparency != 255) { + imageAttributes = new ImageAttributes(); + float a = (float)transparency / 255.0f; + float[][] colorMatrixElements = { + new float[] {1, 0, 0, 0, 0}, + new float[] {0, 1, 0, 0, 0}, + new float[] {0, 0, 1, 0, 0}, + new float[] {0, 0, 0, a, 0}, + new float[] {0, 0, 0, 0, 1}}; + + imageAttributes.SetColorMatrix(new ColorMatrix(colorMatrixElements)); + } + + g.DrawImage(image, + r, // destination rectangle + 0, 0, image.Size.Width, image.Size.Height, // source rectangle + GraphicsUnit.Pixel, + imageAttributes); + } + + #endregion + } + + /// + /// An adornment that will draw text + /// + public class TextAdornment : GraphicAdornment + { + #region Public properties + + /// + /// Gets or sets the background color of the text + /// Set this to Color.Empty to not draw a background + /// + [Category("ObjectListView"), + Description("The background color of the text"), + DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor = Color.Empty; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Brush BackgroundBrush { + get { + return new SolidBrush(Color.FromArgb(this.workingTransparency, this.BackColor)); + } + } + + /// + /// Gets or sets the color of the border around the billboard. + /// Set this to Color.Empty to remove the border + /// + [Category("ObjectListView"), + Description("The color of the border around the text"), + DefaultValue(typeof(Color), "")] + public Color BorderColor { + get { return this.borderColor; } + set { this.borderColor = value; } + } + private Color borderColor = Color.Empty; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Pen BorderPen { + get { + return new Pen(Color.FromArgb(this.workingTransparency, this.BorderColor), this.BorderWidth); + } + } + + /// + /// Gets or sets the width of the border around the text + /// + [Category("ObjectListView"), + Description("The width of the border around the text"), + DefaultValue(0.0f)] + public float BorderWidth { + get { return this.borderWidth; } + set { this.borderWidth = value; } + } + private float borderWidth; + + /// + /// How rounded should the corners of the border be? 0 means no rounding. + /// + /// If this value is too large, the edges of the border will appear odd. + [Category("ObjectListView"), + Description("How rounded should the corners of the border be? 0 means no rounding."), + DefaultValue(16.0f), + NotifyParentProperty(true)] + public float CornerRounding { + get { return this.cornerRounding; } + set { this.cornerRounding = value; } + } + private float cornerRounding = 16.0f; + + /// + /// Gets or sets the font that will be used to draw the text + /// + [Category("ObjectListView"), + Description("The font that will be used to draw the text"), + DefaultValue(null), + NotifyParentProperty(true)] + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets the font that will be used to draw the text or a reasonable default + /// + [Browsable(false)] + public Font FontOrDefault { + get { + return this.Font ?? new Font("Tahoma", 16); + } + } + + /// + /// Does this text have a background? + /// + [Browsable(false)] + public bool HasBackground { + get { + return this.BackColor != Color.Empty; + } + } + + /// + /// Does this overlay have a border? + /// + [Browsable(false)] + public bool HasBorder { + get { + return this.BorderColor != Color.Empty && this.BorderWidth > 0; + } + } + + /// + /// Gets or sets the maximum width of the text. Text longer than this will wrap. + /// 0 means no maximum. + /// + [Category("ObjectListView"), + Description("The maximum width the text (0 means no maximum). Text longer than this will wrap"), + DefaultValue(0)] + public int MaximumTextWidth { + get { return this.maximumTextWidth; } + set { this.maximumTextWidth = value; } + } + private int maximumTextWidth; + + /// + /// Gets or sets the formatting that should be used on the text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual StringFormat StringFormat { + get { + if (this.stringFormat == null) { + this.stringFormat = new StringFormat(); + this.stringFormat.Alignment = StringAlignment.Center; + this.stringFormat.LineAlignment = StringAlignment.Center; + this.stringFormat.Trimming = StringTrimming.EllipsisCharacter; + if (!this.Wrap) + this.stringFormat.FormatFlags = StringFormatFlags.NoWrap; + } + return this.stringFormat; + } + set { this.stringFormat = value; } + } + private StringFormat stringFormat; + + /// + /// Gets or sets the text that will be drawn + /// + [Category("ObjectListView"), + Description("The text that will be drawn over the top of the ListView"), + DefaultValue(null), + NotifyParentProperty(true), + Localizable(true)] + public string Text { + get { return this.text; } + set { this.text = value; } + } + private string text; + + /// + /// Gets the brush that will be used to paint the text + /// + [Browsable(false)] + public Brush TextBrush { + get { + return new SolidBrush(Color.FromArgb(this.workingTransparency, this.TextColor)); + } + } + + /// + /// Gets or sets the color of the text + /// + [Category("ObjectListView"), + Description("The color of the text"), + DefaultValue(typeof(Color), "DarkBlue"), + NotifyParentProperty(true)] + public Color TextColor { + get { return this.textColor; } + set { this.textColor = value; } + } + private Color textColor = Color.DarkBlue; + + /// + /// Gets or sets whether the text will wrap when it exceeds its bounds + /// + [Category("ObjectListView"), + Description("Will the text wrap?"), + DefaultValue(true)] + public bool Wrap { + get { return this.wrap; } + set { this.wrap = value; } + } + private bool wrap = true; + + #endregion + + #region Implementation + + /// + /// Draw our text with our stored configuration in relation to the given + /// reference rectangle + /// + /// The Graphics used for drawing + /// The reference rectangle in relation to which the text will be drawn + public virtual void DrawText(Graphics g, Rectangle r) { + this.DrawText(g, r, this.Text, this.Transparency); + } + + /// + /// Draw the given text with our stored configuration + /// + /// The Graphics used for drawing + /// The reference rectangle in relation to which the text will be drawn + /// The text to draw + /// How opaque should be text be + public virtual void DrawText(Graphics g, Rectangle r, string s, int transparency) { + if (String.IsNullOrEmpty(s)) + return; + + Rectangle textRect = this.CalculateTextBounds(g, r, s); + this.DrawBorderedText(g, textRect, s, transparency); + } + + /// + /// Draw the text with a border + /// + /// The Graphics used for drawing + /// The bounds within which the text should be drawn + /// The text to draw + /// How opaque should be text be + protected virtual void DrawBorderedText(Graphics g, Rectangle textRect, string text, int transparency) { + Rectangle borderRect = textRect; + borderRect.Inflate((int)this.BorderWidth / 2, (int)this.BorderWidth / 2); + borderRect.Y -= 1; // Looker better a little higher + + try { + this.ApplyRotation(g, textRect); + using (GraphicsPath path = this.GetRoundedRect(borderRect, this.CornerRounding)) { + this.workingTransparency = transparency; + if (this.HasBackground) { + using (Brush b = this.BackgroundBrush) + g.FillPath(b, path); + } + + using (Brush b = this.TextBrush) + g.DrawString(text, this.FontOrDefault, b, textRect, this.StringFormat); + + if (this.HasBorder) { + using (Pen p = this.BorderPen) + g.DrawPath(p, path); + } + } + } + finally { + this.UnapplyRotation(g); + } + } + + /// + /// Return the rectangle that will be the precise bounds of the displayed text + /// + /// + /// + /// + /// The bounds of the text + protected virtual Rectangle CalculateTextBounds(Graphics g, Rectangle r, string s) { + int maxWidth = this.MaximumTextWidth <= 0 ? r.Width : this.MaximumTextWidth; + SizeF sizeF = g.MeasureString(s, this.FontOrDefault, maxWidth, this.StringFormat); + Size size = new Size(1 + (int)sizeF.Width, 1 + (int)sizeF.Height); + return this.CreateAlignedRectangle(r, size); + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// The rectangle + /// The diameter of the corners + /// A round cornered rectagle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + protected virtual GraphicsPath GetRoundedRect(Rectangle rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter > 0) { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } else { + path.AddRectangle(rect); + } + + return path; + } + + #endregion + + private int workingTransparency; + } +} diff --git a/ObjectListView/Rendering/Decorations.cs b/ObjectListView/Rendering/Decorations.cs new file mode 100644 index 0000000..bf192ee --- /dev/null +++ b/ObjectListView/Rendering/Decorations.cs @@ -0,0 +1,820 @@ +/* + * Decorations - Images, text or other things that can be rendered onto an ObjectListView + * + * Author: Phillip Piper + * Date: 19/08/2009 10:56 PM + * + * Change log: + * 2011-04-04 JPP - Added ability to have a gradient background on BorderDecoration + * v2.4 + * 2010-04-15 JPP - Tweaked LightBoxDecoration a little + * v2.3 + * 2009-09-23 JPP - Added LeftColumn and RightColumn to RowBorderDecoration + * 2009-08-23 JPP - Added LightBoxDecoration + * 2009-08-19 JPP - Initial version. Separated from Overlays.cs + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A decoration is an overlay that draws itself in relation to a given row or cell. + /// Decorations scroll when the listview scrolls. + /// + public interface IDecoration : IOverlay + { + /// + /// Gets or sets the row that is to be decorated + /// + OLVListItem ListItem { get; set; } + + /// + /// Gets or sets the subitem that is to be decorated + /// + OLVListSubItem SubItem { get; set; } + } + + /// + /// An AbstractDecoration is a safe do-nothing implementation of the IDecoration interface + /// + public class AbstractDecoration : IDecoration + { + #region IDecoration Members + + /// + /// Gets or sets the row that is to be decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the subitem that is to be decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + #endregion + + #region Public properties + + /// + /// Gets the bounds of the decorations row + /// + public Rectangle RowBounds { + get { + if (this.ListItem == null) + return Rectangle.Empty; + else + return this.ListItem.Bounds; + } + } + + /// + /// Get the bounds of the decorations cell + /// + public Rectangle CellBounds { + get { + if (this.ListItem == null || this.SubItem == null) + return Rectangle.Empty; + else + return this.ListItem.GetSubItemBounds(this.ListItem.SubItems.IndexOf(this.SubItem)); + } + } + + #endregion + + #region IOverlay Members + + /// + /// Draw the decoration + /// + /// + /// + /// + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + } + + #endregion + } + + /// + /// This decoration draws a slight tint over a column of the + /// owning listview. If no column is explicitly set, the selected + /// column in the listview will be used. + /// The selected column is normally the sort column, but does not have to be. + /// + public class TintedColumnDecoration : AbstractDecoration + { + #region Constructors + + /// + /// Create a TintedColumnDecoration + /// + public TintedColumnDecoration() { + this.Tint = Color.FromArgb(15, Color.Blue); + } + + /// + /// Create a TintedColumnDecoration + /// + /// + public TintedColumnDecoration(OLVColumn column) + : this() { + this.ColumnToTint = column; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the column that will be tinted + /// + public OLVColumn ColumnToTint { + get { return this.columnToTint; } + set { this.columnToTint = value; } + } + private OLVColumn columnToTint; + + /// + /// Gets or sets the color that will be 'tinted' over the selected column + /// + public Color Tint { + get { return this.tint; } + set { + if (this.tint == value) + return; + + if (this.tintBrush != null) { + this.tintBrush.Dispose(); + this.tintBrush = null; + } + + this.tint = value; + this.tintBrush = new SolidBrush(this.tint); + } + } + private Color tint; + private SolidBrush tintBrush; + + #endregion + + #region IOverlay Members + + /// + /// Draw a slight colouring over our tinted column + /// + /// + /// This overlay only works when: + /// - the list is in Details view + /// - there is at least one row + /// - there is a selected column (or a specified tint column) + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + + if (olv.View != System.Windows.Forms.View.Details) + return; + + if (olv.GetItemCount() == 0) + return; + + OLVColumn column = this.ColumnToTint ?? olv.SelectedColumn; + if (column == null) + return; + + Point sides = NativeMethods.GetScrolledColumnSides(olv, column.Index); + if (sides.X == -1) + return; + + Rectangle columnBounds = new Rectangle(sides.X, r.Top, sides.Y - sides.X, r.Bottom); + + // Find the bottom of the last item. The tinting should extend only to there. + OLVListItem lastItem = olv.GetLastItemInDisplayOrder(); + if (lastItem != null) { + Rectangle lastItemBounds = lastItem.Bounds; + if (!lastItemBounds.IsEmpty && lastItemBounds.Bottom < columnBounds.Bottom) + columnBounds.Height = lastItemBounds.Bottom - columnBounds.Top; + } + g.FillRectangle(this.tintBrush, columnBounds); + } + + #endregion + } + + /// + /// This decoration draws an optionally filled border around a rectangle. + /// Subclasses must override CalculateBounds(). + /// + public class BorderDecoration : AbstractDecoration + { + #region Constructors + + /// + /// Create a BorderDecoration + /// + public BorderDecoration() + : this(new Pen(Color.FromArgb(64, Color.Blue), 1)) { + } + + /// + /// Create a BorderDecoration + /// + /// The pen used to draw the border + public BorderDecoration(Pen borderPen) { + this.BorderPen = borderPen; + } + + /// + /// Create a BorderDecoration + /// + /// The pen used to draw the border + /// The brush used to fill the rectangle + public BorderDecoration(Pen borderPen, Brush fill) { + this.BorderPen = borderPen; + this.FillBrush = fill; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the pen that will be used to draw the border + /// + public Pen BorderPen { + get { return this.borderPen; } + set { this.borderPen = value; } + } + private Pen borderPen; + + /// + /// Gets or sets the padding that will be added to the bounds of the item + /// before drawing the border and fill. + /// + public Size BoundsPadding { + get { return this.boundsPadding; } + set { this.boundsPadding = value; } + } + private Size boundsPadding = new Size(-1, 2); + + /// + /// How rounded should the corners of the border be? 0 means no rounding. + /// + /// If this value is too large, the edges of the border will appear odd. + public float CornerRounding { + get { return this.cornerRounding; } + set { this.cornerRounding = value; } + } + private float cornerRounding = 16.0f; + + /// + /// Gets or sets the brush that will be used to fill the border + /// + /// This value is ignored when using gradient brush + public Brush FillBrush { + get { return this.fillBrush; } + set { this.fillBrush = value; } + } + private Brush fillBrush = new SolidBrush(Color.FromArgb(64, Color.Blue)); + + /// + /// Gets or sets the color that will be used as the start of a gradient fill. + /// + /// This and FillGradientTo must be given value to show a gradient + public Color? FillGradientFrom { + get { return this.fillGradientFrom; } + set { this.fillGradientFrom = value; } + } + private Color? fillGradientFrom; + + /// + /// Gets or sets the color that will be used as the end of a gradient fill. + /// + /// This and FillGradientFrom must be given value to show a gradient + public Color? FillGradientTo { + get { return this.fillGradientTo; } + set { this.fillGradientTo = value; } + } + private Color? fillGradientTo; + + /// + /// Gets or sets the fill mode that will be used for the gradient. + /// + public LinearGradientMode FillGradientMode { + get { return this.fillGradientMode; } + set { this.fillGradientMode = value; } + } + private LinearGradientMode fillGradientMode = LinearGradientMode.Vertical; + + #endregion + + #region IOverlay Members + + /// + /// Draw a filled border + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + Rectangle bounds = this.CalculateBounds(); + if (!bounds.IsEmpty) + this.DrawFilledBorder(g, bounds); + } + + #endregion + + #region Subclass responsibility + + /// + /// Subclasses should override this to say where the border should be drawn + /// + /// + protected virtual Rectangle CalculateBounds() { + return Rectangle.Empty; + } + + #endregion + + #region Implementation utlities + + /// + /// Do the actual work of drawing the filled border + /// + /// + /// + protected void DrawFilledBorder(Graphics g, Rectangle bounds) { + bounds.Inflate(this.BoundsPadding); + GraphicsPath path = this.GetRoundedRect(bounds, this.CornerRounding); + if (this.FillGradientFrom != null && this.FillGradientTo != null) { + if (this.FillBrush != null) + this.FillBrush.Dispose(); + this.FillBrush = new LinearGradientBrush(bounds, this.FillGradientFrom.Value, this.FillGradientTo.Value, this.FillGradientMode); + } + if (this.FillBrush != null) + g.FillPath(this.FillBrush, path); + if (this.BorderPen != null) + g.DrawPath(this.BorderPen, path); + } + + /// + /// Create a GraphicsPath that represents a round cornered rectangle. + /// + /// + /// If this is 0 or less, the rectangle will not be rounded. + /// + protected GraphicsPath GetRoundedRect(RectangleF rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter <= 0.0f) { + path.AddRectangle(rect); + } else { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } + + return path; + } + + #endregion + } + + /// + /// Instances of this class draw a border around the decorated row + /// + public class RowBorderDecoration : BorderDecoration + { + /// + /// Gets or sets the index of the left most column to be used for the border + /// + public int LeftColumn { + get { return leftColumn; } + set { leftColumn = value; } + } + private int leftColumn = -1; + + /// + /// Gets or sets the index of the right most column to be used for the border + /// + public int RightColumn { + get { return rightColumn; } + set { rightColumn = value; } + } + private int rightColumn = -1; + + /// + /// Calculate the boundaries of the border + /// + /// + protected override Rectangle CalculateBounds() { + Rectangle bounds = this.RowBounds; + if (this.ListItem == null) + return bounds; + + if (this.LeftColumn >= 0) { + Rectangle leftCellBounds = this.ListItem.GetSubItemBounds(this.LeftColumn); + if (!leftCellBounds.IsEmpty) { + bounds.Width = bounds.Right - leftCellBounds.Left; + bounds.X = leftCellBounds.Left; + } + } + + if (this.RightColumn >= 0) { + Rectangle rightCellBounds = this.ListItem.GetSubItemBounds(this.RightColumn); + if (!rightCellBounds.IsEmpty) { + bounds.Width = rightCellBounds.Right - bounds.Left; + } + } + + return bounds; + } + } + + /// + /// Instances of this class draw a border around the decorated subitem. + /// + public class CellBorderDecoration : BorderDecoration + { + /// + /// Calculate the boundaries of the border + /// + /// + protected override Rectangle CalculateBounds() { + return this.CellBounds; + } + } + + /// + /// This decoration puts a border around the cell being edited and + /// optionally "lightboxes" the cell (makes the rest of the control dark). + /// + public class EditingCellBorderDecoration : BorderDecoration + { + #region Life and death + + /// + /// Create a EditingCellBorderDecoration + /// + public EditingCellBorderDecoration() { + this.FillBrush = null; + this.BorderPen = new Pen(Color.DarkBlue, 2); + this.CornerRounding = 8; + this.BoundsPadding = new Size(10, 8); + + } + + /// + /// Create a EditingCellBorderDecoration + /// + /// Should the decoration use a lighbox display style? + public EditingCellBorderDecoration(bool useLightBox) : this() + { + this.UseLightbox = useLightbox; + } + + #endregion + + #region Configuration properties + + /// + /// Gets or set whether the decoration should make the rest of + /// the control dark when a cell is being edited + /// + /// If this is true, FillBrush is used to overpaint + /// the control. + public bool UseLightbox { + get { return this.useLightbox; } + set { + if (this.useLightbox == value) + return; + this.useLightbox = value; + if (this.useLightbox) { + if (this.FillBrush == null) + this.FillBrush = new SolidBrush(Color.FromArgb(64, Color.Black)); + } + } + } + private bool useLightbox; + + #endregion + + #region Implementation + + /// + /// Draw the decoration + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (!olv.IsCellEditing) + return; + + Rectangle bounds = olv.CellEditor.Bounds; + if (bounds.IsEmpty) + return; + + bounds.Inflate(this.BoundsPadding); + GraphicsPath path = this.GetRoundedRect(bounds, this.CornerRounding); + if (this.FillBrush != null) { + if (this.UseLightbox) { + using (Region newClip = new Region(r)) { + newClip.Exclude(path); + Region originalClip = g.Clip; + g.Clip = newClip; + g.FillRectangle(this.FillBrush, r); + g.Clip = originalClip; + } + } else { + g.FillPath(this.FillBrush, path); + } + } + if (this.BorderPen != null) + g.DrawPath(this.BorderPen, path); + } + + #endregion + } + + /// + /// This decoration causes everything *except* the row under the mouse to be overpainted + /// with a tint, making the row under the mouse stand out in comparison. + /// The darker and more opaque the fill color, the more obvious the + /// decorated row becomes. + /// + public class LightBoxDecoration : BorderDecoration + { + /// + /// Create a LightBoxDecoration + /// + public LightBoxDecoration() { + this.BoundsPadding = new Size(-1, 4); + this.CornerRounding = 8.0f; + this.FillBrush = new SolidBrush(Color.FromArgb(72, Color.Black)); + } + + /// + /// Draw a tint over everything in the ObjectListView except the + /// row under the mouse. + /// + /// + /// + /// + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (!r.Contains(olv.PointToClient(Cursor.Position))) + return; + + Rectangle bounds = this.RowBounds; + if (bounds.IsEmpty) { + if (olv.View == View.Tile) + g.FillRectangle(this.FillBrush, r); + return; + } + + using (Region newClip = new Region(r)) { + bounds.Inflate(this.BoundsPadding); + newClip.Exclude(this.GetRoundedRect(bounds, this.CornerRounding)); + Region originalClip = g.Clip; + g.Clip = newClip; + g.FillRectangle(this.FillBrush, r); + g.Clip = originalClip; + } + } + } + + /// + /// Instances of this class put an Image over the row/cell that it is decorating + /// + public class ImageDecoration : ImageAdornment, IDecoration + { + #region Constructors + + /// + /// Create an image decoration + /// + public ImageDecoration() { + this.Alignment = ContentAlignment.MiddleRight; + } + + /// + /// Create an image decoration + /// + /// + public ImageDecoration(Image image) + : this() { + this.Image = image; + } + + /// + /// Create an image decoration + /// + /// + /// + public ImageDecoration(Image image, int transparency) + : this() { + this.Image = image; + this.Transparency = transparency; + } + + /// + /// Create an image decoration + /// + /// + /// + public ImageDecoration(Image image, ContentAlignment alignment) + : this() { + this.Image = image; + this.Alignment = alignment; + } + + /// + /// Create an image decoration + /// + /// + /// + /// + public ImageDecoration(Image image, int transparency, ContentAlignment alignment) + : this() { + this.Image = image; + this.Transparency = transparency; + this.Alignment = alignment; + } + + #endregion + + #region IDecoration Members + + /// + /// Gets or sets the item being decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the sub item being decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + #endregion + + #region Commands + + /// + /// Draw this decoration + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + this.DrawImage(g, this.CalculateItemBounds(this.ListItem, this.SubItem)); + } + + #endregion + } + + /// + /// Instances of this class draw some text over the row/cell that they are decorating + /// + public class TextDecoration : TextAdornment, IDecoration + { + #region Constructors + + /// + /// Create a TextDecoration + /// + public TextDecoration() { + this.Alignment = ContentAlignment.MiddleRight; + } + + /// + /// Create a TextDecoration + /// + /// + public TextDecoration(string text) + : this() { + this.Text = text; + } + + /// + /// Create a TextDecoration + /// + /// + /// + public TextDecoration(string text, int transparency) + : this() { + this.Text = text; + this.Transparency = transparency; + } + + /// + /// Create a TextDecoration + /// + /// + /// + public TextDecoration(string text, ContentAlignment alignment) + : this() { + this.Text = text; + this.Alignment = alignment; + } + + /// + /// Create a TextDecoration + /// + /// + /// + /// + public TextDecoration(string text, int transparency, ContentAlignment alignment) + : this() { + this.Text = text; + this.Transparency = transparency; + this.Alignment = alignment; + } + + #endregion + + #region IDecoration Members + + /// + /// Gets or sets the item being decorated + /// + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Gets or sets the sub item being decorated + /// + public OLVListSubItem SubItem { + get { return subItem; } + set { subItem = value; } + } + private OLVListSubItem subItem; + + + #endregion + + #region Commands + + /// + /// Draw this decoration + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + this.DrawText(g, this.CalculateItemBounds(this.ListItem, this.SubItem)); + } + + #endregion + } +} diff --git a/ObjectListView/Rendering/Overlays.cs b/ObjectListView/Rendering/Overlays.cs new file mode 100644 index 0000000..38bad03 --- /dev/null +++ b/ObjectListView/Rendering/Overlays.cs @@ -0,0 +1,302 @@ +/* + * Overlays - Images, text or other things that can be rendered over the top of a ListView + * + * Author: Phillip Piper + * Date: 14/04/2009 4:36 PM + * + * Change log: + * v2.3 + * 2009-08-17 JPP - Overlays now use Adornments + * - Added ITransparentOverlay interface. Overlays can now have separate transparency levels + * 2009-08-10 JPP - Moved decoration related code to new file + * v2.2.1 + * 200-07-24 JPP - TintedColumnDecoration now works when last item is a member of a collapsed + * group (well, it no longer crashes). + * v2.2 + * 2009-06-01 JPP - Make sure that TintedColumnDecoration reaches to the last item in group view + * 2009-05-05 JPP - Unified BillboardOverlay text rendering with that of TextOverlay + * 2009-04-30 JPP - Added TintedColumnDecoration + * 2009-04-14 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; + +namespace BrightIdeasSoftware +{ + /// + /// The interface for an object which can draw itself over the top of + /// an ObjectListView. + /// + public interface IOverlay + { + /// + /// Draw this overlay + /// + /// The ObjectListView that is being overlaid + /// The Graphics onto the given OLV + /// The content area of the OLV + void Draw(ObjectListView olv, Graphics g, Rectangle r); + } + + /// + /// An interface for an overlay that supports variable levels of transparency + /// + public interface ITransparentOverlay : IOverlay + { + /// + /// Gets or sets the transparency of the overlay. + /// 0 is completely transparent, 255 is completely opaque. + /// + int Transparency { get; set; } + } + + /// + /// A null implementation of the IOverlay interface + /// + public class AbstractOverlay : ITransparentOverlay + { + #region IOverlay Members + + /// + /// Draw this overlay + /// + /// The ObjectListView that is being overlaid + /// The Graphics onto the given OLV + /// The content area of the OLV + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + } + + #endregion + + #region ITransparentOverlay Members + + /// + /// How transparent should this overlay be? + /// + [Category("ObjectListView"), + Description("How transparent should this overlay be"), + DefaultValue(128), + NotifyParentProperty(true)] + public int Transparency { + get { return this.transparency; } + set { this.transparency = Math.Min(255, Math.Max(0, value)); } + } + private int transparency = 128; + + #endregion + } + + /// + /// An overlay that will draw an image over the top of the ObjectListView + /// + [TypeConverter("BrightIdeasSoftware.Design.OverlayConverter")] + public class ImageOverlay : ImageAdornment, ITransparentOverlay + { + /// + /// Create an ImageOverlay + /// + public ImageOverlay() { + this.Alignment = System.Drawing.ContentAlignment.BottomRight; + } + + #region Public properties + + /// + /// Gets or sets the horizontal inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("The horizontal inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetX { + get { return this.insetX; } + set { this.insetX = Math.Max(0, value); } + } + private int insetX = 20; + + /// + /// Gets or sets the vertical inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("Gets or sets the vertical inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetY { + get { return this.insetY; } + set { this.insetY = Math.Max(0, value); } + } + private int insetY = 20; + + #endregion + + #region Commands + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + Rectangle insetRect = r; + insetRect.Inflate(-this.InsetX, -this.InsetY); + + // We hard code a transparency of 255 here since transparency is handled by the glass panel + this.DrawImage(g, insetRect, this.Image, 255); + } + + #endregion + } + + /// + /// An overlay that will draw text over the top of the ObjectListView + /// + [TypeConverter("BrightIdeasSoftware.Design.OverlayConverter")] + public class TextOverlay : TextAdornment, ITransparentOverlay + { + /// + /// Create a TextOverlay + /// + public TextOverlay() { + this.Alignment = System.Drawing.ContentAlignment.BottomRight; + } + + #region Public properties + + /// + /// Gets or sets the horizontal inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("The horizontal inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetX { + get { return this.insetX; } + set { this.insetX = Math.Max(0, value); } + } + private int insetX = 20; + + /// + /// Gets or sets the vertical inset by which the position of the overlay will be adjusted + /// + [Category("ObjectListView"), + Description("Gets or sets the vertical inset by which the position of the overlay will be adjusted"), + DefaultValue(20), + NotifyParentProperty(true)] + public int InsetY { + get { return this.insetY; } + set { this.insetY = Math.Max(0, value); } + } + private int insetY = 20; + + /// + /// Gets or sets whether the border will be drawn with rounded corners + /// + [Browsable(false), + Obsolete("Use CornerRounding instead", false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool RoundCorneredBorder { + get { return this.CornerRounding > 0; } + set { + if (value) + this.CornerRounding = 16.0f; + else + this.CornerRounding = 0.0f; + } + } + + #endregion + + #region Commands + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public virtual void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (String.IsNullOrEmpty(this.Text)) + return; + + Rectangle insetRect = r; + insetRect.Inflate(-this.InsetX, -this.InsetY); + // We hard code a transparency of 255 here since transparency is handled by the glass panel + this.DrawText(g, insetRect, this.Text, 255); + } + + #endregion + } + + /// + /// A Billboard overlay is a TextOverlay positioned at an absolute point + /// + public class BillboardOverlay : TextOverlay + { + /// + /// Create a BillboardOverlay + /// + public BillboardOverlay() { + this.Transparency = 255; + this.BackColor = Color.PeachPuff; + this.TextColor = Color.Black; + this.BorderColor = Color.Empty; + this.Font = new Font("Tahoma", 10); + } + + /// + /// Gets or sets where should the top left of the billboard be placed + /// + public Point Location { + get { return this.location; } + set { this.location = value; } + } + private Point location; + + /// + /// Draw this overlay + /// + /// The ObjectListView being decorated + /// The Graphics used for drawing + /// The bounds of the rendering + public override void Draw(ObjectListView olv, Graphics g, Rectangle r) { + if (String.IsNullOrEmpty(this.Text)) + return; + + // Calculate the bounds of the text, and then move it to where it should be + Rectangle textRect = this.CalculateTextBounds(g, r, this.Text); + textRect.Location = this.Location; + + // Make sure the billboard is within the bounds of the List, as far as is possible + if (textRect.Right > r.Width) + textRect.X = Math.Max(r.Left, r.Width - textRect.Width); + if (textRect.Bottom > r.Height) + textRect.Y = Math.Max(r.Top, r.Height - textRect.Height); + + this.DrawBorderedText(g, textRect, this.Text, 255); + } + } +} diff --git a/ObjectListView/Rendering/Renderers.cs b/ObjectListView/Rendering/Renderers.cs new file mode 100644 index 0000000..646b662 --- /dev/null +++ b/ObjectListView/Rendering/Renderers.cs @@ -0,0 +1,3413 @@ +/* + * Renderers - A collection of useful renderers that are used to owner draw a cell in an ObjectListView + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * v2.8 + * 2014-09-26 JPP - Dispose of animation timer in a more robust fashion. + * 2014-05-20 JPP - Handle rendering disabled rows + * v2.7 + * 2013-04-29 JPP - Fixed bug where Images were not vertically aligned + * v2.6 + * 2012-10-26 JPP - Hit detection will no longer report check box hits on columns without checkboxes. + * 2012-07-13 JPP - [Breaking change] Added preferedSize parameter to IRenderer.GetEditRectangle(). + * v2.5.1 + * 2012-07-14 JPP - Added CellPadding to various places. Replaced DescribedTaskRenderer.CellPadding. + * 2012-07-11 JPP - Added CellVerticalAlignment to various places allow cell contents to be vertically + * aligned (rather than always being centered). + * v2.5 + * 2010-08-24 JPP - CheckBoxRenderer handles hot boxes and correctly vertically centers the box. + * 2010-06-23 JPP - Major rework of HighlightTextRenderer. Now uses TextMatchFilter directly. + * Draw highlighting underneath text to improve legibility. Works with new + * TextMatchFilter capabilities. + * v2.4 + * 2009-10-30 JPP - Plugged possible resource leak by using using() with CreateGraphics() + * v2.3 + * 2009-09-28 JPP - Added DescribedTaskRenderer + * 2009-09-01 JPP - Correctly handle an ImageRenderer's handling of an aspect that holds + * the image to be displayed at Byte[]. + * 2009-08-29 JPP - Fixed bug where some of a cell's background was not erased. + * 2009-08-15 JPP - Correctly MeasureText() using the appropriate graphic context + * - Handle translucent selection setting + * v2.2.1 + * 2009-07-24 JPP - Try to honour CanWrap setting when GDI rendering text. + * 2009-07-11 JPP - Correctly calculate edit rectangle for subitems of a tree view + * (previously subitems were indented in the same way as the primary column) + * v2.2 + * 2009-06-06 JPP - Tweaked text rendering so that column 0 isn't ellipsed unnecessarily. + * 2009-05-05 JPP - Added Unfocused foreground and background colors + * (thanks to Christophe Hosten) + * 2009-04-21 JPP - Fixed off-by-1 error when calculating text widths. This caused + * middle and right aligned columns to always wrap one character + * when printed using ListViewPrinter (SF#2776634). + * 2009-04-11 JPP - Correctly renderer checkboxes when RowHeight is non-standard + * 2009-04-06 JPP - Allow for item indent when calculating edit rectangle + * v2.1 + * 2009-02-24 JPP - Work properly with ListViewPrinter again + * 2009-01-26 JPP - AUSTRALIA DAY (why aren't I on holidays!) + * - Major overhaul of renderers. Now uses IRenderer interface. + * - ImagesRenderer and FlagsRenderer are now defunct. + * The names are retained for backward compatibility. + * 2009-01-23 JPP - Align bitmap AND text according to column alignment (previously + * only text was aligned and bitmap was always to the left). + * 2009-01-21 JPP - Changed to use TextRenderer rather than native GDI routines. + * 2009-01-20 JPP - Draw images directly from image list if possible. 30% faster! + * - Tweaked some spacings to look more like native ListView + * - Text highlight for non FullRowSelect is now the right color + * when the control doesn't have focus. + * - Commented out experimental animations. Still needs work. + * 2009-01-19 JPP - Changed to draw text using GDI routines. Looks more like + * native control this way. Set UseGdiTextRendering to false to + * revert to previous behavior. + * 2009-01-15 JPP - Draw background correctly when control is disabled + * - Render checkboxes using CheckBoxRenderer + * v2.0.1 + * 2008-12-29 JPP - Render text correctly when HideSelection is true. + * 2008-12-26 JPP - BaseRenderer now works correctly in all Views + * 2008-12-23 JPP - Fixed two small bugs in BarRenderer + * v2.0 + * 2008-10-26 JPP - Don't owner draw when in Design mode + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * TO DO: + * - Hit detection on renderers doesn't change the controls standard selection behavior + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using Timer = System.Threading.Timer; + +namespace BrightIdeasSoftware +{ + /// + /// Renderers are the mechanism used for owner drawing cells. As such, they can also handle + /// hit detection and positioning of cell editing rectangles. + /// + public interface IRenderer + { + /// + /// Render the whole item within an ObjectListView. This is only used in non-Details views. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the item + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, Object rowObject); + + /// + /// Render one cell within an ObjectListView when it is in Details mode. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the cell + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, Object rowObject); + + /// + /// What is under the given point? + /// + /// + /// x co-ordinate + /// y co-ordinate + /// This method should only alter HitTestLocation and/or UserData. + void HitTest(OlvListViewHitTestInfo hti, int x, int y); + + /// + /// When the value in the given cell is to be edited, where should the edit rectangle be placed? + /// + /// + /// + /// + /// + /// + /// + Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize); + } + + /// + /// An AbstractRenderer is a do-nothing implementation of the IRenderer interface. + /// + [Browsable(true), + ToolboxItem(false)] + public class AbstractRenderer : Component, IRenderer + { + #region IRenderer Members + + /// + /// Render the whole item within an ObjectListView. This is only used in non-Details views. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the item + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + public virtual bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) { + return true; + } + + /// + /// Render one cell within an ObjectListView when it is in Details mode. + /// + /// The event + /// A Graphics for rendering + /// The bounds of the cell + /// The model object to be drawn + /// Return true to indicate that the event was handled and no further processing is needed. + public virtual bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { + return false; + } + + /// + /// What is under the given point? + /// + /// + /// x co-ordinate + /// y co-ordinate + /// This method should only alter HitTestLocation and/or UserData. + public virtual void HitTest(OlvListViewHitTestInfo hti, int x, int y) { + } + + /// + /// When the value in the given cell is to be edited, where should the edit rectangle be placed? + /// + /// + /// + /// + /// + /// + /// + public virtual Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return cellBounds; + } + + #endregion + } + + /// + /// This class provides compatibility for v1 RendererDelegates + /// + [ToolboxItem(false)] + internal class Version1Renderer : AbstractRenderer + { + public Version1Renderer(RenderDelegate renderDelegate) { + this.RenderDelegate = renderDelegate; + } + /// + /// The renderer delegate that this renderer wraps + /// + public RenderDelegate RenderDelegate; + + #region IRenderer Members + + public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { + if (this.RenderDelegate == null) + return base.RenderSubItem(e, g, cellBounds, rowObject); + else + return this.RenderDelegate(e, g, cellBounds, rowObject); + } + + #endregion + } + + /// + /// A BaseRenderer provides useful base level functionality for any custom renderer. + /// + /// + /// Subclasses will normally override the Render or OptionalRender method, and use the other + /// methods as helper functions. + /// + [Browsable(true), + ToolboxItem(true)] + public class BaseRenderer : AbstractRenderer + { + #region Configuration Properties + + /// + /// Can the renderer wrap lines that do not fit completely within the cell? + /// + /// Wrapping text doesn't work with the GDI renderer. + [Category("Appearance"), + Description("Can the renderer wrap text that does not fit completely within the cell"), + DefaultValue(false)] + public bool CanWrap { + get { return canWrap; } + set { + canWrap = value; + if (canWrap) + this.UseGdiTextRendering = false; + } + } + private bool canWrap; + + /// + /// Gets or sets how many pixels will be left blank around this cell + /// + /// + /// + /// This setting only takes effect when the control is owner drawn. + /// + /// for more details. + /// + [Category("ObjectListView"), + Description("The number of pixels that renderer will leave empty around the edge of the cell"), + DefaultValue(null)] + public Rectangle? CellPadding { + get { return this.cellPadding; } + set { this.cellPadding = value; } + } + private Rectangle? cellPadding; + + /// + /// Gets or sets how cells drawn by this renderer will be vertically aligned. + /// + /// + /// + /// If this is not set, the value from the column or control itself will be used. + /// + /// + [Category("ObjectListView"), + Description("How will cell values be vertically aligned?"), + DefaultValue(null)] + public virtual StringAlignment? CellVerticalAlignment { + get { return this.cellVerticalAlignment; } + set { this.cellVerticalAlignment = value; } + } + private StringAlignment? cellVerticalAlignment; + + /// + /// Gets the optional padding that this renderer should apply before drawing. + /// This property considers all possible sources of padding + /// + [Browsable(false)] + protected virtual Rectangle? EffectiveCellPadding { + get { + if (this.cellPadding.HasValue) + return this.cellPadding.Value; + + if (this.OLVSubItem != null && this.OLVSubItem.CellPadding.HasValue) + return this.OLVSubItem.CellPadding.Value; + + if (this.ListItem != null && this.ListItem.CellPadding.HasValue) + return this.ListItem.CellPadding.Value; + + if (this.Column != null && this.Column.CellPadding.HasValue) + return this.Column.CellPadding.Value; + + if (this.ListView != null && this.ListView.CellPadding.HasValue) + return this.ListView.CellPadding.Value; + + return null; + } + } + + /// + /// Gets the vertical cell alignment that should govern the rendering. + /// This property considers all possible sources. + /// + [Browsable(false)] + protected virtual StringAlignment EffectiveCellVerticalAlignment { + get { + if (this.cellVerticalAlignment.HasValue) + return this.cellVerticalAlignment.Value; + + if (this.OLVSubItem != null && this.OLVSubItem.CellVerticalAlignment.HasValue) + return this.OLVSubItem.CellVerticalAlignment.Value; + + if (this.ListItem != null && this.ListItem.CellVerticalAlignment.HasValue) + return this.ListItem.CellVerticalAlignment.Value; + + if (this.Column != null && this.Column.CellVerticalAlignment.HasValue) + return this.Column.CellVerticalAlignment.Value; + + if (this.ListView != null) + return this.ListView.CellVerticalAlignment; + + return StringAlignment.Center; + } + } + + /// + /// Gets or sets the image list from which keyed images will be fetched + /// + [Category("Appearance"), + Description("The image list from which keyed images will be fetched for drawing."), + DefaultValue(null)] + public ImageList ImageList { + get { return imageList; } + set { imageList = value; } + } + private ImageList imageList; + + /// + /// When rendering multiple images, how many pixels should be between each image? + /// + [Category("Appearance"), + Description("When rendering multiple images, how many pixels should be between each image?"), + DefaultValue(1)] + public int Spacing { + get { return spacing; } + set { spacing = value; } + } + private int spacing = 1; + + /// + /// Should text be rendered using GDI routines? This makes the text look more + /// like a native List view control. + /// + [Category("Appearance"), + Description("Should text be rendered using GDI routines?"), + DefaultValue(true)] + public bool UseGdiTextRendering { + get { + // Can't use GDI routines on a GDI+ printer context + return !this.IsPrinting && useGdiTextRendering; + } + set { useGdiTextRendering = value; } + } + private bool useGdiTextRendering = true; + + #endregion + + #region State Properties + + /// + /// Get or set the aspect of the model object that this renderer should draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object Aspect { + get { + if (aspect == null) + aspect = column.GetValue(this.rowObject); + return aspect; + } + set { aspect = value; } + } + private Object aspect; + + /// + /// What are the bounds of the cell that is being drawn? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Rectangle Bounds { + get { return bounds; } + set { bounds = value; } + } + private Rectangle bounds; + + /// + /// Get or set the OLVColumn that this renderer will draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVColumn Column { + get { return column; } + set { column = value; } + } + private OLVColumn column; + + /// + /// Get/set the event that caused this renderer to be called + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public DrawListViewItemEventArgs DrawItemEvent { + get { return drawItemEventArgs; } + set { drawItemEventArgs = value; } + } + private DrawListViewItemEventArgs drawItemEventArgs; + + /// + /// Get/set the event that caused this renderer to be called + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public DrawListViewSubItemEventArgs Event { + get { return eventArgs; } + set { eventArgs = value; } + } + private DrawListViewSubItemEventArgs eventArgs; + + /// + /// Return the font to be used for text in this cell + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Font Font { + get { + if (this.font != null || this.ListItem == null) + return this.font; + + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.Font; + + return this.SubItem.Font; + } + set { + this.font = value; + } + } + private Font font; + + /// + /// Gets the image list from which keyed images will be fetched + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ImageList ImageListOrDefault { + get { return this.ImageList ?? this.ListView.SmallImageList; } + } + + /// + /// Should this renderer fill in the background before drawing? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsDrawBackground { + get { return !this.IsPrinting; } + } + + /// + /// Cache whether or not our item is selected + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsItemSelected { + get { return isItemSelected; } + set { isItemSelected = value; } + } + private bool isItemSelected; + + /// + /// Is this renderer being used on a printer context? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsPrinting { + get { return isPrinting; } + set { isPrinting = value; } + } + private bool isPrinting; + + /// + /// Get or set the listitem that this renderer will be drawing + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListItem ListItem { + get { return listItem; } + set { listItem = value; } + } + private OLVListItem listItem; + + /// + /// Get/set the listview for which the drawing is to be done + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ObjectListView ListView { + get { return objectListView; } + set { objectListView = value; } + } + private ObjectListView objectListView; + + /// + /// Get the specialized OLVSubItem that this renderer is drawing + /// + /// This returns null for column 0. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListSubItem OLVSubItem { + get { return listSubItem as OLVListSubItem; } + } + + /// + /// Get or set the model object that this renderer should draw + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object RowObject { + get { return rowObject; } + set { rowObject = value; } + } + private Object rowObject; + + /// + /// Get or set the list subitem that this renderer will be drawing + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public OLVListSubItem SubItem { + get { return listSubItem; } + set { listSubItem = value; } + } + private OLVListSubItem listSubItem; + + /// + /// The brush that will be used to paint the text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush TextBrush { + get { + if (textBrush == null) + return new SolidBrush(this.GetForegroundColor()); + else + return this.textBrush; + } + set { textBrush = value; } + } + private Brush textBrush; + + /// + /// Will this renderer use the custom images from the parent ObjectListView + /// to draw the checkbox images. + /// + /// + /// + /// If this is true, the renderer will use the images from the + /// StateImageList to represent checkboxes. 0 - unchecked, 1 - checked, 2 - indeterminate. + /// + /// If this is false (the default), then the renderer will use .NET's standard + /// CheckBoxRenderer. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool UseCustomCheckboxImages { + get { return useCustomCheckboxImages; } + set { useCustomCheckboxImages = value; } + } + private bool useCustomCheckboxImages; + + private void ClearState() { + this.Event = null; + this.DrawItemEvent = null; + this.Aspect = null; + this.Font = null; + this.TextBrush = null; + } + + #endregion + + #region Utilities + + /// + /// Align the second rectangle with the first rectangle, + /// according to the alignment of the column + /// + /// The cell's bounds + /// The rectangle to be aligned within the bounds + /// An aligned rectangle + protected virtual Rectangle AlignRectangle(Rectangle outer, Rectangle inner) { + Rectangle r = new Rectangle(outer.Location, inner.Size); + + // Align horizontally depending on the column alignment + if (inner.Width < outer.Width) { + r.X = AlignHorizontally(outer, inner); + } + + // Align vertically too + if (inner.Height < outer.Height) { + r.Y = AlignVertically(outer, inner); + } + + return r; + } + + /// + /// Calculate the left edge of the rectangle that aligns the outer rectangle with the inner one + /// according to this renderer's horizontal alignement + /// + /// + /// + /// + protected int AlignHorizontally(Rectangle outer, Rectangle inner) { + HorizontalAlignment alignment = this.Column == null ? HorizontalAlignment.Left : this.Column.TextAlign; + switch (alignment) { + case HorizontalAlignment.Left: + return outer.Left + 1; + case HorizontalAlignment.Center: + return outer.Left + ((outer.Width - inner.Width)/2); + case HorizontalAlignment.Right: + return outer.Right - inner.Width - 1; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Calculate the top of the rectangle that aligns the outer rectangle with the inner rectangle + /// according to this renders vertical alignment + /// + /// + /// + /// + protected int AlignVertically(Rectangle outer, Rectangle inner) { + return AlignVertically(outer, inner.Height); + } + + /// + /// Calculate the top of the rectangle that aligns the outer rectangle with a rectangle of the given height + /// according to this renderer's vertical alignment + /// + /// + /// + /// + protected int AlignVertically(Rectangle outer, int innerHeight) { + switch (this.EffectiveCellVerticalAlignment) { + case StringAlignment.Near: + return outer.Top + 1; + case StringAlignment.Center: + return outer.Top + ((outer.Height - innerHeight) / 2); + case StringAlignment.Far: + return outer.Bottom - innerHeight - 1; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Calculate the space that our rendering will occupy and then align that space + /// with the given rectangle, according to the Column alignment + /// + /// + /// + /// + protected virtual Rectangle CalculateAlignedRectangle(Graphics g, Rectangle r) { + if (this.Column == null || this.Column.TextAlign == HorizontalAlignment.Left) + return r; + + int width = this.CalculateCheckBoxWidth(g); + width += this.CalculateImageWidth(g, this.GetImageSelector()); + width += this.CalculateTextWidth(g, this.GetText()); + + // If the combined width is greater than the whole cell, + // we just use the cell itself + if (width >= r.Width) + return r; + + return this.AlignRectangle(r, new Rectangle(0, 0, width, r.Height)); + } + + /// + /// Calculate the bounds of a checkbox given the cell bounds + /// + /// + /// + /// + protected Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) { + Size checkBoxSize = (UseCustomCheckboxImages && this.ListView.StateImageList != null) + ? this.ListView.StateImageList.ImageSize + : CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal); + return this.AlignRectangle(cellBounds, + new Rectangle(0, 0, checkBoxSize.Width, checkBoxSize.Height)); + } + + /// + /// How much space will the check box for this cell occupy? + /// + /// Only column 0 can have check boxes. Sub item checkboxes are + /// treated as images + /// + /// + protected virtual int CalculateCheckBoxWidth(Graphics g) { + if (!this.ListView.CheckBoxes || !this.ColumnIsPrimary) + return 0; + + if (UseCustomCheckboxImages && this.ListView.StateImageList != null) + return this.ListView.StateImageList.ImageSize.Width; + + return CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.UncheckedNormal).Width + 6; + } + + /// + /// How much horizontal space will the image of this cell occupy? + /// + /// + /// + /// + protected virtual int CalculateImageWidth(Graphics g, object imageSelector) { + if (imageSelector == null || imageSelector == System.DBNull.Value) + return 0; + + // Draw from the image list (most common case) + ImageList il = this.ImageListOrDefault; + if (il != null) { + int selectorAsInt = -1; + + if (imageSelector is Int32) + selectorAsInt = (Int32)imageSelector; + else { + String selectorAsString = imageSelector as String; + if (selectorAsString != null) + selectorAsInt = il.Images.IndexOfKey(selectorAsString); + } + if (selectorAsInt >= 0) + return il.ImageSize.Width; + } + + // Is the selector actually an image? + Image image = imageSelector as Image; + if (image != null) + return image.Width; + + return 0; + } + + /// + /// How much horizontal space will the text of this cell occupy? + /// + /// + /// + /// + protected virtual int CalculateTextWidth(Graphics g, string txt) { + if (String.IsNullOrEmpty(txt)) + return 0; + + if (this.UseGdiTextRendering) { + Size proposedSize = new Size(int.MaxValue, int.MaxValue); + return TextRenderer.MeasureText(g, txt, this.Font, proposedSize, TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix).Width; + } else { + using (StringFormat fmt = new StringFormat()) { + fmt.Trimming = StringTrimming.EllipsisCharacter; + return 1 + (int)g.MeasureString(txt, this.Font, int.MaxValue, fmt).Width; + } + } + } + + /// + /// Return the Color that is the background color for this item's cell + /// + /// The background color of the subitem + public virtual Color GetBackgroundColor() { + if (!this.ListView.Enabled) + return SystemColors.Control; + + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && this.ListView.FullRowSelect) { + if (this.ListView.Focused) + return this.ListView.HighlightBackgroundColorOrDefault; + + if (!this.ListView.HideSelection) + return this.ListView.UnfocusedHighlightBackgroundColorOrDefault; + } + + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.BackColor; + + return this.SubItem.BackColor; + } + + /// + /// Return the color to be used for text in this cell + /// + /// The text color of the subitem + public virtual Color GetForegroundColor() { + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection && + (this.ColumnIsPrimary || this.ListView.FullRowSelect)) { + if (this.ListView.Focused) + return this.ListView.HighlightForegroundColorOrDefault; + else if (!this.ListView.HideSelection) + return this.ListView.UnfocusedHighlightForegroundColorOrDefault; + } + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.ForeColor; + else + return this.SubItem.ForeColor; + } + + /// + /// Return the image that should be drawn against this subitem + /// + /// An Image or null if no image should be drawn. + protected virtual Image GetImage() { + return this.GetImage(this.GetImageSelector()); + } + + /// + /// Return the actual image that should be drawn when keyed by the given image selector. + /// An image selector can be: + /// an int, giving the index into the image list + /// a string, giving the image key into the image list + /// an Image, being the image itself + /// + /// + /// The value that indicates the image to be used + /// An Image or null + protected virtual Image GetImage(Object imageSelector) { + if (imageSelector == null || imageSelector == System.DBNull.Value) + return null; + + ImageList il = this.ImageListOrDefault; + if (il != null) { + if (imageSelector is Int32) { + Int32 index = (Int32)imageSelector; + if (index < 0 || index >= il.Images.Count) + return null; + + return il.Images[index]; + } + + String str = imageSelector as String; + if (str != null) { + if (il.Images.ContainsKey(str)) + return il.Images[str]; + + return null; + } + } + + return imageSelector as Image; + } + + /// + /// + protected virtual Object GetImageSelector() { + return this.ColumnIsPrimary ? this.ListItem.ImageSelector : this.OLVSubItem.ImageSelector; + } + + /// + /// Return the string that should be drawn within this + /// + /// + protected virtual string GetText() { + return this.SubItem == null ? this.ListItem.Text : this.SubItem.Text; + } + + /// + /// Return the Color that is the background color for this item's text + /// + /// The background color of the subitem's text + protected virtual Color GetTextBackgroundColor() { + //TODO: Refactor with GetBackgroundColor() - they are almost identical + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection + && (this.ColumnIsPrimary || this.ListView.FullRowSelect)) { + if (this.ListView.Focused) + return this.ListView.HighlightBackgroundColorOrDefault; + else + if (!this.ListView.HideSelection) + return this.ListView.UnfocusedHighlightBackgroundColorOrDefault; + } + + if (this.SubItem == null || this.ListItem.UseItemStyleForSubItems) + return this.ListItem.BackColor; + else + return this.SubItem.BackColor; + } + + #endregion + + #region IRenderer members + + /// + /// Render the whole item in a non-details view. + /// + /// + /// + /// + /// + /// + public override bool RenderItem(DrawListViewItemEventArgs e, Graphics g, Rectangle itemBounds, object rowObject) { + this.ClearState(); + + this.DrawItemEvent = e; + this.ListItem = (OLVListItem)e.Item; + this.SubItem = null; + this.ListView = (ObjectListView)this.ListItem.ListView; + this.Column = this.ListView.GetColumn(0); + this.RowObject = rowObject; + this.Bounds = itemBounds; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + + return this.OptionalRender(g, itemBounds); + } + + /// + /// Render one cell + /// + /// + /// + /// + /// + /// + public override bool RenderSubItem(DrawListViewSubItemEventArgs e, Graphics g, Rectangle cellBounds, object rowObject) { + this.ClearState(); + + this.Event = e; + this.ListItem = (OLVListItem)e.Item; + this.SubItem = (OLVListSubItem)e.SubItem; + this.ListView = (ObjectListView)this.ListItem.ListView; + this.Column = (OLVColumn)e.Header; + this.RowObject = rowObject; + this.Bounds = cellBounds; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + + return this.OptionalRender(g, cellBounds); + } + + /// + /// Calculate which part of this cell was hit + /// + /// + /// + /// + public override void HitTest(OlvListViewHitTestInfo hti, int x, int y) { + this.ClearState(); + + this.ListView = hti.ListView; + this.ListItem = hti.Item; + this.SubItem = hti.SubItem; + this.Column = hti.Column; + this.RowObject = hti.RowObject; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + if (this.SubItem == null) + this.Bounds = this.ListItem.Bounds; + else + this.Bounds = this.ListItem.GetSubItemBounds(this.Column.Index); + + using (Graphics g = this.ListView.CreateGraphics()) { + this.HandleHitTest(g, hti, x, y); + } + } + + /// + /// Calculate the edit rectangle + /// + /// + /// + /// + /// + /// + /// + public override Rectangle GetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + this.ClearState(); + + this.ListView = (ObjectListView)item.ListView; + this.ListItem = item; + this.SubItem = item.GetSubItem(subItemIndex); + this.Column = this.ListView.GetColumn(subItemIndex); + this.RowObject = item.RowObject; + this.IsItemSelected = this.ListItem.Selected && this.ListItem.Enabled; + this.Bounds = cellBounds; + + return this.HandleGetEditRectangle(g, cellBounds, item, subItemIndex, preferredSize); + } + + #endregion + + #region IRenderer implementation + + // Subclasses will probably want to override these methods rather than the IRenderer + // interface methods. + + /// + /// Draw our data into the given rectangle using the given graphics context. + /// + /// + /// Subclasses should override this method. + /// The graphics context that should be used for drawing + /// The bounds of the subitem cell + /// Returns whether the renderering has already taken place. + /// If this returns false, the default processing will take over. + /// + public virtual bool OptionalRender(Graphics g, Rectangle r) { + if (this.ListView.View != View.Details) + return false; + + this.Render(g, r); + return true; + } + + /// + /// Draw our data into the given rectangle using the given graphics context. + /// + /// + /// Subclasses should override this method if they never want + /// to fall back on the default processing + /// The graphics context that should be used for drawing + /// The bounds of the subitem cell + public virtual void Render(Graphics g, Rectangle r) { + this.StandardRender(g, r); + } + + /// + /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() + /// + /// + /// + /// + /// + protected virtual void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Rectangle r = this.CalculateAlignedRectangle(g, this.Bounds); + this.StandardHitTest(g, hti, r, x, y); + } + + /// + /// Handle a HitTest request after all state information has been initialized + /// + /// + /// + /// + /// + /// + /// + protected virtual Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + // MAINTAINER NOTE: This type testing is wrong (design-wise). The base class should return cell bounds, + // and a more specialized class should return StandardGetEditRectangle(). But BaseRenderer is used directly + // to draw most normal cells, as well as being directly subclassed for user implemented renderers. And this + // method needs to return different bounds in each of those cases. We should have a StandardRenderer and make + // BaseRenderer into an ABC -- but that would break too much existing code. And so we have this hack :( + + // If we are a standard renderer, return the position of the text, otherwise, use the whole cell. + if (this.GetType() == typeof(BaseRenderer)) + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + else + return cellBounds; + } + + #endregion + + #region Standard IRenderer implementations + + /// + /// Draw the standard "[checkbox] [image] [text]" cell after the state properties have been initialized. + /// + /// + /// + protected void StandardRender(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + // Adjust the first columns rectangle to match the padding used by the native mode of the ListView + if (this.ColumnIsPrimary) { + r.X += 3; + r.Width -= 1; + } + r = this.ApplyCellPadding(r); + this.DrawAlignedImageAndText(g, r); + + // Show where the bounds of the cell padding are (debugging) + if (ObjectListView.ShowCellPaddingBounds) + g.DrawRectangle(Pens.Purple, r); + } + + /// + /// Change the bounds of the given rectangle to take any cell padding into account + /// + /// + /// + public virtual Rectangle ApplyCellPadding(Rectangle r) { + Rectangle? padding = this.EffectiveCellPadding; + if (!padding.HasValue) + return r; + // The two subtractions below look wrong, but are correct! + Rectangle paddingRectangle = padding.Value; + r.Width -= paddingRectangle.Right; + r.Height -= paddingRectangle.Bottom; + r.Offset(paddingRectangle.Location); + return r; + } + + /// + /// Perform normal hit testing relative to the given bounds + /// + /// + /// + /// + /// + /// + protected void StandardHitTest(Graphics g, OlvListViewHitTestInfo hti, Rectangle bounds, int x, int y) { + Rectangle r = bounds; + + // Match tweaking from renderer + if (this.ColumnIsPrimary && !(this is TreeListView.TreeRenderer)) { + r.X += 3; + r.Width -= 1; + } + r = ApplyCellPadding(r); + int width = 0; + + // Did they hit a check box on the primary column? + if (this.ColumnIsPrimary && this.ListView.CheckBoxes) { + Rectangle r2 = this.CalculateCheckBoxBounds(g, r); + Rectangle r3 = r2; + r3.Inflate(2, 2); // slightly larger hit area + //g.DrawRectangle(Pens.DarkGreen, r3); + if (r3.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.CheckBox; + return; + } + width = r3.Width; + } + + // Did they hit the image? If they hit the image of a + // non-primary column that has a checkbox, it counts as a + // checkbox hit + r.X += width; + r.Width -= width; + width = this.CalculateImageWidth(g, this.GetImageSelector()); + Rectangle rTwo = r; + rTwo.Width = width; + //g.DrawRectangle(Pens.Red, rTwo); + if (rTwo.Contains(x, y)) { + if (this.Column != null && (this.Column.Index > 0 && this.Column.CheckBoxes)) + hti.HitTestLocation = HitTestLocation.CheckBox; + else + hti.HitTestLocation = HitTestLocation.Image; + return; + } + + // Did they hit the text? + r.X += width; + r.Width -= width; + width = this.CalculateTextWidth(g, this.GetText()); + rTwo = r; + rTwo.Width = width; + //g.DrawRectangle(Pens.Blue, rTwo); + if (rTwo.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.Text; + return; + } + + hti.HitTestLocation = HitTestLocation.InCell; + } + + /// + /// This method calculates the bounds of the text within a standard layout + /// (i.e. optional checkbox, optional image, text) + /// + /// This method only works correctly if the state of the renderer + /// has been fully initialized (see BaseRenderer.GetEditRectangle) + /// + /// + /// + /// + protected Rectangle StandardGetEditRectangle(Graphics g, Rectangle cellBounds, Size preferredSize) { + Rectangle r = this.CalculateAlignedRectangle(g, cellBounds); + r = CalculatePaddedAlignedBounds(g, r, preferredSize); + + int width = this.CalculateCheckBoxWidth(g); + width += this.CalculateImageWidth(g, this.GetImageSelector()); + + // Indent the primary column by the required amount + if (this.ColumnIsPrimary && this.ListItem.IndentCount > 0) { + int indentWidth = this.ListView.SmallImageSize.Width; + width += (indentWidth*this.ListItem.IndentCount); + } + + // If there was either a check box or an image, + // take the check box and the image out of the rectangle, but ensure that + // there is minimum width to the editor + if (width > 0) { + r.X += width; + r.Width = Math.Max(r.Width - width, 40); + } + return r; + } + + /// + /// Apply any padding to the given bounds, and then align a rectangle of the given + /// size within that padded area. + /// + /// + /// + /// + /// + protected Rectangle CalculatePaddedAlignedBounds(Graphics g, Rectangle cellBounds, Size preferredSize) { + Rectangle r = ApplyCellPadding(cellBounds); + r = this.AlignRectangle(r, new Rectangle(0, 0, r.Width, preferredSize.Height)); + return r; + } + + #endregion + + #region Drawing routines + + /// + /// Draw the given image aligned horizontally within the column. + /// + /// + /// Over tall images are scaled to fit. Over-wide images are + /// truncated. This is by design! + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The image to be drawn + protected virtual void DrawAlignedImage(Graphics g, Rectangle r, Image image) { + if (image == null) + return; + + // By default, the image goes in the top left of the rectangle + Rectangle imageBounds = new Rectangle(r.Location, image.Size); + + // If the image is too tall to be drawn in the space provided, proportionally scale it down. + // Too wide images are not scaled. + if (image.Height > r.Height) { + float scaleRatio = (float)r.Height / (float)image.Height; + imageBounds.Width = (int)((float)image.Width * scaleRatio); + imageBounds.Height = r.Height - 1; + } + + // Align and draw our (possibly scaled) image + Rectangle alignRectangle = this.AlignRectangle(r, imageBounds); + if (this.ListItem.Enabled) + g.DrawImage(image, alignRectangle); + else + ControlPaint.DrawImageDisabled(g, image, alignRectangle.X, alignRectangle.Y, GetBackgroundColor()); + } + + /// + /// Draw our subitems image and text + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual void DrawAlignedImageAndText(Graphics g, Rectangle r) { + this.DrawImageAndText(g, this.CalculateAlignedRectangle(g, r)); + } + + /// + /// Fill in the background of this cell + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual void DrawBackground(Graphics g, Rectangle r) { + if (!this.IsDrawBackground) + return; + + Color backgroundColor = this.GetBackgroundColor(); + + using (Brush brush = new SolidBrush(backgroundColor)) { + g.FillRectangle(brush, r.X - 1, r.Y - 1, r.Width + 2, r.Height + 2); + } + } + + /// + /// Draw the check box of this row + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual int DrawCheckBox(Graphics g, Rectangle r) { + // TODO: Unify this with CheckStateRenderer + + if (this.IsPrinting || this.UseCustomCheckboxImages) { + int imageIndex = this.ListItem.StateImageIndex; + if (this.ListView.StateImageList == null || imageIndex < 0 || imageIndex >= this.ListView.StateImageList.Images.Count) + return 0; + + return this.DrawImage(g, r, this.ListView.StateImageList.Images[imageIndex]) + 4; + } + + // The odd constants are to match checkbox placement in native mode (on XP at least) + r = this.CalculateCheckBoxBounds(g, r); + CheckBoxState boxState = this.GetCheckBoxState(this.ListItem.CheckState); + CheckBoxRenderer.DrawCheckBox(g, r.Location, boxState); + + return CheckBoxRenderer.GetGlyphSize(g, boxState).Width + 6; + } + + + /// + /// Calculate the renderer checkboxstate we need to correctly draw the given state + /// + /// + /// + protected virtual CheckBoxState GetCheckBoxState(CheckState checkState) { + + // Should the checkbox be drawn as disabled? + if (this.IsCheckBoxDisabled) { + switch (checkState) { + case CheckState.Checked: return CheckBoxState.CheckedDisabled; + case CheckState.Unchecked: return CheckBoxState.UncheckedDisabled; + default: return CheckBoxState.MixedDisabled; + } + } + + // Is the cursor currently over this checkbox? + if (this.IsItemHot) { + switch (checkState) { + case CheckState.Checked: return CheckBoxState.CheckedHot; + case CheckState.Unchecked: return CheckBoxState.UncheckedHot; + default: return CheckBoxState.MixedHot; + } + } + + // Not hot and not disabled -- just draw it normally + switch (checkState) { + case CheckState.Checked: return CheckBoxState.CheckedNormal; + case CheckState.Unchecked: return CheckBoxState.UncheckedNormal; + default: return CheckBoxState.MixedNormal; + } + + } + + /// + /// Should this checkbox be drawn as disabled? + /// + protected virtual bool IsCheckBoxDisabled { + get { + if (this.ListItem != null && !this.ListItem.Enabled) + return true; + + if (!this.ListView.RenderNonEditableCheckboxesAsDisabled) + return false; + + return (this.ListView.CellEditActivation == ObjectListView.CellEditActivateMode.None || + (this.Column != null && !this.Column.IsEditable)); + } + } + + /// + /// Is the current item hot (i.e. under the mouse)? + /// + protected bool IsItemHot { + get { + return this.ListView != null && + this.ListItem != null && + this.ListView.HotRowIndex == this.ListItem.Index && + this.ListView.HotColumnIndex == (this.Column == null ? 0 : this.Column.Index) && + this.ListView.HotCellHitLocation == HitTestLocation.CheckBox; + } + } + + /// + /// Draw the given text and optional image in the "normal" fashion + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The optional image to be drawn + protected virtual int DrawImage(Graphics g, Rectangle r, Object imageSelector) { + if (imageSelector == null || imageSelector == System.DBNull.Value) + return 0; + + // Draw from the image list (most common case) + ImageList il = this.ListView.SmallImageList; + if (il != null) { + int selectorAsInt = -1; + + if (imageSelector is Int32) { + selectorAsInt = (Int32)imageSelector; + if (selectorAsInt >= il.Images.Count) + selectorAsInt = -1; + } else { + String selectorAsString = imageSelector as String; + if (selectorAsString != null) + selectorAsInt = il.Images.IndexOfKey(selectorAsString); + } + if (selectorAsInt >= 0) { + if (this.IsPrinting) { + // For some reason, printing from an image list doesn't work onto a printer context + // So get the image from the list and fall through to the "print an image" case + imageSelector = il.Images[selectorAsInt]; + } else { + if (il.ImageSize.Height < r.Height) + r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, il.ImageSize)); + + // If we are not printing, it's probable that the given Graphics object is double buffered using a BufferedGraphics object. + // But the ImageList.Draw method doesn't honor the Translation matrix that's probably in effect on the buffered + // graphics. So we have to calculate our drawing rectangle, relative to the cells natural boundaries. + // This effectively simulates the Translation matrix. + + Rectangle r2 = new Rectangle(r.X - this.Bounds.X, r.Y - this.Bounds.Y, r.Width, r.Height); + //il.Draw(g, r2.Location, selectorAsInt); + + // Use this call instead of the above if you want to images to appear blended when selected + NativeMethods.DrawImageList(g, il, selectorAsInt, r2.X, r2.Y, this.IsItemSelected, !this.ListItem.Enabled); + return il.ImageSize.Width; + } + } + } + + // Is the selector actually an image? + Image image = imageSelector as Image; + if (image != null) { + if (image.Size.Height < r.Height) + r.Y = this.AlignVertically(r, new Rectangle(Point.Empty, image.Size)); + + if (this.ListItem.Enabled) + g.DrawImageUnscaled(image, r.X, r.Y); + else + ControlPaint.DrawImageDisabled(g, image, r.X, r.Y, GetBackgroundColor()); + return image.Width; + } + + return 0; + } + + /// + /// Draw our subitems image and text + /// + /// Graphics context to use for drawing + /// Bounds of the cell + protected virtual void DrawImageAndText(Graphics g, Rectangle r) { + int offset = 0; + if (this.ListView.CheckBoxes && this.ColumnIsPrimary) { + offset = this.DrawCheckBox(g, r); + r.X += offset; + r.Width -= offset; + } + + offset = this.DrawImage(g, r, this.GetImageSelector()); + r.X += offset; + r.Width -= offset; + + this.DrawText(g, r, this.GetText()); + } + + /// + /// Draw the given collection of image selectors + /// + /// + /// + /// + protected virtual int DrawImages(Graphics g, Rectangle r, ICollection imageSelectors) { + // Collect the non-null images + List images = new List(); + foreach (Object selector in imageSelectors) { + Image image = this.GetImage(selector); + if (image != null) + images.Add(image); + } + + // Figure out how much space they will occupy + int width = 0; + int height = 0; + foreach (Image image in images) { + width += (image.Width + this.Spacing); + height = Math.Max(height, image.Height); + } + + // Align the collection of images within the cell + Rectangle r2 = this.AlignRectangle(r, new Rectangle(0, 0, width, height)); + + // Finally, draw all the images in their correct location + Color backgroundColor = GetBackgroundColor(); + Point pt = r2.Location; + foreach (Image image in images) { + if (this.ListItem.Enabled) + g.DrawImage(image, pt); + else + ControlPaint.DrawImageDisabled(g, image, pt.X, pt.Y, backgroundColor); + pt.X += (image.Width + this.Spacing); + } + + // Return the width that the images occupy + return width; + } + + /// + /// Draw the given text and optional image in the "normal" fashion + /// + /// Graphics context to use for drawing + /// Bounds of the cell + /// The string to be drawn + public virtual void DrawText(Graphics g, Rectangle r, String txt) { + if (String.IsNullOrEmpty(txt)) + return; + + if (this.UseGdiTextRendering) + this.DrawTextGdi(g, r, txt); + else + this.DrawTextGdiPlus(g, r, txt); + } + + /// + /// Print the given text in the given rectangle using only GDI routines + /// + /// + /// + /// + /// + /// The native list control uses GDI routines to do its drawing, so using them + /// here makes the owner drawn mode looks more natural. + /// This method doesn't honour the CanWrap setting on the renderer. All + /// text is single line + /// + protected virtual void DrawTextGdi(Graphics g, Rectangle r, String txt) { + Color backColor = Color.Transparent; + if (this.IsDrawBackground && this.IsItemSelected && ColumnIsPrimary && !this.ListView.FullRowSelect) + backColor = this.GetTextBackgroundColor(); + + TextFormatFlags flags = TextFormatFlags.EndEllipsis | TextFormatFlags.NoPrefix | + TextFormatFlags.PreserveGraphicsTranslateTransform | + this.CellVerticalAlignmentAsTextFormatFlag; + + // I think there is a bug in the TextRenderer. Setting or not setting SingleLine doesn't make + // any difference -- it is always single line. + if (!this.CanWrap) + flags |= TextFormatFlags.SingleLine; + TextRenderer.DrawText(g, txt, this.Font, r, this.GetForegroundColor(), backColor, flags); + } + + private bool ColumnIsPrimary { + get { return this.Column != null && this.Column.Index == 0; } + } + + /// + /// Gets the cell's vertical alignment as a TextFormatFlag + /// + /// + protected TextFormatFlags CellVerticalAlignmentAsTextFormatFlag { + get { + switch (this.EffectiveCellVerticalAlignment) { + case StringAlignment.Near: + return TextFormatFlags.Top; + case StringAlignment.Center: + return TextFormatFlags.VerticalCenter; + case StringAlignment.Far: + return TextFormatFlags.Bottom; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + /// + /// Gets the StringFormat needed when drawing text using GDI+ + /// + protected virtual StringFormat StringFormatForGdiPlus { + get { + StringFormat fmt = new StringFormat(); + fmt.LineAlignment = this.EffectiveCellVerticalAlignment; + fmt.Trimming = StringTrimming.EllipsisCharacter; + fmt.Alignment = this.Column == null ? StringAlignment.Near : this.Column.TextStringAlign; + if (!this.CanWrap) + fmt.FormatFlags = StringFormatFlags.NoWrap; + return fmt; + } + } + + /// + /// Print the given text in the given rectangle using normal GDI+ .NET methods + /// + /// Printing to a printer dc has to be done using this method. + protected virtual void DrawTextGdiPlus(Graphics g, Rectangle r, String txt) { + using (StringFormat fmt = this.StringFormatForGdiPlus) { + // Draw the background of the text as selected, if it's the primary column + // and it's selected and it's not in FullRowSelect mode. + Font f = this.Font; + if (this.IsDrawBackground && this.IsItemSelected && this.ColumnIsPrimary && !this.ListView.FullRowSelect) { + SizeF size = g.MeasureString(txt, f, r.Width, fmt); + Rectangle r2 = r; + r2.Width = (int)size.Width + 1; + using (Brush brush = new SolidBrush(this.ListView.HighlightBackgroundColorOrDefault)) { + g.FillRectangle(brush, r2); + } + } + RectangleF rf = r; + g.DrawString(txt, f, this.TextBrush, rf, fmt); + } + + // We should put a focus rectange around the column 0 text if it's selected -- + // but we don't because: + // - I really dislike this UI convention + // - we are using buffered graphics, so the DrawFocusRecatangle method of the event doesn't work + + //if (this.ColumnIsPrimary) { + // Size size = TextRenderer.MeasureText(this.SubItem.Text, this.ListView.ListFont); + // if (r.Width > size.Width) + // r.Width = size.Width; + // this.Event.DrawFocusRectangle(r); + //} + } + + #endregion + } + + + /// + /// This renderer highlights substrings that match a given text filter. + /// + public class HighlightTextRenderer : BaseRenderer + { + #region Life and death + + /// + /// Create a HighlightTextRenderer + /// + public HighlightTextRenderer() { + this.FramePen = Pens.DarkGreen; + this.FillBrush = Brushes.Yellow; + } + + /// + /// Create a HighlightTextRenderer + /// + /// + public HighlightTextRenderer(TextMatchFilter filter) + : this() { + this.Filter = filter; + } + + /// + /// Create a HighlightTextRenderer + /// + /// + [Obsolete("Use HighlightTextRenderer(TextMatchFilter) instead", true)] + public HighlightTextRenderer(string text) { + } + + #endregion + + #region Configuration properties + + /// + /// Gets or set how rounded will be the corners of the text match frame + /// + [Category("Appearance"), + DefaultValue(3.0f), + Description("How rounded will be the corners of the text match frame?")] + public float CornerRoundness { + get { return cornerRoundness; } + set { cornerRoundness = value; } + } + private float cornerRoundness = 3.0f; + + /// + /// Gets or set the brush will be used to paint behind the matched substrings. + /// Set this to null to not fill the frame. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush FillBrush { + get { return fillBrush; } + set { fillBrush = value; } + } + private Brush fillBrush; + + /// + /// Gets or sets the filter that is filtering the ObjectListView and for + /// which this renderer should highlight text + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public TextMatchFilter Filter { + get { return filter; } + set { filter = value; } + } + private TextMatchFilter filter; + + /// + /// Gets or set the pen will be used to frame the matched substrings. + /// Set this to null to not draw a frame. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Pen FramePen { + get { return framePen; } + set { framePen = value; } + } + private Pen framePen; + + /// + /// Gets or sets whether the frame around a text match will have rounded corners + /// + [Category("Appearance"), + DefaultValue(true), + Description("Will the frame around a text match will have rounded corners?")] + public bool UseRoundedRectangle { + get { return useRoundedRectangle; } + set { useRoundedRectangle = value; } + } + private bool useRoundedRectangle = true; + + #endregion + + #region Compatibility properties + + /// + /// Gets or set the text that will be highlighted + /// + [Obsolete("Set the Filter directly rather than just the text", true)] + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public string TextToHighlight { + get { return String.Empty; } + set { } + } + + /// + /// Gets or sets the manner in which substring will be compared. + /// + /// + /// Use this to control if substring matches are case sensitive or insensitive. + [Obsolete("Set the Filter directly rather than just this setting", true)] + [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public StringComparison StringComparison { + get { return StringComparison.CurrentCultureIgnoreCase; } + set { } + } + + #endregion + + #region IRenderer interface overrides + + /// + /// Handle a HitTest request after all state information has been initialized + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + } + + #endregion + + #region Rendering + + // This class has two implement two highlighting schemes: one for GDI, another for GDI+. + // Naturally, GDI+ makes the task easier, but we have to provide something for GDI + // since that it is what is normally used. + + /// + /// Draw text using GDI + /// + /// + /// + /// + protected override void DrawTextGdi(Graphics g, Rectangle r, string txt) { + if (this.ShouldDrawHighlighting) + this.DrawGdiTextHighlighting(g, r, txt); + + base.DrawTextGdi(g, r, txt); + } + + /// + /// Draw the highlighted text using GDI + /// + /// + /// + /// + protected virtual void DrawGdiTextHighlighting(Graphics g, Rectangle r, string txt) { + TextFormatFlags flags = TextFormatFlags.NoPrefix | + TextFormatFlags.VerticalCenter | TextFormatFlags.PreserveGraphicsTranslateTransform; + + // TextRenderer puts horizontal padding around the strings, so we need to take + // that into account when measuring strings + int paddingAdjustment = 6; + + // Cache the font + Font f = this.Font; + + foreach (CharacterRange range in this.Filter.FindAllMatchedRanges(txt)) { + // Measure the text that comes before our substring + Size precedingTextSize = Size.Empty; + if (range.First > 0) { + string precedingText = txt.Substring(0, range.First); + precedingTextSize = TextRenderer.MeasureText(g, precedingText, f, r.Size, flags); + precedingTextSize.Width -= paddingAdjustment; + } + + // Measure the length of our substring (may be different each time due to case differences) + string highlightText = txt.Substring(range.First, range.Length); + Size textToHighlightSize = TextRenderer.MeasureText(g, highlightText, f, r.Size, flags); + textToHighlightSize.Width -= paddingAdjustment; + + float textToHighlightLeft = r.X + precedingTextSize.Width + 1; + float textToHighlightTop = this.AlignVertically(r, textToHighlightSize.Height); + + // Draw a filled frame around our substring + this.DrawSubstringFrame(g, textToHighlightLeft, textToHighlightTop, textToHighlightSize.Width, textToHighlightSize.Height); + } + } + + /// + /// Draw an indication around the given frame that shows a text match + /// + /// + /// + /// + /// + /// + protected virtual void DrawSubstringFrame(Graphics g, float x, float y, float width, float height) { + if (this.UseRoundedRectangle) { + using (GraphicsPath path = this.GetRoundedRect(x, y, width, height, 3.0f)) { + if (this.FillBrush != null) + g.FillPath(this.FillBrush, path); + if (this.FramePen != null) + g.DrawPath(this.FramePen, path); + } + } else { + if (this.FillBrush != null) + g.FillRectangle(this.FillBrush, x, y, width, height); + if (this.FramePen != null) + g.DrawRectangle(this.FramePen, x, y, width, height); + } + } + + /// + /// Draw the text using GDI+ + /// + /// + /// + /// + protected override void DrawTextGdiPlus(Graphics g, Rectangle r, string txt) { + if (this.ShouldDrawHighlighting) + this.DrawGdiPlusTextHighlighting(g, r, txt); + + base.DrawTextGdiPlus(g, r, txt); + } + + /// + /// Draw the highlighted text using GDI+ + /// + /// + /// + /// + protected virtual void DrawGdiPlusTextHighlighting(Graphics g, Rectangle r, string txt) { + // Find the substrings we want to highlight + List ranges = new List(this.Filter.FindAllMatchedRanges(txt)); + + if (ranges.Count == 0) + return; + + using (StringFormat fmt = this.StringFormatForGdiPlus) { + RectangleF rf = r; + fmt.SetMeasurableCharacterRanges(ranges.ToArray()); + Region[] stringRegions = g.MeasureCharacterRanges(txt, this.Font, rf, fmt); + + foreach (Region region in stringRegions) { + RectangleF bounds = region.GetBounds(g); + this.DrawSubstringFrame(g, bounds.X - 1, bounds.Y - 1, bounds.Width + 2, bounds.Height); + } + } + } + + #endregion + + #region Utilities + + /// + /// Gets whether the renderer should actually draw highlighting + /// + protected bool ShouldDrawHighlighting { + get { + return this.Column == null || (this.Column.Searchable && this.Filter != null && this.Filter.HasComponents); + } + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// A round cornered rectagle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + /// + /// + /// + /// + /// + protected GraphicsPath GetRoundedRect(float x, float y, float width, float height, float diameter) { + return GetRoundedRect(new RectangleF(x, y, width, height), diameter); + } + + /// + /// Return a GraphicPath that is a round cornered rectangle + /// + /// The rectangle + /// The diameter of the corners + /// A round cornered rectagle path + /// If I could rely on people using C# 3.0+, this should be + /// an extension method of GraphicsPath. + protected GraphicsPath GetRoundedRect(RectangleF rect, float diameter) { + GraphicsPath path = new GraphicsPath(); + + if (diameter > 0) { + RectangleF arc = new RectangleF(rect.X, rect.Y, diameter, diameter); + path.AddArc(arc, 180, 90); + arc.X = rect.Right - diameter; + path.AddArc(arc, 270, 90); + arc.Y = rect.Bottom - diameter; + path.AddArc(arc, 0, 90); + arc.X = rect.Left; + path.AddArc(arc, 90, 90); + path.CloseFigure(); + } else { + path.AddRectangle(rect); + } + + return path; + } + + #endregion + } + + /// + /// This class maps a data value to an image that should be drawn for that value. + /// + /// It is useful for drawing data that is represented as an enum or boolean. + public class MappedImageRenderer : BaseRenderer + { + /// + /// Return a renderer that draw boolean values using the given images + /// + /// Draw this when our data value is true + /// Draw this when our data value is false + /// A Renderer + static public MappedImageRenderer Boolean(Object trueImage, Object falseImage) { + return new MappedImageRenderer(true, trueImage, false, falseImage); + } + + /// + /// Return a renderer that draw tristate boolean values using the given images + /// + /// Draw this when our data value is true + /// Draw this when our data value is false + /// Draw this when our data value is null + /// A Renderer + static public MappedImageRenderer TriState(Object trueImage, Object falseImage, Object nullImage) { + return new MappedImageRenderer(new Object[] { true, trueImage, false, falseImage, null, nullImage }); + } + + /// + /// Make a new empty renderer + /// + public MappedImageRenderer() { + map = new System.Collections.Hashtable(); + } + + /// + /// Make a new renderer that will show the given image when the given key is the aspect value + /// + /// The data value to be matched + /// The image to be shown when the key is matched + public MappedImageRenderer(Object key, Object image) + : this() { + this.Add(key, image); + } + + /// + /// Make a new renderer that will show the given images when it receives the given keys + /// + /// + /// + /// + /// + public MappedImageRenderer(Object key1, Object image1, Object key2, Object image2) + : this() { + this.Add(key1, image1); + this.Add(key2, image2); + } + + /// + /// Build a renderer from the given array of keys and their matching images + /// + /// An array of key/image pairs + public MappedImageRenderer(Object[] keysAndImages) + : this() { + if ((keysAndImages.GetLength(0) % 2) != 0) + throw new ArgumentException("Array must have key/image pairs"); + + for (int i = 0; i < keysAndImages.GetLength(0); i += 2) + this.Add(keysAndImages[i], keysAndImages[i + 1]); + } + + /// + /// Register the image that should be drawn when our Aspect has the data value. + /// + /// Value that the Aspect must match + /// An ImageSelector -- an int, string or image + public void Add(Object value, Object image) { + if (value == null) + this.nullImage = image; + else + map[value] = image; + } + + /// + /// Render our value + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + + ICollection aspectAsCollection = this.Aspect as ICollection; + if (aspectAsCollection == null) + this.RenderOne(g, r, this.Aspect); + else + this.RenderCollection(g, r, aspectAsCollection); + } + + /// + /// Draw a collection of images + /// + /// + /// + /// + protected void RenderCollection(Graphics g, Rectangle r, ICollection imageSelectors) { + ArrayList images = new ArrayList(); + Image image = null; + foreach (Object selector in imageSelectors) { + if (selector == null) + image = this.GetImage(this.nullImage); + else if (map.ContainsKey(selector)) + image = this.GetImage(map[selector]); + else + image = null; + + if (image != null) + images.Add(image); + } + + this.DrawImages(g, r, images); + } + + /// + /// Draw one image + /// + /// + /// + /// + protected void RenderOne(Graphics g, Rectangle r, Object selector) { + Image image = null; + if (selector == null) + image = this.GetImage(this.nullImage); + else + if (map.ContainsKey(selector)) + image = this.GetImage(map[selector]); + + if (image != null) + this.DrawAlignedImage(g, r, image); + } + + #region Private variables + + private Hashtable map; // Track the association between values and images + private Object nullImage; // image to be drawn for null values (since null can't be a key) + + #endregion + } + + /// + /// This renderer draws just a checkbox to match the check state of our model object. + /// + public class CheckStateRenderer : BaseRenderer + { + /// + /// Draw our cell + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + if (this.Column == null) + return; + r = this.ApplyCellPadding(r); + CheckState state = this.Column.GetCheckState(this.RowObject); + if (this.IsPrinting) { + // Renderers don't work onto printer DCs, so we have to draw the image ourselves + string key = ObjectListView.CHECKED_KEY; + if (state == CheckState.Unchecked) + key = ObjectListView.UNCHECKED_KEY; + if (state == CheckState.Indeterminate) + key = ObjectListView.INDETERMINATE_KEY; + this.DrawAlignedImage(g, r, this.ListView.SmallImageList.Images[key]); + } else { + r = this.CalculateCheckBoxBounds(g, r); + CheckBoxRenderer.DrawCheckBox(g, r.Location, this.GetCheckBoxState(state)); + } + } + + + /// + /// Handle the GetEditRectangle request + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); + } + + /// + /// Handle the HitTest request + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Rectangle r = this.CalculateCheckBoxBounds(g, this.Bounds); + if (r.Contains(x, y)) + hti.HitTestLocation = HitTestLocation.CheckBox; + } + } + + /// + /// Render an image that comes from our data source. + /// + /// The image can be sourced from: + /// + /// a byte-array (normally when the image to be shown is + /// stored as a value in a database) + /// an int, which is treated as an index into the image list + /// a string, which is treated first as a file name, and failing that as an index into the image list + /// an ICollection of ints or strings, which will be drawn as consecutive images + /// + /// If an image is an animated GIF, it's state is stored in the SubItem object. + /// By default, the image renderer does not render animations (it begins life with animations paused). + /// To enable animations, you must call Unpause(). + /// In the current implementation (2009-09), each column showing animated gifs must have a + /// different instance of ImageRenderer assigned to it. You cannot share the same instance of + /// an image renderer between two animated gif columns. If you do, only the last column will be + /// animated. + /// + public class ImageRenderer : BaseRenderer + { + /// + /// Make an empty image renderer + /// + public ImageRenderer() { + this.stopwatch = new Stopwatch(); + } + + /// + /// Make an empty image renderer that begins life ready for animations + /// + public ImageRenderer(bool startAnimations) + : this() { + this.Paused = !startAnimations; + } + + /// + /// Finalizer + /// + protected override void Dispose(bool disposing) { + Paused = true; + base.Dispose(disposing); + } + + #region Properties + + /// + /// Should the animations in this renderer be paused? + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool Paused { + get { return isPaused; } + set { + if (this.isPaused == value) + return; + + this.isPaused = value; + if (this.isPaused) { + this.StopTickler(); + this.stopwatch.Stop(); + } else { + this.Tickler.Change(1, Timeout.Infinite); + this.stopwatch.Start(); + } + } + } + private bool isPaused = true; + + private void StopTickler() { + if (this.tickler == null) + return; + + this.tickler.Dispose(); + this.tickler = null; + } + + /// + /// Gets a timer that can be used to trigger redraws on animations + /// + protected Timer Tickler { + get { + if (this.tickler == null) + this.tickler = new System.Threading.Timer(new TimerCallback(this.OnTimer), null, Timeout.Infinite, Timeout.Infinite); + return this.tickler; + } + } + + #endregion + + #region Commands + + /// + /// Pause any animations + /// + public void Pause() { + this.Paused = true; + } + + /// + /// Unpause any animations + /// + public void Unpause() { + this.Paused = false; + } + + #endregion + + #region Drawing + + /// + /// Draw our image + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + if (this.Aspect == null || this.Aspect == System.DBNull.Value) + return; + r = this.ApplyCellPadding(r); + + if (this.Aspect is System.Byte[]) { + this.DrawAlignedImage(g, r, this.GetImageFromAspect()); + } else { + ICollection imageSelectors = this.Aspect as ICollection; + if (imageSelectors == null) + this.DrawAlignedImage(g, r, this.GetImageFromAspect()); + else + this.DrawImages(g, r, imageSelectors); + } + } + + /// + /// Translate our Aspect into an image. + /// + /// The strategy is: + /// If its a byte array, we treat it as an in-memory image + /// If it's an int, we use that as an index into our image list + /// If it's a string, we try to load a file by that name. If we can't, + /// we use the string as an index into our image list. + /// + /// An image + protected Image GetImageFromAspect() { + // If we've already figured out the image, don't do it again + if (this.OLVSubItem != null && this.OLVSubItem.ImageSelector is Image) { + if (this.OLVSubItem.AnimationState == null) + return (Image)this.OLVSubItem.ImageSelector; + else + return this.OLVSubItem.AnimationState.image; + } + + // Try to convert our Aspect into an Image + // If its a byte array, we treat it as an in-memory image + // If it's an int, we use that as an index into our image list + // If it's a string, we try to find a file by that name. + // If we can't, we use the string as an index into our image list. + Image image = null; + if (this.Aspect is System.Byte[]) { + using (MemoryStream stream = new MemoryStream((System.Byte[])this.Aspect)) { + try { + image = Image.FromStream(stream); + } + catch (ArgumentException) { + // ignore + } + } + } else if (this.Aspect is Int32) { + image = this.GetImage(this.Aspect); + } else { + String str = this.Aspect as String; + if (!String.IsNullOrEmpty(str)) { + try { + image = Image.FromFile(str); + } + catch (FileNotFoundException) { + image = this.GetImage(this.Aspect); + } + catch (OutOfMemoryException) { + image = this.GetImage(this.Aspect); + } + } + } + + // If this image is an animation, initialize the animation process + if (this.OLVSubItem != null && AnimationState.IsAnimation(image)) { + this.OLVSubItem.AnimationState = new AnimationState(image); + } + + // Cache the image so we don't repeat this dreary process + if (this.OLVSubItem != null) + this.OLVSubItem.ImageSelector = image; + + return image; + } + + #endregion + + #region Events + + /// + /// This is the method that is invoked by the timer. It basically switches control to the listview thread. + /// + /// not used + public void OnTimer(Object state) { + if (this.ListView == null || this.Paused) + return; + + if (this.ListView.InvokeRequired) + this.ListView.Invoke((MethodInvoker)delegate { this.OnTimer(state); }); + else + this.OnTimerInThread(); + } + + /// + /// This is the OnTimer callback, but invoked in the same thread as the creator of the ListView. + /// This method can use all of ListViews methods without creating a CrossThread exception. + /// + protected void OnTimerInThread() { + // MAINTAINER NOTE: This method must renew the tickler. If it doesn't the animations will stop. + + // If this listview has been destroyed, we can't do anything, so we return without + // renewing the tickler, effectively killing all animations on this renderer + if (this.ListView == null || this.Paused || this.ListView.IsDisposed) + return; + + // If we're not in Detail view or our column has been removed from the list, + // we can't do anything at the moment, but we still renew the tickler because the view may change later. + if (this.ListView.View != System.Windows.Forms.View.Details || this.Column == null || this.Column.Index < 0) { + this.Tickler.Change(1000, Timeout.Infinite); + return; + } + + long elapsedMilliseconds = this.stopwatch.ElapsedMilliseconds; + int subItemIndex = this.Column.Index; + long nextCheckAt = elapsedMilliseconds + 1000; // wait at most one second before checking again + Rectangle updateRect = new Rectangle(); // what part of the view must be updated to draw the changed gifs? + + // Run through all the subitems in the view for our column, and for each one that + // has an animation attached to it, see if the frame needs updating. + + for (int i=0; i= state.currentFrameExpiresAt) { + state.AdvanceFrame(elapsedMilliseconds); + + // Track the area of the view that needs to be redrawn to show the changed images + if (updateRect.IsEmpty) + updateRect = lvsi.Bounds; + else + updateRect = Rectangle.Union(updateRect, lvsi.Bounds); + } + + // Remember the minimum time at which a frame is next due to change + nextCheckAt = Math.Min(nextCheckAt, state.currentFrameExpiresAt); + } + + // Update the part of the listview where frames have changed + if (!updateRect.IsEmpty) + this.ListView.Invalidate(updateRect); + + // Renew the tickler in time for the next frame change + this.Tickler.Change(nextCheckAt - elapsedMilliseconds, Timeout.Infinite); + } + + #endregion + + /// + /// Instances of this class kept track of the animation state of a single image. + /// + internal class AnimationState + { + const int PropertyTagTypeShort = 3; + const int PropertyTagTypeLong = 4; + const int PropertyTagFrameDelay = 0x5100; + const int PropertyTagLoopCount = 0x5101; + + /// + /// Is the given image an animation + /// + /// The image to be tested + /// Is the image an animation? + static public bool IsAnimation(Image image) { + if (image == null) + return false; + else + return (new List(image.FrameDimensionsList)).Contains(FrameDimension.Time.Guid); + } + + /// + /// Create an AnimationState in a quiet state + /// + public AnimationState() { + this.imageDuration = new List(); + } + + /// + /// Create an animation state for the given image, which may or may not + /// be an animation + /// + /// The image to be rendered + public AnimationState(Image image) + : this() { + if (!AnimationState.IsAnimation(image)) + return; + + // How many frames in the animation? + this.image = image; + this.frameCount = this.image.GetFrameCount(FrameDimension.Time); + + // Find the delay between each frame. + // The delays are stored an array of 4-byte ints. Each int is the + // number of 1/100th of a second that should elapsed before the frame expires + foreach (PropertyItem pi in this.image.PropertyItems) { + if (pi.Id == PropertyTagFrameDelay) { + for (int i = 0; i < pi.Len; i += 4) { + //TODO: There must be a better way to convert 4-bytes to an int + int delay = (pi.Value[i + 3] << 24) + (pi.Value[i + 2] << 16) + (pi.Value[i + 1] << 8) + pi.Value[i]; + this.imageDuration.Add(delay * 10); // store delays as milliseconds + } + break; + } + } + + // There should be as many frame durations as frames + Debug.Assert(this.imageDuration.Count == this.frameCount, "There should be as many frame durations as there are frames."); + } + + /// + /// Does this state represent a valid animation + /// + public bool IsValid { + get { + return (this.image != null && this.frameCount > 0); + } + } + + /// + /// Advance our images current frame and calculate when it will expire + /// + public void AdvanceFrame(long millisecondsNow) { + this.currentFrame = (this.currentFrame + 1) % this.frameCount; + this.currentFrameExpiresAt = millisecondsNow + this.imageDuration[this.currentFrame]; + this.image.SelectActiveFrame(FrameDimension.Time, this.currentFrame); + } + + internal int currentFrame; + internal long currentFrameExpiresAt; + internal Image image; + internal List imageDuration; + internal int frameCount; + } + + #region Private variables + + private System.Threading.Timer tickler; // timer used to tickle the animations + private Stopwatch stopwatch; // clock used to time the animation frame changes + + #endregion + } + + /// + /// Render our Aspect as a progress bar + /// + public class BarRenderer : BaseRenderer + { + #region Constructors + + /// + /// Make a BarRenderer + /// + public BarRenderer() + : base() { + } + + /// + /// Make a BarRenderer for the given range of data values + /// + public BarRenderer(int minimum, int maximum) + : this() { + this.MinimumValue = minimum; + this.MaximumValue = maximum; + } + + /// + /// Make a BarRenderer using a custom bar scheme + /// + public BarRenderer(Pen pen, Brush brush) + : this() { + this.Pen = pen; + this.Brush = brush; + this.UseStandardBar = false; + } + + /// + /// Make a BarRenderer using a custom bar scheme + /// + public BarRenderer(int minimum, int maximum, Pen pen, Brush brush) + : this(minimum, maximum) { + this.Pen = pen; + this.Brush = brush; + this.UseStandardBar = false; + } + + /// + /// Make a BarRenderer that uses a horizontal gradient + /// + public BarRenderer(Pen pen, Color start, Color end) + : this() { + this.Pen = pen; + this.SetGradient(start, end); + } + + /// + /// Make a BarRenderer that uses a horizontal gradient + /// + public BarRenderer(int minimum, int maximum, Pen pen, Color start, Color end) + : this(minimum, maximum) { + this.Pen = pen; + this.SetGradient(start, end); + } + + #endregion + + #region Configuration Properties + + /// + /// Should this bar be drawn in the system style? + /// + [Category("ObjectListView"), + Description("Should this bar be drawn in the system style?"), + DefaultValue(true)] + public bool UseStandardBar { + get { return useStandardBar; } + set { useStandardBar = value; } + } + private bool useStandardBar = true; + + /// + /// How many pixels in from our cell border will this bar be drawn + /// + [Category("ObjectListView"), + Description("How many pixels in from our cell border will this bar be drawn"), + DefaultValue(2)] + public int Padding { + get { return padding; } + set { padding = value; } + } + private int padding = 2; + + /// + /// What color will be used to fill the interior of the control before the + /// progress bar is drawn? + /// + [Category("ObjectListView"), + Description("The color of the interior of the bar"), + DefaultValue(typeof(Color), "AliceBlue")] + public Color BackgroundColor { + get { return backgroundColor; } + set { backgroundColor = value; } + } + private Color backgroundColor = Color.AliceBlue; + + /// + /// What color should the frame of the progress bar be? + /// + [Category("ObjectListView"), + Description("What color should the frame of the progress bar be"), + DefaultValue(typeof(Color), "Black")] + public Color FrameColor { + get { return frameColor; } + set { frameColor = value; } + } + private Color frameColor = Color.Black; + + /// + /// How many pixels wide should the frame of the progress bar be? + /// + [Category("ObjectListView"), + Description("How many pixels wide should the frame of the progress bar be"), + DefaultValue(1.0f)] + public float FrameWidth { + get { return frameWidth; } + set { frameWidth = value; } + } + private float frameWidth = 1.0f; + + /// + /// What color should the 'filled in' part of the progress bar be? + /// + /// This is only used if GradientStartColor is Color.Empty + [Category("ObjectListView"), + Description("What color should the 'filled in' part of the progress bar be"), + DefaultValue(typeof(Color), "BlueViolet")] + public Color FillColor { + get { return fillColor; } + set { fillColor = value; } + } + private Color fillColor = Color.BlueViolet; + + /// + /// Use a gradient to fill the progress bar starting with this color + /// + [Category("ObjectListView"), + Description("Use a gradient to fill the progress bar starting with this color"), + DefaultValue(typeof(Color), "CornflowerBlue")] + public Color GradientStartColor { + get { return startColor; } + set { + startColor = value; + } + } + private Color startColor = Color.CornflowerBlue; + + /// + /// Use a gradient to fill the progress bar ending with this color + /// + [Category("ObjectListView"), + Description("Use a gradient to fill the progress bar ending with this color"), + DefaultValue(typeof(Color), "DarkBlue")] + public Color GradientEndColor { + get { return endColor; } + set { + endColor = value; + } + } + private Color endColor = Color.DarkBlue; + + /// + /// Regardless of how wide the column become the progress bar will never be wider than this + /// + [Category("Behavior"), + Description("The progress bar will never be wider than this"), + DefaultValue(100)] + public int MaximumWidth { + get { return maximumWidth; } + set { maximumWidth = value; } + } + private int maximumWidth = 100; + + /// + /// Regardless of how high the cell is the progress bar will never be taller than this + /// + [Category("Behavior"), + Description("The progress bar will never be taller than this"), + DefaultValue(16)] + public int MaximumHeight { + get { return maximumHeight; } + set { maximumHeight = value; } + } + private int maximumHeight = 16; + + /// + /// The minimum data value expected. Values less than this will given an empty bar + /// + [Category("Behavior"), + Description("The minimum data value expected. Values less than this will given an empty bar"), + DefaultValue(0.0)] + public double MinimumValue { + get { return minimumValue; } + set { minimumValue = value; } + } + private double minimumValue = 0.0; + + /// + /// The maximum value for the range. Values greater than this will give a full bar + /// + [Category("Behavior"), + Description("The maximum value for the range. Values greater than this will give a full bar"), + DefaultValue(100.0)] + public double MaximumValue { + get { return maximumValue; } + set { maximumValue = value; } + } + private double maximumValue = 100.0; + + #endregion + + #region Public Properties (non-IDE) + + /// + /// The Pen that will draw the frame surrounding this bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Pen Pen { + get { + if (this.pen == null && !this.FrameColor.IsEmpty) + return new Pen(this.FrameColor, this.FrameWidth); + else + return this.pen; + } + set { + this.pen = value; + } + } + private Pen pen; + + /// + /// The brush that will be used to fill the bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush Brush { + get { + if (this.brush == null && !this.FillColor.IsEmpty) + return new SolidBrush(this.FillColor); + else + return this.brush; + } + set { + this.brush = value; + } + } + private Brush brush; + + /// + /// The brush that will be used to fill the background of the bar + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Brush BackgroundBrush { + get { + if (this.backgroundBrush == null && !this.BackgroundColor.IsEmpty) + return new SolidBrush(this.BackgroundColor); + else + return this.backgroundBrush; + } + set { + this.backgroundBrush = value; + } + } + private Brush backgroundBrush; + + #endregion + + /// + /// Draw this progress bar using a gradient + /// + /// + /// + public void SetGradient(Color start, Color end) { + this.GradientStartColor = start; + this.GradientEndColor = end; + } + + /// + /// Draw our aspect + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + r = this.ApplyCellPadding(r); + + Rectangle frameRect = Rectangle.Inflate(r, 0 - this.Padding, 0 - this.Padding); + frameRect.Width = Math.Min(frameRect.Width, this.MaximumWidth); + frameRect.Height = Math.Min(frameRect.Height, this.MaximumHeight); + frameRect = this.AlignRectangle(r, frameRect); + + // Convert our aspect to a numeric value + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo); + + Rectangle fillRect = Rectangle.Inflate(frameRect, -1, -1); + if (aspectValue <= this.MinimumValue) + fillRect.Width = 0; + else if (aspectValue < this.MaximumValue) + fillRect.Width = (int)(fillRect.Width * (aspectValue - this.MinimumValue) / this.MaximumValue); + + // MS-themed progress bars don't work when printing + if (this.UseStandardBar && ProgressBarRenderer.IsSupported && !this.IsPrinting) { + ProgressBarRenderer.DrawHorizontalBar(g, frameRect); + ProgressBarRenderer.DrawHorizontalChunks(g, fillRect); + } else { + g.FillRectangle(this.BackgroundBrush, frameRect); + if (fillRect.Width > 0) { + // FillRectangle fills inside the given rectangle, so expand it a little + fillRect.Width++; + fillRect.Height++; + if (this.GradientStartColor == Color.Empty) + g.FillRectangle(this.Brush, fillRect); + else { + using (LinearGradientBrush gradient = new LinearGradientBrush(frameRect, this.GradientStartColor, this.GradientEndColor, LinearGradientMode.Horizontal)) { + g.FillRectangle(gradient, fillRect); + } + } + } + g.DrawRectangle(this.Pen, frameRect); + } + } + + /// + /// Handle the GetEditRectangle request + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); + } + } + + /// + /// An ImagesRenderer draws zero or more images depending on the data returned by its Aspect. + /// + /// This renderer's Aspect must return a ICollection of ints, strings or Images, + /// each of which will be drawn horizontally one after the other. + /// As of v2.1, this functionality has been absorbed into ImageRenderer and this is now an + /// empty shell, solely for backwards compatibility. + /// + [ToolboxItem(false)] + public class ImagesRenderer : ImageRenderer + { + } + + /// + /// A MultiImageRenderer draws the same image a number of times based on our data value + /// + /// The stars in the Rating column of iTunes is a good example of this type of renderer. + public class MultiImageRenderer : BaseRenderer + { + /// + /// Make a quiet rendererer + /// + public MultiImageRenderer() + : base() { + } + + /// + /// Make an image renderer that will draw the indicated image, at most maxImages times. + /// + /// + /// + /// + /// + public MultiImageRenderer(Object imageSelector, int maxImages, int minValue, int maxValue) + : this() { + this.ImageSelector = imageSelector; + this.MaxNumberImages = maxImages; + this.MinimumValue = minValue; + this.MaximumValue = maxValue; + } + + #region Configuration Properties + + /// + /// The index of the image that should be drawn + /// + [Category("Behavior"), + Description("The index of the image that should be drawn"), + DefaultValue(-1)] + public int ImageIndex { + get { + if (imageSelector is Int32) + return (Int32)imageSelector; + else + return -1; + } + set { imageSelector = value; } + } + + /// + /// The name of the image that should be drawn + /// + [Category("Behavior"), + Description("The index of the image that should be drawn"), + DefaultValue(null)] + public string ImageName { + get { + return imageSelector as String; + } + set { imageSelector = value; } + } + + /// + /// The image selector that will give the image to be drawn + /// + /// Like all image selectors, this can be an int, string or Image + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Object ImageSelector { + get { return imageSelector; } + set { imageSelector = value; } + } + private Object imageSelector; + + /// + /// What is the maximum number of images that this renderer should draw? + /// + [Category("Behavior"), + Description("The maximum number of images that this renderer should draw"), + DefaultValue(10)] + public int MaxNumberImages { + get { return maxNumberImages; } + set { maxNumberImages = value; } + } + private int maxNumberImages = 10; + + /// + /// Values less than or equal to this will have 0 images drawn + /// + [Category("Behavior"), + Description("Values less than or equal to this will have 0 images drawn"), + DefaultValue(0)] + public int MinimumValue { + get { return minimumValue; } + set { minimumValue = value; } + } + private int minimumValue = 0; + + /// + /// Values greater than or equal to this will have MaxNumberImages images drawn + /// + [Category("Behavior"), + Description("Values greater than or equal to this will have MaxNumberImages images drawn"), + DefaultValue(100)] + public int MaximumValue { + get { return maximumValue; } + set { maximumValue = value; } + } + private int maximumValue = 100; + + #endregion + + /// + /// Draw our data value + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + + Image image = this.GetImage(this.ImageSelector); + if (image == null) + return; + + // Convert our aspect to a numeric value + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + double aspectValue = convertable.ToDouble(NumberFormatInfo.InvariantInfo); + + // Calculate how many images we need to draw to represent our aspect value + int numberOfImages; + if (aspectValue <= this.MinimumValue) + numberOfImages = 0; + else if (aspectValue < this.MaximumValue) + numberOfImages = 1 + (int)(this.MaxNumberImages * (aspectValue - this.MinimumValue) / this.MaximumValue); + else + numberOfImages = this.MaxNumberImages; + + // If we need to shrink the image, what will its on-screen dimensions be? + int imageScaledWidth = image.Width; + int imageScaledHeight = image.Height; + if (r.Height < image.Height) { + imageScaledWidth = (int)((float)image.Width * (float)r.Height / (float)image.Height); + imageScaledHeight = r.Height; + } + // Calculate where the images should be drawn + Rectangle imageBounds = r; + imageBounds.Width = (this.MaxNumberImages * (imageScaledWidth + this.Spacing)) - this.Spacing; + imageBounds.Height = imageScaledHeight; + imageBounds = this.AlignRectangle(r, imageBounds); + + // Finally, draw the images + Color backgroundColor = GetBackgroundColor(); + for (int i = 0; i < numberOfImages; i++) + { + if (this.ListItem.Enabled) + g.DrawImage(image, imageBounds.X, imageBounds.Y, imageScaledWidth, imageScaledHeight); + else + ControlPaint.DrawImageDisabled(g, image, imageBounds.X, imageBounds.Y, backgroundColor); + imageBounds.X += (imageScaledWidth + this.Spacing); + } + } + + /// + /// Handle the GetEditRectangle request + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.CalculatePaddedAlignedBounds(g, cellBounds, preferredSize); + } + } + + + /// + /// A class to render a value that contains a bitwise-OR'ed collection of values. + /// + public class FlagRenderer : BaseRenderer + { + /// + /// Register the given image to the given value + /// + /// When this flag is present... + /// ...draw this image + public void Add(Object key, Object imageSelector) { + Int32 k2 = ((IConvertible)key).ToInt32(NumberFormatInfo.InvariantInfo); + + this.imageMap[k2] = imageSelector; + this.keysInOrder.Remove(k2); + this.keysInOrder.Add(k2); + } + + /// + /// Draw the flags + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + + r = this.ApplyCellPadding(r); + + Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo); + ArrayList images = new ArrayList(); + foreach (Int32 key in this.keysInOrder) { + if ((v2 & key) == key) { + Image image = this.GetImage(this.imageMap[key]); + if (image != null) + images.Add(image); + } + } + if (images.Count > 0) + this.DrawImages(g, r, images); + } + + /// + /// Do the actual work of hit testing. Subclasses should override this rather than HitTest() + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + IConvertible convertable = this.Aspect as IConvertible; + if (convertable == null) + return; + + Int32 v2 = convertable.ToInt32(NumberFormatInfo.InvariantInfo); + + Point pt = this.Bounds.Location; + foreach (Int32 key in this.keysInOrder) { + if ((v2 & key) == key) { + Image image = this.GetImage(this.imageMap[key]); + if (image != null) { + Rectangle imageRect = new Rectangle(pt, image.Size); + if (imageRect.Contains(x, y)) { + hti.UserData = key; + return; + } + pt.X += (image.Width + this.Spacing); + } + } + } + } + + private List keysInOrder = new List(); + private Dictionary imageMap = new Dictionary(); + } + + /// + /// This renderer draws an image, a single line title, and then multi-line descrition + /// under the title. + /// + /// + /// This class works best with FullRowSelect = true. + /// It's not designed to work with cell editing -- it will work but will look odd. + /// + /// This class is experimental. It may not work properly and may disappear from + /// future versions. + /// + /// + public class DescribedTaskRenderer : BaseRenderer + { + /// + /// Create a DescribedTaskRenderer + /// + public DescribedTaskRenderer() { + } + + #region Configuration properties + + /// + /// Gets or set the font that will be used to draw the title of the task + /// + /// If this is null, the ListView's font will be used + [Category("ObjectListView"), + Description("The font that will be used to draw the title of the task"), + DefaultValue(null)] + public Font TitleFont { + get { return titleFont; } + set { titleFont = value; } + } + private Font titleFont; + + /// + /// Return a font that has been set for the title or a reasonable default + /// + [Browsable(false)] + public Font TitleFontOrDefault { + get { + return this.TitleFont ?? this.ListView.Font; + } + } + + /// + /// Gets or set the color of the title of the task + /// + /// This color is used when the task is not selected or when the listview + /// has a translucent selection mechanism. + [Category("ObjectListView"), + Description("The color of the title"), + DefaultValue(typeof(Color), "")] + public Color TitleColor { + get { return titleColor; } + set { titleColor = value; } + } + private Color titleColor; + + /// + /// Return the color of the title of the task or a reasonable default + /// + [Browsable(false)] + public Color TitleColorOrDefault { + get { + if (this.IsItemSelected || this.TitleColor.IsEmpty) + return this.GetForegroundColor(); + else + return this.TitleColor; + } + } + + /// + /// Gets or set the font that will be used to draw the description of the task + /// + /// If this is null, the ListView's font will be used + [Category("ObjectListView"), + Description("The font that will be used to draw the description of the task"), + DefaultValue(null)] + public Font DescriptionFont { + get { return descriptionFont; } + set { descriptionFont = value; } + } + private Font descriptionFont; + + /// + /// Return a font that has been set for the title or a reasonable default + /// + [Browsable(false)] + public Font DescriptionFontOrDefault { + get { + return this.DescriptionFont ?? this.ListView.Font; + } + } + + /// + /// Gets or set the color of the description of the task + /// + /// This color is used when the task is not selected or when the listview + /// has a translucent selection mechanism. + [Category("ObjectListView"), + Description("The color of the description"), + DefaultValue(typeof(Color), "DimGray")] + public Color DescriptionColor { + get { return descriptionColor; } + set { descriptionColor = value; } + } + private Color descriptionColor = Color.DimGray; + + /// + /// Return the color of the description of the task or a reasonable default + /// + [Browsable(false)] + public Color DescriptionColorOrDefault { + get { + if (this.DescriptionColor.IsEmpty || (this.IsItemSelected && !this.ListView.UseTranslucentSelection)) + return this.GetForegroundColor(); + else + return this.DescriptionColor; + } + } + + /// + /// Gets or sets the number of pixels that will be left between the image and the text + /// + [Category("ObjectListView"), + Description("The number of pixels that that will be left between the image and the text"), + DefaultValue(4)] + public int ImageTextSpace { + get { return imageTextSpace; } + set { imageTextSpace = value; } + } + private int imageTextSpace = 4; + + /// + /// Gets or sets the name of the aspect of the model object that contains the task description + /// + [Category("ObjectListView"), + Description("The name of the aspect of the model object that contains the task description"), + DefaultValue(null)] + public string DescriptionAspectName { + get { return descriptionAspectName; } + set { descriptionAspectName = value; } + } + private string descriptionAspectName; + + #endregion + + #region Calculating + + /// + /// Fetch the description from the model class + /// + /// + protected virtual string GetDescription() { + if (String.IsNullOrEmpty(this.DescriptionAspectName)) + return String.Empty; + + if (this.descriptionGetter == null) + this.descriptionGetter = new Munger(this.DescriptionAspectName); + + return this.descriptionGetter.GetValue(this.RowObject) as String; + } + Munger descriptionGetter; + + #endregion + + #region Rendering + + /// + /// Draw our item + /// + /// + /// + public override void Render(Graphics g, Rectangle r) { + this.DrawBackground(g, r); + r = this.ApplyCellPadding(r); + this.DrawDescribedTask(g, r, this.Aspect as String, this.GetDescription(), this.GetImage()); + } + + /// + /// Draw the task + /// + /// + /// + /// + /// + /// + protected virtual void DrawDescribedTask(Graphics g, Rectangle r, string title, string description, Image image) { + Rectangle cellBounds = this.ApplyCellPadding(r); + Rectangle textBounds = cellBounds; + + if (image != null) { + g.DrawImage(image, cellBounds.Location); + int gapToText = image.Width + this.ImageTextSpace; + textBounds.X += gapToText; + textBounds.Width -= gapToText; + } + + // Color the background if the row is selected and we're not using a translucent selection + if (this.IsItemSelected && !this.ListView.UseTranslucentSelection) { + using (SolidBrush b = new SolidBrush(this.GetTextBackgroundColor())) { + g.FillRectangle(b, textBounds); + } + } + + // Draw the title + if (!String.IsNullOrEmpty(title)) { + using (StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap)) { + fmt.Trimming = StringTrimming.EllipsisCharacter; + fmt.Alignment = StringAlignment.Near; + fmt.LineAlignment = StringAlignment.Near; + Font f = this.TitleFontOrDefault; + using (SolidBrush b = new SolidBrush(this.TitleColorOrDefault)) { + g.DrawString(title, f, b, textBounds, fmt); + } + + // How tall was the title? + SizeF size = g.MeasureString(title, f, (int)textBounds.Width, fmt); + textBounds.Y += (int)size.Height; + textBounds.Height -= (int)size.Height; + } + } + + // Draw the description + if (!String.IsNullOrEmpty(description)) { + using (StringFormat fmt2 = new StringFormat()) { + fmt2.Trimming = StringTrimming.EllipsisCharacter; + using (SolidBrush b = new SolidBrush(this.DescriptionColorOrDefault)) { + g.DrawString(description, this.DescriptionFontOrDefault, b, textBounds, fmt2); + } + } + } + } + + #endregion + + #region Hit Testing + + /// + /// Handle the HitTest request + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + if (this.Bounds.Contains(x, y)) + hti.HitTestLocation = HitTestLocation.Text; + } + + #endregion + } +} diff --git a/ObjectListView/Rendering/Styles.cs b/ObjectListView/Rendering/Styles.cs new file mode 100644 index 0000000..0892287 --- /dev/null +++ b/ObjectListView/Rendering/Styles.cs @@ -0,0 +1,400 @@ +/* + * Styles - A style is a group of formatting attributes that can be applied to a row or a cell + * + * Author: Phillip Piper + * Date: 29/07/2009 23:09 + * + * Change log: + * v2.4 + * 2010-03-23 JPP - Added HeaderFormatStyle and support + * v2.3 + * 2009-08-15 JPP - Added Decoration and Overlay properties to HotItemStyle + * 2009-07-29 JPP - Initial version + * + * To do: + * - These should be more generally available. It should be possible to do something like this: + * this.olv.GetItem(i).Style = new ItemStyle(); + * this.olv.GetItem(i).GetSubItem(j).Style = new CellStyle(); + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// The common interface supported by all style objects + /// + public interface IItemStyle + { + /// + /// Gets or set the font that will be used by this style + /// + Font Font { get; set; } + + /// + /// Gets or set the font style + /// + FontStyle FontStyle { get; set; } + + /// + /// Gets or sets the ForeColor + /// + Color ForeColor { get; set; } + + /// + /// Gets or sets the BackColor + /// + Color BackColor { get; set; } + } + + /// + /// Basic implementation of IItemStyle + /// + public class SimpleItemStyle : System.ComponentModel.Component, IItemStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + [DefaultValue(null)] + public Font Font + { + get { return this.font; } + set { this.font = value; } + } + + private Font font; + + /// + /// Gets or sets the style of font that will be applied by this style + /// + [DefaultValue(FontStyle.Regular)] + public FontStyle FontStyle + { + get { return this.fontStyle; } + set { this.fontStyle = value; } + } + + private FontStyle fontStyle; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof (Color), "")] + public Color ForeColor + { + get { return this.foreColor; } + set { this.foreColor = value; } + } + + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof (Color), "")] + public Color BackColor + { + get { return this.backColor; } + set { this.backColor = value; } + } + + private Color backColor; + } + + + /// + /// Instances of this class specify how should "hot items" (non-selected + /// rows under the cursor) be renderered. + /// + public class HotItemStyle : SimpleItemStyle + { + /// + /// Gets or sets the overlay that should be drawn as part of the hot item + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IOverlay Overlay { + get { return this.overlay; } + set { this.overlay = value; } + } + private IOverlay overlay; + + /// + /// Gets or sets the decoration that should be drawn as part of the hot item + /// + /// A decoration is different from an overlay in that an decoration + /// scrolls with the listview contents, whilst an overlay does not. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IDecoration Decoration { + get { return this.decoration; } + set { this.decoration = value; } + } + private IDecoration decoration; + } + + /// + /// This class defines how a cell should be formatted + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class CellStyle : IItemStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets or sets the style of font that will be applied by this style + /// + [DefaultValue(FontStyle.Regular)] + public FontStyle FontStyle { + get { return this.fontStyle; } + set { this.fontStyle = value; } + } + private FontStyle fontStyle; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color ForeColor { + get { return this.foreColor; } + set { this.foreColor = value; } + } + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor; + } + + /// + /// Instances of this class describe how hyperlinks will appear + /// + public class HyperlinkStyle : System.ComponentModel.Component + { + /// + /// Create a HyperlinkStyle + /// + public HyperlinkStyle() { + this.Normal = new CellStyle(); + this.Normal.ForeColor = Color.Blue; + this.Over = new CellStyle(); + this.Over.FontStyle = FontStyle.Underline; + this.Visited = new CellStyle(); + this.Visited.ForeColor = Color.Purple; + this.OverCursor = Cursors.Hand; + } + + /// + /// What sort of formatting should be applied to hyperlinks in their normal state? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn")] + public CellStyle Normal { + get { return this.normalStyle; } + set { this.normalStyle = value; } + } + private CellStyle normalStyle; + + /// + /// What sort of formatting should be applied to hyperlinks when the mouse is over them? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn when the mouse is over them?")] + public CellStyle Over { + get { return this.overStyle; } + set { this.overStyle = value; } + } + private CellStyle overStyle; + + /// + /// What sort of formatting should be applied to hyperlinks after they have been clicked? + /// + [Category("Appearance"), + Description("How should hyperlinks be drawn after they have been clicked")] + public CellStyle Visited { + get { return this.visitedStyle; } + set { this.visitedStyle = value; } + } + private CellStyle visitedStyle; + + /// + /// Gets or sets the cursor that should be shown when the mouse is over a hyperlink. + /// + [Category("Appearance"), + Description("What cursor should be shown when the mouse is over a link?")] + public Cursor OverCursor { + get { return this.overCursor; } + set { this.overCursor = value; } + } + private Cursor overCursor; + } + + /// + /// Instances of this class control one the styling of one particular state + /// (normal, hot, pressed) of a header control + /// + [TypeConverter(typeof(ExpandableObjectConverter))] + public class HeaderStateStyle + { + /// + /// Gets or sets the font that will be applied by this style + /// + [DefaultValue(null)] + public Font Font { + get { return this.font; } + set { this.font = value; } + } + private Font font; + + /// + /// Gets or sets the color of the text that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color ForeColor { + get { return this.foreColor; } + set { this.foreColor = value; } + } + private Color foreColor; + + /// + /// Gets or sets the background color that will be applied by this style + /// + [DefaultValue(typeof(Color), "")] + public Color BackColor { + get { return this.backColor; } + set { this.backColor = value; } + } + private Color backColor; + + /// + /// Gets or sets the color in which a frame will be drawn around the header for this column + /// + [DefaultValue(typeof(Color), "")] + public Color FrameColor { + get { return this.frameColor; } + set { this.frameColor = value; } + } + private Color frameColor; + + /// + /// Gets or sets the width of the frame that will be drawn around the header for this column + /// + [DefaultValue(0.0f)] + public float FrameWidth { + get { return this.frameWidth; } + set { this.frameWidth = value; } + } + private float frameWidth; + } + + /// + /// This class defines how a header should be formatted in its various states. + /// + public class HeaderFormatStyle : System.ComponentModel.Component + { + /// + /// Create a new HeaderFormatStyle + /// + public HeaderFormatStyle() { + this.Hot = new HeaderStateStyle(); + this.Normal = new HeaderStateStyle(); + this.Pressed = new HeaderStateStyle(); + } + + /// + /// What sort of formatting should be applied to a column header when the mouse is over it? + /// + [Category("Appearance"), + Description("How should the header be drawn when the mouse is over it?")] + public HeaderStateStyle Hot { + get { return this.hotStyle; } + set { this.hotStyle = value; } + } + private HeaderStateStyle hotStyle; + + /// + /// What sort of formatting should be applied to a column header in its normal state? + /// + [Category("Appearance"), + Description("How should a column header normally be drawn")] + public HeaderStateStyle Normal { + get { return this.normalStyle; } + set { this.normalStyle = value; } + } + private HeaderStateStyle normalStyle; + + /// + /// What sort of formatting should be applied to a column header when pressed? + /// + [Category("Appearance"), + Description("How should a column header be drawn when it is pressed")] + public HeaderStateStyle Pressed { + get { return this.pressedStyle; } + set { this.pressedStyle = value; } + } + private HeaderStateStyle pressedStyle; + + /// + /// Set the font for all three states + /// + /// + public void SetFont(Font font) { + this.Normal.Font = font; + this.Hot.Font = font; + this.Pressed.Font = font; + } + + /// + /// Set the fore color for all three states + /// + /// + public void SetForeColor(Color color) { + this.Normal.ForeColor = color; + this.Hot.ForeColor = color; + this.Pressed.ForeColor = color; + } + + /// + /// Set the back color for all three states + /// + /// + public void SetBackColor(Color color) { + this.Normal.BackColor = color; + this.Hot.BackColor = color; + this.Pressed.BackColor = color; + } + } +} diff --git a/ObjectListView/Rendering/TreeRenderer.cs b/ObjectListView/Rendering/TreeRenderer.cs new file mode 100644 index 0000000..efe493f --- /dev/null +++ b/ObjectListView/Rendering/TreeRenderer.cs @@ -0,0 +1,248 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Drawing; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; +using System.Drawing.Drawing2D; + +namespace BrightIdeasSoftware { + + public partial class TreeListView { + /// + /// This class handles drawing the tree structure of the primary column. + /// + public class TreeRenderer : HighlightTextRenderer { + /// + /// Create a TreeRenderer + /// + public TreeRenderer() { + this.LinePen = new Pen(Color.Blue, 1.0f); + this.LinePen.DashStyle = DashStyle.Dot; + } + + /// + /// Return the branch that the renderer is currently drawing. + /// + private Branch Branch { + get { + return this.TreeListView.TreeModel.GetBranch(this.RowObject); + } + } + + /// + /// Return the pen that will be used to draw the lines between branches + /// + public Pen LinePen { + get { return linePen; } + set { linePen = value; } + } + private Pen linePen; + + /// + /// Return the TreeListView for which the renderer is being used. + /// + public TreeListView TreeListView { + get { + return (TreeListView)this.ListView; + } + } + + /// + /// Should the renderer draw lines connecting siblings? + /// + public bool IsShowLines { + get { return isShowLines; } + set { isShowLines = value; } + } + private bool isShowLines = true; + + /// + /// How many pixels will be reserved for each level of indentation? + /// + public static int PIXELS_PER_LEVEL = 16 + 1; + + /// + /// The real work of drawing the tree is done in this method + /// + /// + /// + public override void Render(System.Drawing.Graphics g, System.Drawing.Rectangle r) { + this.DrawBackground(g, r); + + Branch br = this.Branch; + + Rectangle paddedRectangle = this.ApplyCellPadding(r); + + Rectangle expandGlyphRectangle = paddedRectangle; + expandGlyphRectangle.Offset((br.Level - 1) * PIXELS_PER_LEVEL, 0); + expandGlyphRectangle.Width = PIXELS_PER_LEVEL; + expandGlyphRectangle.Height = PIXELS_PER_LEVEL; + expandGlyphRectangle.Y = this.AlignVertically(paddedRectangle, expandGlyphRectangle); + int expandGlyphRectangleMidVertical = expandGlyphRectangle.Y + (expandGlyphRectangle.Height/2); + + if (this.IsShowLines) + this.DrawLines(g, r, this.LinePen, br, expandGlyphRectangleMidVertical); + + if (br.CanExpand) + this.DrawExpansionGlyph(g, expandGlyphRectangle, br.IsExpanded); + + int indent = br.Level * PIXELS_PER_LEVEL; + paddedRectangle.Offset(indent, 0); + paddedRectangle.Width -= indent; + + this.DrawImageAndText(g, paddedRectangle); + } + + /// + /// Draw the expansion indicator + /// + /// + /// + /// + protected virtual void DrawExpansionGlyph(Graphics g, Rectangle r, bool isExpanded) { + if (this.UseStyles) { + this.DrawExpansionGlyphStyled(g, r, isExpanded); + } else { + this.DrawExpansionGlyphManual(g, r, isExpanded); + } + } + + /// + /// Gets whether or not we should render using styles + /// + protected virtual bool UseStyles { + get { + return !this.IsPrinting && Application.RenderWithVisualStyles; + } + } + + /// + /// Draw the expansion indicator using styles + /// + /// + /// + /// + protected virtual void DrawExpansionGlyphStyled(Graphics g, Rectangle r, bool isExpanded) { + VisualStyleElement element = VisualStyleElement.TreeView.Glyph.Closed; + if (isExpanded) + element = VisualStyleElement.TreeView.Glyph.Opened; + VisualStyleRenderer renderer = new VisualStyleRenderer(element); + renderer.DrawBackground(g, r); + } + + /// + /// Draw the expansion indicator without using styles + /// + /// + /// + /// + protected virtual void DrawExpansionGlyphManual(Graphics g, Rectangle r, bool isExpanded) { + int h = 8; + int w = 8; + int x = r.X + 4; + int y = r.Y + (r.Height / 2) - 4; + + g.DrawRectangle(new Pen(SystemBrushes.ControlDark), x, y, w, h); + g.FillRectangle(Brushes.White, x + 1, y + 1, w - 1, h - 1); + g.DrawLine(Pens.Black, x + 2, y + 4, x + w - 2, y + 4); + + if (!isExpanded) + g.DrawLine(Pens.Black, x + 4, y + 2, x + 4, y + h - 2); + } + + /// + /// Draw the lines of the tree + /// + /// + /// + /// + /// + /// + protected virtual void DrawLines(Graphics g, Rectangle r, Pen p, Branch br, int glyphMidVertical) { + Rectangle r2 = r; + r2.Width = PIXELS_PER_LEVEL; + + // Vertical lines have to start on even points, otherwise the dotted line looks wrong. + // This is only needed if pen is dotted. + int top = r2.Top; + //if (p.DashStyle == DashStyle.Dot && (top & 1) == 0) + // top += 1; + + // Draw lines for ancestors + int midX; + IList ancestors = br.Ancestors; + foreach (Branch ancestor in ancestors) { + if (!ancestor.IsLastChild && !ancestor.IsOnlyBranch) { + midX = r2.Left + r2.Width / 2; + g.DrawLine(p, midX, top, midX, r2.Bottom); + } + r2.Offset(PIXELS_PER_LEVEL, 0); + } + + // Draw lines for this branch + midX = r2.Left + r2.Width / 2; + + // Horizontal line first + g.DrawLine(p, midX, glyphMidVertical, r2.Right, glyphMidVertical); + + // Vertical line second + if (br.IsFirstBranch) { + if (!br.IsLastChild && !br.IsOnlyBranch) + g.DrawLine(p, midX, glyphMidVertical, midX, r2.Bottom); + } else { + if (br.IsLastChild) + g.DrawLine(p, midX, top, midX, glyphMidVertical); + else + g.DrawLine(p, midX, top, midX, r2.Bottom); + } + } + + /// + /// Do the hit test + /// + /// + /// + /// + /// + protected override void HandleHitTest(Graphics g, OlvListViewHitTestInfo hti, int x, int y) { + Branch br = this.Branch; + + Rectangle r = this.Bounds; + if (br.CanExpand) { + r.Offset((br.Level - 1) * PIXELS_PER_LEVEL, 0); + r.Width = PIXELS_PER_LEVEL; + if (r.Contains(x, y)) { + hti.HitTestLocation = HitTestLocation.ExpandButton; + return; + } + } + + r = this.Bounds; + int indent = br.Level * PIXELS_PER_LEVEL; + r.X += indent; + r.Width -= indent; + + // Ignore events in the indent zone + if (x < r.Left) { + hti.HitTestLocation = HitTestLocation.Nothing; + } else { + this.StandardHitTest(g, hti, r, x, y); + } + } + + /// + /// Calculate the edit rect + /// + /// + /// + /// + /// + /// + /// + protected override Rectangle HandleGetEditRectangle(Graphics g, Rectangle cellBounds, OLVListItem item, int subItemIndex, Size preferredSize) { + return this.StandardGetEditRectangle(g, cellBounds, preferredSize); + } + } + } +} \ No newline at end of file diff --git a/ObjectListView/Resources/clear-filter.png b/ObjectListView/Resources/clear-filter.png new file mode 100644 index 0000000..2ddf707 Binary files /dev/null and b/ObjectListView/Resources/clear-filter.png differ diff --git a/ObjectListView/Resources/coffee.jpg b/ObjectListView/Resources/coffee.jpg new file mode 100644 index 0000000..6032d83 Binary files /dev/null and b/ObjectListView/Resources/coffee.jpg differ diff --git a/ObjectListView/Resources/filter-icons3.png b/ObjectListView/Resources/filter-icons3.png new file mode 100644 index 0000000..8017891 Binary files /dev/null and b/ObjectListView/Resources/filter-icons3.png differ diff --git a/ObjectListView/Resources/filter.png b/ObjectListView/Resources/filter.png new file mode 100644 index 0000000..c09c6d0 Binary files /dev/null and b/ObjectListView/Resources/filter.png differ diff --git a/ObjectListView/Resources/sort-ascending.png b/ObjectListView/Resources/sort-ascending.png new file mode 100644 index 0000000..a21be93 Binary files /dev/null and b/ObjectListView/Resources/sort-ascending.png differ diff --git a/ObjectListView/Resources/sort-descending.png b/ObjectListView/Resources/sort-descending.png new file mode 100644 index 0000000..92dbe63 Binary files /dev/null and b/ObjectListView/Resources/sort-descending.png differ diff --git a/ObjectListView/SubControls/GlassPanelForm.cs b/ObjectListView/SubControls/GlassPanelForm.cs new file mode 100644 index 0000000..ec4ff3b --- /dev/null +++ b/ObjectListView/SubControls/GlassPanelForm.cs @@ -0,0 +1,459 @@ +/* + * GlassPanelForm - A transparent form that is placed over an ObjectListView + * to allow flicker-free overlay images during scrolling. + * + * Author: Phillip Piper + * Date: 14/04/2009 4:36 PM + * + * Change log: + * 2010-08-18 JPP - Added WS_EX_TOOLWINDOW style so that the form won't appear in Alt-Tab list. + * v2.4 + * 2010-03-11 JPP - Work correctly in MDI applications -- more or less. Actually, less than more. + * They don't crash but they don't correctly handle overlapping MDI children. + * Overlays from one control are shown on top of other other windows. + * 2010-03-09 JPP - Correctly Unbind() when the related ObjectListView is disposed. + * 2009-10-28 JPP - Use FindForm() rather than TopMostControl, since the latter doesn't work + * as I expected when the OLV is part of an MDI child window. Thanks to + * wvd_vegt who tracked this down. + * v2.3 + * 2009-08-19 JPP - Only hide the glass pane on resize, not on move + * - Each glass panel now only draws one overlays + * v2.2 + * 2009-06-05 JPP - Handle when owning window is a topmost window + * 2009-04-14 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A GlassPanelForm sits transparently over an ObjectListView to show overlays. + /// + internal partial class GlassPanelForm : Form + { + public GlassPanelForm() { + this.Name = "GlassPanelForm"; + this.Text = "GlassPanelForm"; + + ClientSize = new System.Drawing.Size(0, 0); + ControlBox = false; + FormBorderStyle = FormBorderStyle.None; + SizeGripStyle = SizeGripStyle.Hide; + StartPosition = FormStartPosition.Manual; + MaximizeBox = false; + MinimizeBox = false; + ShowIcon = false; + ShowInTaskbar = false; + FormBorderStyle = FormBorderStyle.None; + + SetStyle(ControlStyles.Selectable, false); + + this.Opacity = 0.5f; + this.BackColor = Color.FromArgb(255, 254, 254, 254); + this.TransparencyKey = this.BackColor; + this.HideGlass(); + NativeMethods.ShowWithoutActivate(this); + } + + protected override void Dispose(bool disposing) { + if (disposing) + this.Unbind(); + + base.Dispose(disposing); + } + + #region Properties + + /// + /// Get the low-level windows flag that will be given to CreateWindow. + /// + protected override CreateParams CreateParams { + get { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x20; // WS_EX_TRANSPARENT + cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW + return cp; + } + } + + #endregion + + #region Commands + + /// + /// Attach this form to the given ObjectListView + /// + public void Bind(ObjectListView olv, IOverlay overlay) { + if (this.objectListView != null) + this.Unbind(); + + this.objectListView = olv; + this.Overlay = overlay; + this.mdiClient = null; + this.mdiOwner = null; + + if (this.objectListView == null) + return; + + // NOTE: If you listen to any events here, you *must* stop listening in Unbind() + + this.objectListView.Disposed += new EventHandler(objectListView_Disposed); + this.objectListView.LocationChanged += new EventHandler(objectListView_LocationChanged); + this.objectListView.SizeChanged += new EventHandler(objectListView_SizeChanged); + this.objectListView.VisibleChanged += new EventHandler(objectListView_VisibleChanged); + this.objectListView.ParentChanged += new EventHandler(objectListView_ParentChanged); + + // Collect our ancestors in the widget hierachy + if (this.ancestors == null) + this.ancestors = new List(); + Control parent = this.objectListView.Parent; + while (parent != null) { + this.ancestors.Add(parent); + parent = parent.Parent; + } + + // Listen for changes in the hierachy + foreach (Control ancestor in this.ancestors) { + ancestor.ParentChanged += new EventHandler(objectListView_ParentChanged); + TabControl tabControl = ancestor as TabControl; + if (tabControl != null) { + tabControl.Selected += new TabControlEventHandler(tabControl_Selected); + } + } + + // Listen for changes in our owning form + this.Owner = this.objectListView.FindForm(); + this.myOwner = this.Owner; + if (this.Owner != null) { + this.Owner.LocationChanged += new EventHandler(Owner_LocationChanged); + this.Owner.SizeChanged += new EventHandler(Owner_SizeChanged); + this.Owner.ResizeBegin += new EventHandler(Owner_ResizeBegin); + this.Owner.ResizeEnd += new EventHandler(Owner_ResizeEnd); + if (this.Owner.TopMost) { + // We can't do this.TopMost = true; since that will activate the panel, + // taking focus away from the owner of the listview + NativeMethods.MakeTopMost(this); + } + + // We need special code to handle MDI + this.mdiOwner = this.Owner.MdiParent; + if (this.mdiOwner != null) { + this.mdiOwner.LocationChanged += new EventHandler(Owner_LocationChanged); + this.mdiOwner.SizeChanged += new EventHandler(Owner_SizeChanged); + this.mdiOwner.ResizeBegin += new EventHandler(Owner_ResizeBegin); + this.mdiOwner.ResizeEnd += new EventHandler(Owner_ResizeEnd); + + // Find the MDIClient control, which houses all MDI children + foreach (Control c in this.mdiOwner.Controls) { + this.mdiClient = c as MdiClient; + if (this.mdiClient != null) { + break; + } + } + if (this.mdiClient != null) { + this.mdiClient.ClientSizeChanged += new EventHandler(myMdiClient_ClientSizeChanged); + } + } + } + + this.UpdateTransparency(); + } + + void myMdiClient_ClientSizeChanged(object sender, EventArgs e) { + this.RecalculateBounds(); + this.Invalidate(); + } + + /// + /// Made the overlay panel invisible + /// + public void HideGlass() { + if (!this.isGlassShown) + return; + this.isGlassShown = false; + this.Bounds = new Rectangle(-10000, -10000, 1, 1); + } + + /// + /// Show the overlay panel in its correctly location + /// + /// + /// If the panel is always shown, this method does nothing. + /// If the panel is being resized, this method also does nothing. + /// + public void ShowGlass() { + if (this.isGlassShown || this.isDuringResizeSequence) + return; + + this.isGlassShown = true; + this.RecalculateBounds(); + } + + /// + /// Detach this glass panel from its previous ObjectListView + /// + /// + /// You should unbind the overlay panel before making any changes to the + /// widget hierarchy. + /// + public void Unbind() { + if (this.objectListView != null) { + this.objectListView.Disposed -= new EventHandler(objectListView_Disposed); + this.objectListView.LocationChanged -= new EventHandler(objectListView_LocationChanged); + this.objectListView.SizeChanged -= new EventHandler(objectListView_SizeChanged); + this.objectListView.VisibleChanged -= new EventHandler(objectListView_VisibleChanged); + this.objectListView.ParentChanged -= new EventHandler(objectListView_ParentChanged); + this.objectListView = null; + } + + if (this.ancestors != null) { + foreach (Control parent in this.ancestors) { + parent.ParentChanged -= new EventHandler(objectListView_ParentChanged); + TabControl tabControl = parent as TabControl; + if (tabControl != null) { + tabControl.Selected -= new TabControlEventHandler(tabControl_Selected); + } + } + this.ancestors = null; + } + + if (this.myOwner != null) { + this.myOwner.LocationChanged -= new EventHandler(Owner_LocationChanged); + this.myOwner.SizeChanged -= new EventHandler(Owner_SizeChanged); + this.myOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin); + this.myOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd); + this.myOwner = null; + } + + if (this.mdiOwner != null) { + this.mdiOwner.LocationChanged -= new EventHandler(Owner_LocationChanged); + this.mdiOwner.SizeChanged -= new EventHandler(Owner_SizeChanged); + this.mdiOwner.ResizeBegin -= new EventHandler(Owner_ResizeBegin); + this.mdiOwner.ResizeEnd -= new EventHandler(Owner_ResizeEnd); + this.mdiOwner = null; + } + + if (this.mdiClient != null) { + this.mdiClient.ClientSizeChanged -= new EventHandler(myMdiClient_ClientSizeChanged); + this.mdiClient = null; + } + } + + #endregion + + #region Event Handlers + + void objectListView_Disposed(object sender, EventArgs e) { + this.Unbind(); + } + + /// + /// Handle when the form that owns the ObjectListView begins to be resized + /// + /// + /// + void Owner_ResizeBegin(object sender, EventArgs e) { + // When the top level window is being resized, we just want to hide + // the overlay window. When the resizing finishes, we want to show + // the overlay window, if it was shown before the resize started. + this.isDuringResizeSequence = true; + this.wasGlassShownBeforeResize = this.isGlassShown; + } + + /// + /// Handle when the form that owns the ObjectListView finished to be resized + /// + /// + /// + void Owner_ResizeEnd(object sender, EventArgs e) { + this.isDuringResizeSequence = false; + if (this.wasGlassShownBeforeResize) + this.ShowGlass(); + } + + /// + /// The owning form has moved. Move the overlay panel too. + /// + /// + /// + void Owner_LocationChanged(object sender, EventArgs e) { + if (this.mdiOwner != null) + this.HideGlass(); + else + this.RecalculateBounds(); + } + + /// + /// The owning form is resizing. Hide our overlay panel until the resizing stops + /// + /// + /// + void Owner_SizeChanged(object sender, EventArgs e) { + this.HideGlass(); + } + + + /// + /// Handle when the bound OLV changes its location. The overlay panel must + /// be moved too, IFF it is currently visible. + /// + /// + /// + void objectListView_LocationChanged(object sender, EventArgs e) { + if (this.isGlassShown) { + this.RecalculateBounds(); + } + } + + /// + /// Handle when the bound OLV changes size. The overlay panel must + /// resize too, IFF it is currently visible. + /// + /// + /// + void objectListView_SizeChanged(object sender, EventArgs e) { + // This event is triggered in all sorts of places, and not always when the size changes. + //if (this.isGlassShown) { + // this.Size = this.objectListView.ClientSize; + //} + } + + /// + /// Handle when the bound OLV is part of a TabControl and that + /// TabControl changes tabs. The overlay panel is hidden. The + /// first time the bound OLV is redrawn, the overlay panel will + /// be shown again. + /// + /// + /// + void tabControl_Selected(object sender, TabControlEventArgs e) { + this.HideGlass(); + } + + /// + /// Somewhere the parent of the bound OLV has changed. Update + /// our events. + /// + /// + /// + void objectListView_ParentChanged(object sender, EventArgs e) { + ObjectListView olv = this.objectListView; + IOverlay overlay = this.Overlay; + this.Unbind(); + this.Bind(olv, overlay); + } + + /// + /// Handle when the bound OLV changes its visibility. + /// The overlay panel should match the OLV's visibility. + /// + /// + /// + void objectListView_VisibleChanged(object sender, EventArgs e) { + if (this.objectListView.Visible) + this.ShowGlass(); + else + this.HideGlass(); + } + + #endregion + + #region Implementation + + protected override void OnPaint(PaintEventArgs e) { + if (this.objectListView == null || this.Overlay == null) + return; + + Graphics g = e.Graphics; + g.TextRenderingHint = ObjectListView.TextRenderingHint; + g.SmoothingMode = ObjectListView.SmoothingMode; + //g.DrawRectangle(new Pen(Color.Green, 4.0f), this.ClientRectangle); + + // If we are part of an MDI app, make sure we don't draw outside the bounds + if (this.mdiClient != null) { + Rectangle r = mdiClient.RectangleToScreen(mdiClient.ClientRectangle); + Rectangle r2 = this.objectListView.RectangleToClient(r); + g.SetClip(r2, System.Drawing.Drawing2D.CombineMode.Intersect); + } + + this.Overlay.Draw(this.objectListView, g, this.objectListView.ClientRectangle); + } + + protected void RecalculateBounds() { + if (!this.isGlassShown) + return; + + Rectangle rect = this.objectListView.ClientRectangle; + rect.X = 0; + rect.Y = 0; + this.Bounds = this.objectListView.RectangleToScreen(rect); + } + + internal void UpdateTransparency() { + ITransparentOverlay transparentOverlay = this.Overlay as ITransparentOverlay; + if (transparentOverlay == null) + this.Opacity = this.objectListView.OverlayTransparency / 255.0f; + else + this.Opacity = transparentOverlay.Transparency / 255.0f; + } + + protected override void WndProc(ref Message m) { + const int WM_NCHITTEST = 132; + const int HTTRANSPARENT = -1; + switch (m.Msg) { + // Ignore all mouse interactions + case WM_NCHITTEST: + m.Result = (IntPtr)HTTRANSPARENT; + break; + } + base.WndProc(ref m); + } + + #endregion + + #region Implementation variables + + internal IOverlay Overlay; + + #endregion + + #region Private variables + + private ObjectListView objectListView; + private bool isDuringResizeSequence; + private bool isGlassShown; + private bool wasGlassShownBeforeResize; + + // Cache these so we can unsubscribe from events even when the OLV has been disposed. + private Form myOwner; + private Form mdiOwner; + private List ancestors; + MdiClient mdiClient; + + #endregion + + } +} diff --git a/ObjectListView/SubControls/HeaderControl.cs b/ObjectListView/SubControls/HeaderControl.cs new file mode 100644 index 0000000..da71f40 --- /dev/null +++ b/ObjectListView/SubControls/HeaderControl.cs @@ -0,0 +1,1218 @@ +/* + * HeaderControl - A limited implementation of HeaderControl + * + * Author: Phillip Piper + * Date: 25/11/2008 17:15 + * + * Change log: + * 2014-09-07 JPP - Added ability to have checkboxes in headers + * + * 2011-05-11 JPP - Fixed bug that prevented columns from being resized in IDE Designer + * by dragging the column divider + * 2011-04-12 JPP - Added ability to draw filter indicator in a column's header + * v2.4.1 + * 2010-08-23 JPP - Added ability to draw header vertically (thanks to Mark Fenwick) + * - Uses OLVColumn.HeaderTextAlign to decide how to align the column's header + * 2010-08-08 JPP - Added ability to have image in header + * v2.4 + * 2010-03-22 JPP - Draw header using header styles + * 2009-10-30 JPP - Plugged GDI resource leak, where font handles were created during custom + * drawing, but never destroyed + * v2.3 + * 2009-10-03 JPP - Handle when ListView.HeaderFormatStyle is None + * 2009-08-24 JPP - Handle the header being destroyed + * v2.2.1 + * 2009-08-16 JPP - Correctly handle header themes + * 2009-08-15 JPP - Added formatting capabilities: font, color, word wrap + * v2.2 + * 2009-06-01 JPP - Use ToolTipControl + * 2009-05-10 JPP - Removed all unsafe code + * 2008-11-25 JPP - Initial version + * + * TO DO: + * - Put drawing code into header style object, so that it can be easily customized. + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Drawing; +using System.Runtime.Remoting.Messaging; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Windows.Forms.VisualStyles; +using System.Drawing.Drawing2D; +using BrightIdeasSoftware.Properties; +using System.Security.Permissions; + +namespace BrightIdeasSoftware { + + /// + /// Class used to capture window messages for the header of the list view + /// control. + /// + public class HeaderControl : NativeWindow { + /// + /// Create a header control for the given ObjectListView. + /// + /// + public HeaderControl(ObjectListView olv) { + this.ListView = olv; + this.AssignHandle(NativeMethods.GetHeaderControl(olv)); + } + + #region Properties + + /// + /// Return the index of the column under the current cursor position, + /// or -1 if the cursor is not over a column + /// + /// Index of the column under the cursor, or -1 + public int ColumnIndexUnderCursor { + get { + Point pt = this.ScrolledCursorPosition; + return NativeMethods.GetColumnUnderPoint(this.Handle, pt); + } + } + + /// + /// Return the Windows handle behind this control + /// + /// + /// When an ObjectListView is initialized as part of a UserControl, the + /// GetHeaderControl() method returns 0 until the UserControl is + /// completely initialized. So the AssignHandle() call in the constructor + /// doesn't work. So we override the Handle property so value is always + /// current. + /// + public new IntPtr Handle { + get { return NativeMethods.GetHeaderControl(this.ListView); } + } + + //TODO: The Handle property may no longer be necessary. CHECK! 2008/11/28 + + /// + /// Gets or sets a style that should be applied to the font of the + /// column's header text when the mouse is over that column + /// + /// THIS IS EXPERIMENTAL. USE AT OWN RISK. August 2009 + [Obsolete("Use HeaderStyle.Hot.FontStyle instead")] + public FontStyle HotFontStyle { + get { return FontStyle.Regular; } + set { } + } + + /// + /// Gets the index of the column under the cursor if the cursor is over it's checkbox + /// + protected int GetColumnCheckBoxUnderCursor() { + Point pt = this.ScrolledCursorPosition; + + int columnIndex = NativeMethods.GetColumnUnderPoint(this.Handle, pt); + return this.IsPointOverHeaderCheckBox(columnIndex, pt) ? columnIndex : -1; + } + + /// + /// Gets the client rectangle for the header + /// + public Rectangle ClientRectangle { + get { + Rectangle r = new Rectangle(); + NativeMethods.GetClientRect(this.Handle, ref r); + return r; + } + } + + /// + /// Return true if the given point is over the checkbox for the given column. + /// + /// + /// + /// + protected bool IsPointOverHeaderCheckBox(int columnIndex, Point pt) { + if (columnIndex < 0 || columnIndex >= this.ListView.Columns.Count) + return false; + + OLVColumn column = this.ListView.GetColumn(columnIndex); + if (!this.HasCheckBox(column)) + return false; + + Rectangle r = this.GetCheckBoxBounds(column); + r.Inflate(1, 1); // make the target slightly bigger + return r.Contains(pt); + } + + /// + /// Gets whether the cursor is over a "locked" divider, i.e. + /// one that cannot be dragged by the user. + /// + protected bool IsCursorOverLockedDivider { + get { + Point pt = this.ScrolledCursorPosition; + int dividerIndex = NativeMethods.GetDividerUnderPoint(this.Handle, pt); + if (dividerIndex >= 0 && dividerIndex < this.ListView.Columns.Count) { + OLVColumn column = this.ListView.GetColumn(dividerIndex); + return column.IsFixedWidth || column.FillsFreeSpace; + } else + return false; + } + } + + private Point ScrolledCursorPosition { + get { + Point pt = this.ListView.PointToClient(Cursor.Position); + pt.X += this.ListView.LowLevelScrollPosition.X; + return pt; + } + } + + /// + /// Gets or sets the listview that this header belongs to + /// + protected ObjectListView ListView { + get { return this.listView; } + set { this.listView = value; } + } + + private ObjectListView listView; + + /// + /// Gets the maximum height of the header. -1 means no maximum. + /// + public int MaximumHeight { + get { return this.ListView.HeaderMaximumHeight; } + } + + /// + /// Get or set the ToolTip that shows tips for the header + /// + public ToolTipControl ToolTip { + get { + if (this.toolTip == null) { + this.CreateToolTip(); + } + return this.toolTip; + } + protected set { this.toolTip = value; } + } + + private ToolTipControl toolTip; + + /// + /// Gets or sets whether the text in column headers should be word + /// wrapped when it is too long to fit within the column + /// + public bool WordWrap { + get { return this.wordWrap; } + set { this.wordWrap = value; } + } + + private bool wordWrap; + + #endregion + + #region Commands + + /// + /// Calculate how height the header needs to be + /// + /// Height in pixels + protected int CalculateHeight(Graphics g) { + TextFormatFlags flags = this.TextFormatFlags; + int columnUnderCursor = this.ColumnIndexUnderCursor; + float height = 0.0f; + for (int i = 0; i < this.ListView.Columns.Count; i++) { + OLVColumn column = this.ListView.GetColumn(i); + height = Math.Max(height, CalculateColumnHeight(g, column, flags, columnUnderCursor == i, i)); + } + return this.MaximumHeight == -1 ? (int) height : Math.Min(this.MaximumHeight, (int) height); + } + + private float CalculateColumnHeight(Graphics g, OLVColumn column, TextFormatFlags flags, bool isHot, int i) { + Font f = this.CalculateFont(column, isHot, false); + if (column.IsHeaderVertical) + return TextRenderer.MeasureText(g, column.Text, f, new Size(10000, 10000), flags).Width; + + const int fudge = 9; // 9 is a magic constant that makes it perfectly match XP behavior + if (!this.WordWrap) + return f.Height + fudge; + + Rectangle r = this.GetHeaderDrawRect(i); + if (this.HasNonThemedSortIndicator(column)) + r.Width -= 16; + if (column.HasHeaderImage) + r.Width -= column.ImageList.ImageSize.Width + 3; + if (this.HasFilterIndicator(column)) + r.Width -= this.CalculateFilterIndicatorWidth(r); + if (this.HasCheckBox(column)) + r.Width -= this.CalculateCheckBoxBounds(g, r).Width; + SizeF size = TextRenderer.MeasureText(g, column.Text, f, new Size(r.Width, 100), flags); + return size.Height + fudge; + } + + /// + /// Get the bounds of the checkbox against the given column + /// + /// + /// + public Rectangle GetCheckBoxBounds(OLVColumn column) { + Rectangle r = this.GetHeaderDrawRect(column.Index); + + using (Graphics g = this.ListView.CreateGraphics()) + return this.CalculateCheckBoxBounds(g, r); + } + + /// + /// Should the given column be drawn with a checkbox against it? + /// + /// + /// + public bool HasCheckBox(OLVColumn column) { + return column.HeaderCheckBox || column.HeaderTriStateCheckBox; + } + + /// + /// Should the given column show a sort indicator? + /// + /// + /// + protected bool HasSortIndicator(OLVColumn column) { + if (!this.ListView.ShowSortIndicators) + return false; + return column == this.ListView.LastSortColumn && this.ListView.LastSortOrder != SortOrder.None; + } + + /// + /// Should the given column be drawn with a filter indicator against it? + /// + /// + /// + protected bool HasFilterIndicator(OLVColumn column) { + return (this.ListView.UseFiltering && this.ListView.UseFilterIndicator && column.HasFilterIndicator); + } + + /// + /// Should the given column show a non-themed sort indicator? + /// + /// + /// + protected bool HasNonThemedSortIndicator(OLVColumn column) { + if (!this.ListView.ShowSortIndicators) + return false; + if (VisualStyleRenderer.IsSupported) + return !VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.SortArrow.SortedUp) && + this.HasSortIndicator(column); + else + return this.HasSortIndicator(column); + } + + /// + /// Return the bounds of the item with the given index + /// + /// + /// + public Rectangle GetItemRect(int itemIndex) { + const int HDM_FIRST = 0x1200; + const int HDM_GETITEMRECT = HDM_FIRST + 7; + NativeMethods.RECT r = new NativeMethods.RECT(); + NativeMethods.SendMessageRECT(this.Handle, HDM_GETITEMRECT, itemIndex, ref r); + return Rectangle.FromLTRB(r.left, r.top, r.right, r.bottom); + } + + /// + /// Return the bounds within which the given column will be drawn + /// + /// + /// + public Rectangle GetHeaderDrawRect(int itemIndex) { + Rectangle r = this.GetItemRect(itemIndex); + + // Tweak the text rectangle a little to improve aethestics + r.Inflate(-3, 0); + r.Y -= 2; + + return r; + } + + /// + /// Force the header to redraw by invalidating it + /// + public void Invalidate() { + NativeMethods.InvalidateRect(this.Handle, 0, true); + } + + /// + /// Force the header to redraw a single column by invalidating it + /// + public void Invalidate(OLVColumn column) { + NativeMethods.InvalidateRect(this.Handle, 0, true); // todo + } + + #endregion + + #region Tooltip + + /// + /// Create a native tool tip control for this listview + /// + protected virtual void CreateToolTip() { + this.ToolTip = new ToolTipControl(); + this.ToolTip.Create(this.Handle); + this.ToolTip.AddTool(this); + this.ToolTip.Showing += new EventHandler(this.ListView.HeaderToolTipShowingCallback); + } + + #endregion + + #region Windows messaging + + /// + /// Override the basic message pump + /// + /// + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + protected override void WndProc(ref Message m) { + const int WM_DESTROY = 2; + const int WM_SETCURSOR = 0x20; + const int WM_NOTIFY = 0x4E; + const int WM_MOUSEMOVE = 0x200; + const int WM_LBUTTONDOWN = 0x201; + const int WM_LBUTTONUP = 0x202; + const int WM_MOUSELEAVE = 675; + const int HDM_FIRST = 0x1200; + const int HDM_LAYOUT = (HDM_FIRST + 5); + + // System.Diagnostics.Debug.WriteLine(String.Format("WndProc: {0}", m.Msg)); + + switch (m.Msg) { + case WM_SETCURSOR: + if (!this.HandleSetCursor(ref m)) + return; + break; + + case WM_NOTIFY: + if (!this.HandleNotify(ref m)) + return; + break; + + case WM_MOUSEMOVE: + if (!this.HandleMouseMove(ref m)) + return; + break; + + case WM_MOUSELEAVE: + if (!this.HandleMouseLeave(ref m)) + return; + break; + + case HDM_LAYOUT: + if (!this.HandleLayout(ref m)) + return; + break; + + case WM_DESTROY: + if (!this.HandleDestroy(ref m)) + return; + break; + + case WM_LBUTTONDOWN: + if (!this.HandleLButtonDown(ref m)) + return; + break; + + case WM_LBUTTONUP: + if (!this.HandleLButtonUp(ref m)) + return; + break; + } + + base.WndProc(ref m); + } + + private bool HandleReflectNotify(ref Message m) + { + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + System.Diagnostics.Debug.WriteLine(String.Format("rn: {0}", nmhdr.code)); + return true; + } + + /// + /// Handle the LButtonDown windows message + /// + /// + /// + protected bool HandleLButtonDown(ref Message m) + { + // Was there a header checkbox under the cursor? + this.columnIndexCheckBoxMouseDown = this.GetColumnCheckBoxUnderCursor(); + if (this.columnIndexCheckBoxMouseDown < 0) + return true; + + // Redraw the header so the checkbox redraws + this.Invalidate(); + + // Force the owning control to ignore this mouse click + // We don't want to sort the listview when they click the checkbox + m.Result = (IntPtr)1; + return false; + } + + private int columnIndexCheckBoxMouseDown = -1; + + /// + /// Handle the LButtonUp windows message + /// + /// + /// + protected bool HandleLButtonUp(ref Message m) { + //System.Diagnostics.Debug.WriteLine("WM_LBUTTONUP"); + + // Was the mouse released over a header checkbox? + if (this.columnIndexCheckBoxMouseDown < 0) + return true; + + // Was the mouse released over the same checkbox on which it was pressed? + if (this.columnIndexCheckBoxMouseDown != this.GetColumnCheckBoxUnderCursor()) + return true; + + // Toggle the header's checkbox + OLVColumn column = this.ListView.GetColumn(this.columnIndexCheckBoxMouseDown); + this.ListView.ToggleHeaderCheckBox(column); + + return true; + } + + /// + /// Handle the SetCursor windows message + /// + /// + /// + protected bool HandleSetCursor(ref Message m) { + if (this.IsCursorOverLockedDivider) { + m.Result = (IntPtr) 1; // Don't change the cursor + return false; + } + return true; + } + + /// + /// Handle the MouseMove windows message + /// + /// + /// + protected bool HandleMouseMove(ref Message m) { + + // Forward the mouse move event to the ListView itself + if (this.ListView.TriggerCellOverEventsWhenOverHeader) { + int x = m.LParam.ToInt32() & 0xFFFF; + int y = (m.LParam.ToInt32() >> 16) & 0xFFFF; + this.ListView.HandleMouseMove(new Point(x, y)); + } + + int columnIndex = this.ColumnIndexUnderCursor; + + // If the mouse has moved to a different header, pop the current tip (if any) + // For some reason, references this.ToolTip when in design mode, causes the + // columns to not be resizable by dragging the divider in the Designer. No idea why. + if (columnIndex != this.columnShowingTip && !this.ListView.IsDesignMode) { + this.ToolTip.PopToolTip(this); + this.columnShowingTip = columnIndex; + } + + // If the mouse has moved onto or away from a checkbox, we need to draw + int checkBoxUnderCursor = this.GetColumnCheckBoxUnderCursor(); + if (checkBoxUnderCursor != this.lastCheckBoxUnderCursor) { + this.Invalidate(); + this.lastCheckBoxUnderCursor = checkBoxUnderCursor; + } + + return true; + } + + private int columnShowingTip = -1; + private int lastCheckBoxUnderCursor = -1; + + /// + /// Handle the MouseLeave windows message + /// + /// + /// + protected bool HandleMouseLeave(ref Message m) { + // Forward the mouse leave event to the ListView itself + if (this.ListView.TriggerCellOverEventsWhenOverHeader) + this.ListView.HandleMouseMove(new Point(-1, -1)); + + return true; + } + + /// + /// Handle the Notify windows message + /// + /// + /// + protected bool HandleNotify(ref Message m) { + // Can this ever happen? JPP 2009-05-22 + if (m.LParam == IntPtr.Zero) + return false; + + NativeMethods.NMHDR nmhdr = (NativeMethods.NMHDR)m.GetLParam(typeof(NativeMethods.NMHDR)); + switch (nmhdr.code) + { + + case ToolTipControl.TTN_SHOW: + //System.Diagnostics.Debug.WriteLine("hdr TTN_SHOW"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandleShow(ref m); + + case ToolTipControl.TTN_POP: + //System.Diagnostics.Debug.WriteLine("hdr TTN_POP"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandlePop(ref m); + + case ToolTipControl.TTN_GETDISPINFO: + //System.Diagnostics.Debug.WriteLine("hdr TTN_GETDISPINFO"); + //System.Diagnostics.Trace.Assert(this.ToolTip.Handle == nmhdr.hwndFrom); + return this.ToolTip.HandleGetDispInfo(ref m); + } + + return false; + } + + /// + /// Handle the CustomDraw windows message + /// + /// + /// + internal virtual bool HandleHeaderCustomDraw(ref Message m) { + const int CDRF_NEWFONT = 2; + const int CDRF_SKIPDEFAULT = 4; + const int CDRF_NOTIFYPOSTPAINT = 0x10; + const int CDRF_NOTIFYITEMDRAW = 0x20; + + const int CDDS_PREPAINT = 1; + const int CDDS_POSTPAINT = 2; + const int CDDS_ITEM = 0x00010000; + const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT); + const int CDDS_ITEMPOSTPAINT = (CDDS_ITEM | CDDS_POSTPAINT); + + NativeMethods.NMCUSTOMDRAW nmcustomdraw = (NativeMethods.NMCUSTOMDRAW) m.GetLParam(typeof (NativeMethods.NMCUSTOMDRAW)); + //System.Diagnostics.Debug.WriteLine(String.Format("header cd: {0:x}, {1}, {2:x}", nmcustomdraw.dwDrawStage, nmcustomdraw.dwItemSpec, nmcustomdraw.uItemState)); + switch (nmcustomdraw.dwDrawStage) { + case CDDS_PREPAINT: + this.cachedNeedsCustomDraw = this.NeedsCustomDraw(); + m.Result = (IntPtr) CDRF_NOTIFYITEMDRAW; + return true; + + case CDDS_ITEMPREPAINT: + int columnIndex = nmcustomdraw.dwItemSpec.ToInt32(); + OLVColumn column = this.ListView.GetColumn(columnIndex); + + // These don't work when visual styles are enabled + //NativeMethods.SetBkColor(nmcustomdraw.hdc, ColorTranslator.ToWin32(Color.Red)); + //NativeMethods.SetTextColor(nmcustomdraw.hdc, ColorTranslator.ToWin32(Color.Blue)); + //m.Result = IntPtr.Zero; + + if (this.cachedNeedsCustomDraw) { + using (Graphics g = Graphics.FromHdc(nmcustomdraw.hdc)) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + this.CustomDrawHeaderCell(g, columnIndex, nmcustomdraw.uItemState); + } + m.Result = (IntPtr) CDRF_SKIPDEFAULT; + } else { + const int CDIS_SELECTED = 1; + bool isPressed = ((nmcustomdraw.uItemState & CDIS_SELECTED) == CDIS_SELECTED); + + // We don't need to modify this based on checkboxes, since there can't be checkboxes if we are here + bool isHot = columnIndex == this.ColumnIndexUnderCursor; + + Font f = this.CalculateFont(column, isHot, isPressed); + + this.fontHandle = f.ToHfont(); + NativeMethods.SelectObject(nmcustomdraw.hdc, this.fontHandle); + + m.Result = (IntPtr) (CDRF_NEWFONT | CDRF_NOTIFYPOSTPAINT); + } + + return true; + + case CDDS_ITEMPOSTPAINT: + if (this.fontHandle != IntPtr.Zero) { + NativeMethods.DeleteObject(this.fontHandle); + this.fontHandle = IntPtr.Zero; + } + break; + } + + return false; + } + + private bool cachedNeedsCustomDraw; + private IntPtr fontHandle; + + /// + /// The message divides a ListView's space between the header and the rows of the listview. + /// The WINDOWPOS structure controls the headers bounds, the RECT controls the listview bounds. + /// + /// + /// + protected bool HandleLayout(ref Message m) { + if (this.ListView.HeaderStyle == ColumnHeaderStyle.None) + return true; + + NativeMethods.HDLAYOUT hdlayout = (NativeMethods.HDLAYOUT) m.GetLParam(typeof (NativeMethods.HDLAYOUT)); + NativeMethods.RECT rect = (NativeMethods.RECT) Marshal.PtrToStructure(hdlayout.prc, typeof (NativeMethods.RECT)); + NativeMethods.WINDOWPOS wpos = (NativeMethods.WINDOWPOS) Marshal.PtrToStructure(hdlayout.pwpos, typeof (NativeMethods.WINDOWPOS)); + + using (Graphics g = this.ListView.CreateGraphics()) { + g.TextRenderingHint = ObjectListView.TextRenderingHint; + int height = this.CalculateHeight(g); + wpos.hwnd = this.Handle; + wpos.hwndInsertAfter = IntPtr.Zero; + wpos.flags = NativeMethods.SWP_FRAMECHANGED; + wpos.x = rect.left; + wpos.y = rect.top; + wpos.cx = rect.right - rect.left; + wpos.cy = height; + + rect.top = height; + + Marshal.StructureToPtr(rect, hdlayout.prc, false); + Marshal.StructureToPtr(wpos, hdlayout.pwpos, false); + } + + this.ListView.BeginInvoke((MethodInvoker) delegate { + this.Invalidate(); + this.ListView.Invalidate(); + }); + return false; + } + + /// + /// Handle when the underlying header control is destroyed + /// + /// + /// + protected bool HandleDestroy(ref Message m) { + if (this.toolTip != null) { + this.toolTip.Showing -= new EventHandler(this.ListView.HeaderToolTipShowingCallback); + } + return false; + } + + #endregion + + #region Rendering + + /// + /// Does this header need to be custom drawn? + /// + /// Word wrapping and colored text require custom drawning. Funnily enough, we + /// can change the font natively. + protected bool NeedsCustomDraw() { + if (this.WordWrap) + return true; + + if (this.ListView.HeaderUsesThemes) + return false; + + if (this.NeedsCustomDraw(this.ListView.HeaderFormatStyle)) + return true; + + foreach (OLVColumn column in this.ListView.Columns) { + if (column.HasHeaderImage || + !column.ShowTextInHeader || + column.IsHeaderVertical || + this.HasFilterIndicator(column) || + this.HasCheckBox(column) || + column.TextAlign != column.HeaderTextAlign || + this.NeedsCustomDraw(column.HeaderFormatStyle)) + return true; + } + return false; + } + + private bool NeedsCustomDraw(HeaderFormatStyle style) { + if (style == null) + return false; + + return (this.NeedsCustomDraw(style.Normal) || + this.NeedsCustomDraw(style.Hot) || + this.NeedsCustomDraw(style.Pressed)); + } + + private bool NeedsCustomDraw(HeaderStateStyle style) { + if (style == null) + return false; + + // If we want fancy colors or frames, we have to custom draw. Oddly enough, we + // can handle font changes without custom drawing. + if (!style.BackColor.IsEmpty) + return true; + + if (style.FrameWidth > 0f && !style.FrameColor.IsEmpty) + return true; + + return (!style.ForeColor.IsEmpty && style.ForeColor != Color.Black); + } + + /// + /// Draw one cell of the header + /// + /// + /// + /// + protected void CustomDrawHeaderCell(Graphics g, int columnIndex, int itemState) { + OLVColumn column = this.ListView.GetColumn(columnIndex); + + bool hasCheckBox = this.HasCheckBox(column); + bool isMouseOverCheckBox = columnIndex == this.lastCheckBoxUnderCursor; + bool isMouseDownOnCheckBox = isMouseOverCheckBox && Control.MouseButtons == MouseButtons.Left; + bool isHot = (columnIndex == this.ColumnIndexUnderCursor) && (!(hasCheckBox && isMouseOverCheckBox)); + + const int CDIS_SELECTED = 1; + bool isPressed = ((itemState & CDIS_SELECTED) == CDIS_SELECTED); + + // System.Diagnostics.Debug.WriteLine(String.Format("{2:hh:mm:ss.ff} - HeaderCustomDraw: {0}, {1}", columnIndex, itemState, DateTime.Now)); + + // Calculate which style should be used for the header + HeaderStateStyle stateStyle = this.CalculateStateStyle(column, isHot, isPressed); + + // If there is an owner drawn delegate installed, give it a chance to draw the header + Rectangle fullCellBounds = this.GetItemRect(columnIndex); + if (column.HeaderDrawing != null) + { + if (!column.HeaderDrawing(g, fullCellBounds, columnIndex, column, isPressed, stateStyle)) + return; + } + + // Draw the background + if (this.ListView.HeaderUsesThemes && + VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.Item.Normal)) + this.DrawThemedBackground(g, fullCellBounds, columnIndex, isPressed, isHot); + else + this.DrawUnthemedBackground(g, fullCellBounds, columnIndex, isPressed, isHot, stateStyle); + + Rectangle r = this.GetHeaderDrawRect(columnIndex); + + // Draw the sort indicator if this column has one + if (this.HasSortIndicator(column)) { + if (this.ListView.HeaderUsesThemes && + VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.SortArrow.SortedUp)) + this.DrawThemedSortIndicator(g, r); + else + r = this.DrawUnthemedSortIndicator(g, r); + } + + if (this.HasFilterIndicator(column)) + r = this.DrawFilterIndicator(g, r); + + if (hasCheckBox) + r = this.DrawCheckBox(g, r, column.HeaderCheckState, column.HeaderCheckBoxDisabled, isMouseOverCheckBox, isMouseDownOnCheckBox); + + // Debugging - Where is the text going to be drawn + // g.DrawRectangle(Pens.Blue, r); + + // Finally draw the text + this.DrawHeaderImageAndText(g, r, column, stateStyle); + } + + private Rectangle DrawCheckBox(Graphics g, Rectangle r, CheckState checkState, bool isDisabled, bool isHot, + bool isPressed) { + CheckBoxState checkBoxState = this.GetCheckBoxState(checkState, isDisabled, isHot, isPressed); + Rectangle checkBoxBounds = this.CalculateCheckBoxBounds(g, r); + CheckBoxRenderer.DrawCheckBox(g, checkBoxBounds.Location, checkBoxState); + + // Move the left edge without changing the right edge + int newX = checkBoxBounds.Right + 3; + r.Width -= (newX - r.X); + r.X = newX; + + return r; + } + + private Rectangle CalculateCheckBoxBounds(Graphics g, Rectangle cellBounds) { + Size checkBoxSize = CheckBoxRenderer.GetGlyphSize(g, CheckBoxState.CheckedNormal); + + // Vertically center the checkbox + int topOffset = (cellBounds.Height - checkBoxSize.Height)/2; + return new Rectangle(cellBounds.X + 3, cellBounds.Y + topOffset, checkBoxSize.Width, checkBoxSize.Height); + } + + private CheckBoxState GetCheckBoxState(CheckState checkState, bool isDisabled, bool isHot, bool isPressed) { + // Should the checkbox be drawn as disabled? + if (isDisabled) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedDisabled; + case CheckState.Unchecked: + return CheckBoxState.UncheckedDisabled; + default: + return CheckBoxState.MixedDisabled; + } + } + + // Is the mouse button currently down? + if (isPressed) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedPressed; + case CheckState.Unchecked: + return CheckBoxState.UncheckedPressed; + default: + return CheckBoxState.MixedPressed; + } + } + + // Is the cursor currently over this checkbox? + if (isHot) { + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedHot; + case CheckState.Unchecked: + return CheckBoxState.UncheckedHot; + default: + return CheckBoxState.MixedHot; + } + } + + // Not hot and not disabled -- just draw it normally + switch (checkState) { + case CheckState.Checked: + return CheckBoxState.CheckedNormal; + case CheckState.Unchecked: + return CheckBoxState.UncheckedNormal; + default: + return CheckBoxState.MixedNormal; + } + } + + /// + /// Draw a background for the header, without using Themes. + /// + /// + /// + /// + /// + /// + /// + protected void DrawUnthemedBackground(Graphics g, Rectangle r, int columnIndex, bool isPressed, bool isHot, HeaderStateStyle stateStyle) { + if (stateStyle.BackColor.IsEmpty) + // I know we're supposed to be drawing the unthemed background, but let's just see if we + // can draw something more interesting than the dull raised block + if (VisualStyleRenderer.IsSupported && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.Item.Normal)) + this.DrawThemedBackground(g, r, columnIndex, isPressed, isHot); + else + ControlPaint.DrawBorder3D(g, r, Border3DStyle.RaisedInner); + else { + using (Brush b = new SolidBrush(stateStyle.BackColor)) + g.FillRectangle(b, r); + } + + // Draw the frame if the style asks for one + if (!stateStyle.FrameColor.IsEmpty && stateStyle.FrameWidth > 0f) { + RectangleF r2 = r; + r2.Inflate(-stateStyle.FrameWidth, -stateStyle.FrameWidth); + using (Pen pen = new Pen(stateStyle.FrameColor, stateStyle.FrameWidth)) + g.DrawRectangle(pen, r2.X, r2.Y, r2.Width, r2.Height); + } + } + + /// + /// Draw a more-or-less pure themed header background. + /// + /// + /// + /// + /// + /// + protected void DrawThemedBackground(Graphics g, Rectangle r, int columnIndex, bool isPressed, bool isHot) { + int part = 1; // normal item + if (columnIndex == 0 && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.ItemLeft.Normal)) + part = 2; // left item + if (columnIndex == this.ListView.Columns.Count - 1 && + VisualStyleRenderer.IsElementDefined(VisualStyleElement.Header.ItemRight.Normal)) + part = 3; // right item + + int state = 1; // normal state + if (isPressed) + state = 3; // pressed + else if (isHot) + state = 2; // hot + + VisualStyleRenderer renderer = new VisualStyleRenderer("HEADER", part, state); + renderer.DrawBackground(g, r); + } + + /// + /// Draw a sort indicator using themes + /// + /// + /// + protected void DrawThemedSortIndicator(Graphics g, Rectangle r) { + VisualStyleRenderer renderer2 = null; + if (this.ListView.LastSortOrder == SortOrder.Ascending) + renderer2 = new VisualStyleRenderer(VisualStyleElement.Header.SortArrow.SortedUp); + if (this.ListView.LastSortOrder == SortOrder.Descending) + renderer2 = new VisualStyleRenderer(VisualStyleElement.Header.SortArrow.SortedDown); + if (renderer2 != null) { + Size sz = renderer2.GetPartSize(g, ThemeSizeType.True); + Point pt = renderer2.GetPoint(PointProperty.Offset); + // GetPoint() should work, but if it doesn't, put the arrow in the top middle + if (pt.X == 0 && pt.Y == 0) + pt = new Point(r.X + (r.Width/2) - (sz.Width/2), r.Y); + renderer2.DrawBackground(g, new Rectangle(pt, sz)); + } + } + + /// + /// Draw a sort indicator without using themes + /// + /// + /// + /// + protected Rectangle DrawUnthemedSortIndicator(Graphics g, Rectangle r) { + // No theme support for sort indicators. So, we draw a triangle at the right edge + // of the column header. + const int triangleHeight = 16; + const int triangleWidth = 16; + const int midX = triangleWidth/2; + const int midY = (triangleHeight/2) - 1; + const int deltaX = midX - 2; + const int deltaY = deltaX/2; + + Point triangleLocation = new Point(r.Right - triangleWidth - 2, r.Top + (r.Height - triangleHeight)/2); + Point[] pts = new Point[] {triangleLocation, triangleLocation, triangleLocation}; + + if (this.ListView.LastSortOrder == SortOrder.Ascending) { + pts[0].Offset(midX - deltaX, midY + deltaY); + pts[1].Offset(midX, midY - deltaY - 1); + pts[2].Offset(midX + deltaX, midY + deltaY); + } else { + pts[0].Offset(midX - deltaX, midY - deltaY); + pts[1].Offset(midX, midY + deltaY); + pts[2].Offset(midX + deltaX, midY - deltaY); + } + + g.FillPolygon(Brushes.SlateGray, pts); + r.Width = r.Width - triangleWidth; + return r; + } + + /// + /// Draw an indication that this column has a filter applied to it + /// + /// + /// + /// + protected Rectangle DrawFilterIndicator(Graphics g, Rectangle r) { + int width = this.CalculateFilterIndicatorWidth(r); + if (width <= 0) + return r; + + Image indicator = Resources.ColumnFilterIndicator; + int x = r.Right - width; + int y = r.Top + (r.Height - indicator.Height)/2; + g.DrawImageUnscaled(indicator, x, y); + + r.Width -= width; + return r; + } + + private int CalculateFilterIndicatorWidth(Rectangle r) { + if (Resources.ColumnFilterIndicator == null || r.Width < 48) + return 0; + return Resources.ColumnFilterIndicator.Width + 1; + } + + /// + /// Draw the header's image and text + /// + /// + /// + /// + /// + protected void DrawHeaderImageAndText(Graphics g, Rectangle r, OLVColumn column, HeaderStateStyle stateStyle) { + + TextFormatFlags flags = this.TextFormatFlags; + flags |= TextFormatFlags.VerticalCenter; + if (column.HeaderTextAlign == HorizontalAlignment.Center) + flags |= TextFormatFlags.HorizontalCenter; + if (column.HeaderTextAlign == HorizontalAlignment.Right) + flags |= TextFormatFlags.Right; + + Font f = this.ListView.HeaderUsesThemes ? this.ListView.Font : stateStyle.Font ?? this.ListView.Font; + Color color = this.ListView.HeaderUsesThemes ? Color.Black : stateStyle.ForeColor; + if (color.IsEmpty) + color = Color.Black; + + const int imageTextGap = 3; + + if (column.IsHeaderVertical) { + DrawVerticalText(g, r, column, f, color); + } else { + // Does the column have a header image and is there space for it? + if (column.HasHeaderImage && r.Width > column.ImageList.ImageSize.Width*2) + DrawImageAndText(g, r, column, flags, f, color, imageTextGap); + else + DrawText(g, r, column, flags, f, color); + } + } + + private void DrawText(Graphics g, Rectangle r, OLVColumn column, TextFormatFlags flags, Font f, Color color) { + if (column.ShowTextInHeader) + TextRenderer.DrawText(g, column.Text, f, r, color, Color.Transparent, flags); + } + + private void DrawImageAndText(Graphics g, Rectangle r, OLVColumn column, TextFormatFlags flags, Font f, + Color color, int imageTextGap) { + Rectangle textRect = r; + textRect.X += (column.ImageList.ImageSize.Width + imageTextGap); + textRect.Width -= (column.ImageList.ImageSize.Width + imageTextGap); + + Size textSize = Size.Empty; + if (column.ShowTextInHeader) + textSize = TextRenderer.MeasureText(g, column.Text, f, textRect.Size, flags); + + int imageY = r.Top + ((r.Height - column.ImageList.ImageSize.Height)/2); + int imageX = textRect.Left; + if (column.HeaderTextAlign == HorizontalAlignment.Center) + imageX = textRect.Left + ((textRect.Width - textSize.Width)/2); + if (column.HeaderTextAlign == HorizontalAlignment.Right) + imageX = textRect.Right - textSize.Width; + imageX -= (column.ImageList.ImageSize.Width + imageTextGap); + + column.ImageList.Draw(g, imageX, imageY, column.ImageList.Images.IndexOfKey(column.HeaderImageKey)); + + this.DrawText(g, textRect, column, flags, f, color); + } + + private static void DrawVerticalText(Graphics g, Rectangle r, OLVColumn column, Font f, Color color) { + try { + // Create a matrix transformation that will rotate the text 90 degrees vertically + // AND place the text in the middle of where it was previously. [Think of tipping + // a box over by its bottom left edge -- you have to move it back a bit so it's + // in the same place as it started] + Matrix m = new Matrix(); + m.RotateAt(-90, new Point(r.X, r.Bottom)); + m.Translate(0, r.Height); + g.Transform = m; + StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap); + fmt.Alignment = StringAlignment.Near; + fmt.LineAlignment = column.HeaderTextAlignAsStringAlignment; + //fmt.Trimming = StringTrimming.EllipsisCharacter; + + // The drawing is rotated 90 degrees, so switch our text boundaries + Rectangle textRect = r; + textRect.Width = r.Height; + textRect.Height = r.Width; + using (Brush b = new SolidBrush(color)) + g.DrawString(column.Text, f, b, textRect, fmt); + } + finally { + g.ResetTransform(); + } + } + + /// + /// Return the header format that should be used for the given column + /// + /// + /// + protected HeaderFormatStyle CalculateHeaderStyle(OLVColumn column) { + return column.HeaderFormatStyle ?? this.ListView.HeaderFormatStyle ?? new HeaderFormatStyle(); + } + + /// + /// What style should be applied to the header? + /// + /// + /// + /// + /// + protected HeaderStateStyle CalculateStateStyle(OLVColumn column, bool isHot, bool isPressed) { + HeaderFormatStyle headerStyle = this.CalculateHeaderStyle(column); + if (this.ListView.IsDesignMode) + return headerStyle.Normal; + if (isPressed) + return headerStyle.Pressed; + if (isHot) + return headerStyle.Hot; + return headerStyle.Normal; + } + + /// + /// What font should be used to draw the header text? + /// + /// + /// + /// + /// + protected Font CalculateFont(OLVColumn column, bool isHot, bool isPressed) { + HeaderStateStyle stateStyle = this.CalculateStateStyle(column, isHot, isPressed); + return stateStyle.Font ?? this.ListView.Font; + } + + /// + /// What flags will be used when drawing text + /// + protected TextFormatFlags TextFormatFlags { + get { + TextFormatFlags flags = TextFormatFlags.EndEllipsis | + TextFormatFlags.NoPrefix | + TextFormatFlags.WordEllipsis | + TextFormatFlags.PreserveGraphicsTranslateTransform; + if (this.WordWrap) + flags |= TextFormatFlags.WordBreak; + else + flags |= TextFormatFlags.SingleLine; + if (this.ListView.RightToLeft == RightToLeft.Yes) + flags |= TextFormatFlags.RightToLeft; + + return flags; + } + } + + /// + /// Perform a HitTest for the header control + /// + /// + /// + /// Null if the given point isn't over the header + internal OlvListViewHitTestInfo.HeaderHitTestInfo HitTest(int x, int y) + { + Rectangle r = this.ClientRectangle; + if (!r.Contains(x, y)) + return null; + + Point pt = new Point(x + this.ListView.LowLevelScrollPosition.X, y); + + OlvListViewHitTestInfo.HeaderHitTestInfo hti = new OlvListViewHitTestInfo.HeaderHitTestInfo(); + hti.ColumnIndex = NativeMethods.GetColumnUnderPoint(this.Handle, pt); + hti.IsOverCheckBox = this.IsPointOverHeaderCheckBox(hti.ColumnIndex, pt); + hti.OverDividerIndex = NativeMethods.GetDividerUnderPoint(this.Handle, pt); + + return hti; + } + + #endregion + } +} diff --git a/ObjectListView/SubControls/ToolStripCheckedListBox.cs b/ObjectListView/SubControls/ToolStripCheckedListBox.cs new file mode 100644 index 0000000..9b1dec0 --- /dev/null +++ b/ObjectListView/SubControls/ToolStripCheckedListBox.cs @@ -0,0 +1,189 @@ +/* + * ToolStripCheckedListBox - Puts a CheckedListBox into a tool strip menu item + * + * Author: Phillip Piper + * Date: 4-March-2011 11:59 pm + * + * Change log: + * 2011-03-04 JPP - First version + * + * Copyright (C) 2011-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Drawing; + +namespace BrightIdeasSoftware { + + /// + /// Instances of this class put a CheckedListBox into a tool strip menu item. + /// + public class ToolStripCheckedListBox : ToolStripControlHost { + + /// + /// Create a ToolStripCheckedListBox + /// + public ToolStripCheckedListBox() + : base(new CheckedListBox()) { + this.CheckedListBoxControl.MaximumSize = new Size(400, 700); + this.CheckedListBoxControl.ThreeDCheckBoxes = true; + this.CheckedListBoxControl.CheckOnClick = true; + this.CheckedListBoxControl.SelectionMode = SelectionMode.One; + } + + /// + /// Gets the control embedded in the menu + /// + public CheckedListBox CheckedListBoxControl { + get { + return Control as CheckedListBox; + } + } + + /// + /// Gets the items shown in the checkedlistbox + /// + public CheckedListBox.ObjectCollection Items { + get { + return this.CheckedListBoxControl.Items; + } + } + + /// + /// Gets or sets whether an item should be checked when it is clicked + /// + public bool CheckedOnClick { + get { + return this.CheckedListBoxControl.CheckOnClick; + } + set { + this.CheckedListBoxControl.CheckOnClick = value; + } + } + + /// + /// Gets a collection of the checked items + /// + public CheckedListBox.CheckedItemCollection CheckedItems { + get { + return this.CheckedListBoxControl.CheckedItems; + } + } + + /// + /// Add a possibly checked item to the control + /// + /// + /// + public void AddItem(object item, bool isChecked) { + this.Items.Add(item); + if (isChecked) + this.CheckedListBoxControl.SetItemChecked(this.Items.Count - 1, true); + } + + /// + /// Add an item with the given state to the control + /// + /// + /// + public void AddItem(object item, CheckState state) { + this.Items.Add(item); + this.CheckedListBoxControl.SetItemCheckState(this.Items.Count - 1, state); + } + + /// + /// Gets the checkedness of the i'th item + /// + /// + /// + public CheckState GetItemCheckState(int i) { + return this.CheckedListBoxControl.GetItemCheckState(i); + } + + /// + /// Set the checkedness of the i'th item + /// + /// + /// + public void SetItemState(int i, CheckState checkState) { + if (i >= 0 && i < this.Items.Count) + this.CheckedListBoxControl.SetItemCheckState(i, checkState); + } + + /// + /// Check all the items in the control + /// + public void CheckAll() { + for (int i = 0; i < this.Items.Count; i++) + this.CheckedListBoxControl.SetItemChecked(i, true); + } + + /// + /// Unchecked all the items in the control + /// + public void UncheckAll() { + for (int i = 0; i < this.Items.Count; i++) + this.CheckedListBoxControl.SetItemChecked(i, false); + } + + #region Events + + /// + /// Listen for events on the underlying control + /// + /// + protected override void OnSubscribeControlEvents(Control c) { + base.OnSubscribeControlEvents(c); + + CheckedListBox control = (CheckedListBox)c; + control.ItemCheck += new ItemCheckEventHandler(OnItemCheck); + } + + /// + /// Stop listening for events on the underlying control + /// + /// + protected override void OnUnsubscribeControlEvents(Control c) { + base.OnUnsubscribeControlEvents(c); + + CheckedListBox control = (CheckedListBox)c; + control.ItemCheck -= new ItemCheckEventHandler(OnItemCheck); + } + + /// + /// Tell the world that an item was checked + /// + public event ItemCheckEventHandler ItemCheck; + + /// + /// Trigger the ItemCheck event + /// + /// + /// + private void OnItemCheck(object sender, ItemCheckEventArgs e) { + if (ItemCheck != null) { + ItemCheck(this, e); + } + } + + #endregion + } +} diff --git a/ObjectListView/SubControls/ToolTipControl.cs b/ObjectListView/SubControls/ToolTipControl.cs new file mode 100644 index 0000000..92d4761 --- /dev/null +++ b/ObjectListView/SubControls/ToolTipControl.cs @@ -0,0 +1,699 @@ +/* + * ToolTipControl - A limited wrapper around a Windows tooltip control + * + * For some reason, the ToolTip class in the .NET framework is implemented in a significantly + * different manner to other controls. For our purposes, the worst of these problems + * is that we cannot get the Handle, so we cannot send Windows level messages to the control. + * + * Author: Phillip Piper + * Date: 2009-05-17 7:22PM + * + * Change log: + * v2.3 + * 2009-06-13 JPP - Moved ToolTipShowingEventArgs to Events.cs + * v2.2 + * 2009-06-06 JPP - Fixed some Vista specific problems + * 2009-05-17 JPP - Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.ComponentModel; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using System.Security.Permissions; + +namespace BrightIdeasSoftware +{ + /// + /// A limited wrapper around a Windows tooltip window. + /// + public class ToolTipControl : NativeWindow + { + #region Constants + + /// + /// These are the standard icons that a tooltip can display. + /// + public enum StandardIcons + { + /// + /// No icon + /// + None = 0, + + /// + /// Info + /// + Info = 1, + + /// + /// Warning + /// + Warning = 2, + + /// + /// Error + /// + Error = 3, + + /// + /// Large info (Vista and later only) + /// + InfoLarge = 4, + + /// + /// Large warning (Vista and later only) + /// + WarningLarge = 5, + + /// + /// Large error (Vista and later only) + /// + ErrorLarge = 6 + } + + const int GWL_STYLE = -16; + const int WM_GETFONT = 0x31; + const int WM_SETFONT = 0x30; + const int WS_BORDER = 0x800000; + const int WS_EX_TOPMOST = 8; + + const int TTM_ADDTOOL = 0x432; + const int TTM_ADJUSTRECT = 0x400 + 31; + const int TTM_DELTOOL = 0x433; + const int TTM_GETBUBBLESIZE = 0x400 + 30; + const int TTM_GETCURRENTTOOL = 0x400 + 59; + const int TTM_GETTIPBKCOLOR = 0x400 + 22; + const int TTM_GETTIPTEXTCOLOR = 0x400 + 23; + const int TTM_GETDELAYTIME = 0x400 + 21; + const int TTM_NEWTOOLRECT = 0x400 + 52; + const int TTM_POP = 0x41c; + const int TTM_SETDELAYTIME = 0x400 + 3; + const int TTM_SETMAXTIPWIDTH = 0x400 + 24; + const int TTM_SETTIPBKCOLOR = 0x400 + 19; + const int TTM_SETTIPTEXTCOLOR = 0x400 + 20; + const int TTM_SETTITLE = 0x400 + 33; + const int TTM_SETTOOLINFO = 0x400 + 54; + + const int TTF_IDISHWND = 1; + //const int TTF_ABSOLUTE = 0x80; + const int TTF_CENTERTIP = 2; + const int TTF_RTLREADING = 4; + const int TTF_SUBCLASS = 0x10; + //const int TTF_TRACK = 0x20; + //const int TTF_TRANSPARENT = 0x100; + const int TTF_PARSELINKS = 0x1000; + + const int TTS_NOPREFIX = 2; + const int TTS_BALLOON = 0x40; + const int TTS_USEVISUALSTYLE = 0x100; + + const int TTN_FIRST = -520; + + /// + /// + /// + public const int TTN_SHOW = (TTN_FIRST - 1); + + /// + /// + /// + public const int TTN_POP = (TTN_FIRST - 2); + + /// + /// + /// + public const int TTN_LINKCLICK = (TTN_FIRST - 3); + + /// + /// + /// + public const int TTN_GETDISPINFO = (TTN_FIRST - 10); + + const int TTDT_AUTOMATIC = 0; + const int TTDT_RESHOW = 1; + const int TTDT_AUTOPOP = 2; + const int TTDT_INITIAL = 3; + + #endregion + + #region Properties + + /// + /// Get or set if the style of the tooltip control + /// + internal int WindowStyle { + get { + return (int)NativeMethods.GetWindowLong(this.Handle, GWL_STYLE); + } + set { + NativeMethods.SetWindowLong(this.Handle, GWL_STYLE, value); + } + } + + /// + /// Get or set if the tooltip should be shown as a ballon + /// + public bool IsBalloon { + get { + return (this.WindowStyle & TTS_BALLOON) == TTS_BALLOON; + } + set { + if (this.IsBalloon == value) + return; + + int windowStyle = this.WindowStyle; + if (value) { + windowStyle |= (TTS_BALLOON | TTS_USEVISUALSTYLE); + // On XP, a border makes the ballon look wrong + if (!ObjectListView.IsVistaOrLater) + windowStyle &= ~WS_BORDER; + } else { + windowStyle &= ~(TTS_BALLOON | TTS_USEVISUALSTYLE); + if (!ObjectListView.IsVistaOrLater) { + if (this.hasBorder) + windowStyle |= WS_BORDER; + else + windowStyle &= ~WS_BORDER; + } + } + this.WindowStyle = windowStyle; + } + } + + /// + /// Get or set if the tooltip should be shown as a ballon + /// + public bool HasBorder { + get { + return this.hasBorder; + } + set { + if (this.hasBorder == value) + return; + + if (value) { + this.WindowStyle |= WS_BORDER; + } else { + this.WindowStyle &= ~WS_BORDER; + } + } + } + private bool hasBorder = true; + + /// + /// Get or set the background color of the tooltip + /// + public Color BackColor { + get { + int color = (int)NativeMethods.SendMessage(this.Handle, TTM_GETTIPBKCOLOR, 0, 0); + return ColorTranslator.FromWin32(color); + } + set { + // For some reason, setting the color fails on Vista and messes up later ops. + // So we don't even try to set it. + if (!ObjectListView.IsVistaOrLater) { + int color = ColorTranslator.ToWin32(value); + NativeMethods.SendMessage(this.Handle, TTM_SETTIPBKCOLOR, color, 0); + //int x2 = Marshal.GetLastWin32Error(); + } + } + } + + /// + /// Get or set the color of the text and border on the tooltip. + /// + public Color ForeColor { + get { + int color = (int)NativeMethods.SendMessage(this.Handle, TTM_GETTIPTEXTCOLOR, 0, 0); + return ColorTranslator.FromWin32(color); + } + set { + // For some reason, setting the color fails on Vista and messes up later ops. + // So we don't even try to set it. + if (!ObjectListView.IsVistaOrLater) { + int color = ColorTranslator.ToWin32(value); + NativeMethods.SendMessage(this.Handle, TTM_SETTIPTEXTCOLOR, color, 0); + } + } + } + + /// + /// Get or set the title that will be shown on the tooltip. + /// + public string Title { + get { + return this.title; + } + set { + if (String.IsNullOrEmpty(value)) + this.title = String.Empty; + else + if (value.Length >= 100) + this.title = value.Substring(0, 99); + else + this.title = value; + NativeMethods.SendMessageString(this.Handle, TTM_SETTITLE, (int)this.standardIcon, this.title); + } + } + private string title; + + /// + /// Get or set the icon that will be shown on the tooltip. + /// + public StandardIcons StandardIcon { + get { + return this.standardIcon; + } + set { + this.standardIcon = value; + NativeMethods.SendMessageString(this.Handle, TTM_SETTITLE, (int)this.standardIcon, this.title); + } + } + private StandardIcons standardIcon; + + /// + /// Gets or sets the font that will be used to draw this control. + /// is still. + /// + /// Setting this to null reverts to the default font. + public Font Font { + get { + IntPtr hfont = NativeMethods.SendMessage(this.Handle, WM_GETFONT, 0, 0); + if (hfont == IntPtr.Zero) + return Control.DefaultFont; + else + return Font.FromHfont(hfont); + } + set { + Font newFont = value ?? Control.DefaultFont; + if (newFont == this.font) + return; + + this.font = newFont; + IntPtr hfont = this.font.ToHfont(); // THINK: When should we delete this hfont? + NativeMethods.SendMessage(this.Handle, WM_SETFONT, hfont, 0); + } + } + private Font font; + + /// + /// Gets or sets how many milliseconds the tooltip will remain visible while the mouse + /// is still. + /// + public int AutoPopDelay { + get { return this.GetDelayTime(TTDT_AUTOPOP); } + set { this.SetDelayTime(TTDT_AUTOPOP, value); } + } + + /// + /// Gets or sets how many milliseconds the mouse must be still before the tooltip is shown. + /// + public int InitialDelay { + get { return this.GetDelayTime(TTDT_INITIAL); } + set { this.SetDelayTime(TTDT_INITIAL, value); } + } + + /// + /// Gets or sets how many milliseconds the mouse must be still before the tooltip is shown again. + /// + public int ReshowDelay { + get { return this.GetDelayTime(TTDT_RESHOW); } + set { this.SetDelayTime(TTDT_RESHOW, value); } + } + + private int GetDelayTime(int which) { + return (int)NativeMethods.SendMessage(this.Handle, TTM_GETDELAYTIME, which, 0); + } + + private void SetDelayTime(int which, int value) { + NativeMethods.SendMessage(this.Handle, TTM_SETDELAYTIME, which, value); + } + + #endregion + + #region Commands + + /// + /// Create the underlying control. + /// + /// The parent of the tooltip + /// This does nothing if the control has already been created + public void Create(IntPtr parentHandle) { + if (this.Handle != IntPtr.Zero) + return; + + CreateParams cp = new CreateParams(); + cp.ClassName = "tooltips_class32"; + cp.Style = TTS_NOPREFIX; + cp.ExStyle = WS_EX_TOPMOST; + cp.Parent = parentHandle; + this.CreateHandle(cp); + + // Ensure that multiline tooltips work correctly + this.SetMaxWidth(); + } + + /// + /// Take a copy of the current settings and restore them when the + /// tooltip is poppped. + /// + /// + /// This call cannot be nested. Subsequent calls to this method will be ignored + /// until PopSettings() is called. + /// + public void PushSettings() { + // Ignore any nested calls + if (this.settings != null) + return; + this.settings = new Hashtable(); + this.settings["IsBalloon"] = this.IsBalloon; + this.settings["HasBorder"] = this.HasBorder; + this.settings["BackColor"] = this.BackColor; + this.settings["ForeColor"] = this.ForeColor; + this.settings["Title"] = this.Title; + this.settings["StandardIcon"] = this.StandardIcon; + this.settings["AutoPopDelay"] = this.AutoPopDelay; + this.settings["InitialDelay"] = this.InitialDelay; + this.settings["ReshowDelay"] = this.ReshowDelay; + this.settings["Font"] = this.Font; + } + private Hashtable settings; + + /// + /// Restore the settings of the tooltip as they were when PushSettings() + /// was last called. + /// + public void PopSettings() { + if (this.settings == null) + return; + + this.IsBalloon = (bool)this.settings["IsBalloon"]; + this.HasBorder = (bool)this.settings["HasBorder"]; + this.BackColor = (Color)this.settings["BackColor"]; + this.ForeColor = (Color)this.settings["ForeColor"]; + this.Title = (string)this.settings["Title"]; + this.StandardIcon = (StandardIcons)this.settings["StandardIcon"]; + this.AutoPopDelay = (int)this.settings["AutoPopDelay"]; + this.InitialDelay = (int)this.settings["InitialDelay"]; + this.ReshowDelay = (int)this.settings["ReshowDelay"]; + this.Font = (Font)this.settings["Font"]; + + this.settings = null; + } + + /// + /// Add the given window to those for whom this tooltip will show tips + /// + /// The window + public void AddTool(IWin32Window window) { + NativeMethods.TOOLINFO lParam = this.MakeToolInfoStruct(window); + NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_ADDTOOL, 0, lParam); + } + + /// + /// Hide any currently visible tooltip + /// + /// + public void PopToolTip(IWin32Window window) { + NativeMethods.SendMessage(this.Handle, TTM_POP, 0, 0); + } + + //public void Munge() { + // NativeMethods.TOOLINFO tool = new NativeMethods.TOOLINFO(); + // IntPtr result = NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_GETCURRENTTOOL, 0, tool); + // System.Diagnostics.Trace.WriteLine("-"); + // System.Diagnostics.Trace.WriteLine(result); + // result = NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_GETBUBBLESIZE, 0, tool); + // System.Diagnostics.Trace.WriteLine(String.Format("{0} {1}", result.ToInt32() >> 16, result.ToInt32() & 0xFFFF)); + // NativeMethods.ChangeSize(this, result.ToInt32() & 0xFFFF, result.ToInt32() >> 16); + // //NativeMethods.RECT r = new NativeMethods.RECT(); + // //r.right + // //IntPtr x = NativeMethods.SendMessageRECT(this.Handle, TTM_ADJUSTRECT, true, ref r); + + // //System.Diagnostics.Trace.WriteLine(String.Format("{0} {1} {2} {3}", r.left, r.top, r.right, r.bottom)); + //} + + /// + /// Remove the given window from those managed by this tooltip + /// + /// + public void RemoveToolTip(IWin32Window window) { + NativeMethods.TOOLINFO lParam = this.MakeToolInfoStruct(window); + NativeMethods.SendMessageTOOLINFO(this.Handle, TTM_DELTOOL, 0, lParam); + } + + /// + /// Set the maximum width of a tooltip string. + /// + public void SetMaxWidth() { + this.SetMaxWidth(SystemInformation.MaxWindowTrackSize.Width); + } + + /// + /// Set the maximum width of a tooltip string. + /// + /// Setting this ensures that line breaks in the tooltip are honoured. + public void SetMaxWidth(int maxWidth) { + NativeMethods.SendMessage(this.Handle, TTM_SETMAXTIPWIDTH, 0, maxWidth); + } + + #endregion + + #region Implementation + + /// + /// Make a TOOLINFO structure for the given window + /// + /// + /// A filled in TOOLINFO + private NativeMethods.TOOLINFO MakeToolInfoStruct(IWin32Window window) { + + NativeMethods.TOOLINFO toolinfo_tooltip = new NativeMethods.TOOLINFO(); + toolinfo_tooltip.hwnd = window.Handle; + toolinfo_tooltip.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + toolinfo_tooltip.uId = window.Handle; + toolinfo_tooltip.lpszText = (IntPtr)(-1); // LPSTR_TEXTCALLBACK + + return toolinfo_tooltip; + } + + /// + /// Handle a WmNotify message + /// + /// The msg + /// True if the message has been handled + protected virtual bool HandleNotify(ref Message msg) { + + //THINK: What do we have to do here? Nothing it seems :) + + //NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)msg.GetLParam(typeof(NativeMethods.NMHEADER)); + //System.Diagnostics.Trace.WriteLine("HandleNotify"); + //System.Diagnostics.Trace.WriteLine(nmheader.nhdr.code); + + //switch (nmheader.nhdr.code) { + //} + + return false; + } + + /// + /// Handle a get display info message + /// + /// The msg + /// True if the message has been handled + public virtual bool HandleGetDispInfo(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleGetDispInfo"); + this.SetMaxWidth(); + ToolTipShowingEventArgs args = new ToolTipShowingEventArgs(); + args.ToolTipControl = this; + this.OnShowing(args); + if (String.IsNullOrEmpty(args.Text)) + return false; + + this.ApplyEventFormatting(args); + + NativeMethods.NMTTDISPINFO dispInfo = (NativeMethods.NMTTDISPINFO)msg.GetLParam(typeof(NativeMethods.NMTTDISPINFO)); + dispInfo.lpszText = args.Text; + dispInfo.hinst = IntPtr.Zero; + if (args.RightToLeft == RightToLeft.Yes) + dispInfo.uFlags |= TTF_RTLREADING; + Marshal.StructureToPtr(dispInfo, msg.LParam, false); + + return true; + } + + private void ApplyEventFormatting(ToolTipShowingEventArgs args) { + if (!args.IsBalloon.HasValue && + !args.BackColor.HasValue && + !args.ForeColor.HasValue && + args.Title == null && + !args.StandardIcon.HasValue && + !args.AutoPopDelay.HasValue && + args.Font == null) + return; + + this.PushSettings(); + if (args.IsBalloon.HasValue) + this.IsBalloon = args.IsBalloon.Value; + if (args.BackColor.HasValue) + this.BackColor = args.BackColor.Value; + if (args.ForeColor.HasValue) + this.ForeColor = args.ForeColor.Value; + if (args.StandardIcon.HasValue) + this.StandardIcon = args.StandardIcon.Value; + if (args.AutoPopDelay.HasValue) + this.AutoPopDelay = args.AutoPopDelay.Value; + if (args.Font != null) + this.Font = args.Font; + if (args.Title != null) + this.Title = args.Title; + } + + /// + /// Handle a TTN_LINKCLICK message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandleLinkClick(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleLinkClick"); + return false; + } + + /// + /// Handle a TTN_POP message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandlePop(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandlePop"); + this.PopSettings(); + return true; + } + + /// + /// Handle a TTN_SHOW message + /// + /// The msg + /// True if the message has been handled + /// This cannot call base.WndProc() since the msg may have come from another control. + public virtual bool HandleShow(ref Message msg) { + //System.Diagnostics.Trace.WriteLine("HandleShow"); + return false; + } + + /// + /// Handle a reflected notify message + /// + /// The msg + /// True if the message has been handled + protected virtual bool HandleReflectNotify(ref Message msg) { + + NativeMethods.NMHEADER nmheader = (NativeMethods.NMHEADER)msg.GetLParam(typeof(NativeMethods.NMHEADER)); + switch (nmheader.nhdr.code) { + case TTN_SHOW: + //System.Diagnostics.Trace.WriteLine("reflect TTN_SHOW"); + if (this.HandleShow(ref msg)) + return true; + break; + case TTN_POP: + //System.Diagnostics.Trace.WriteLine("reflect TTN_POP"); + if (this.HandlePop(ref msg)) + return true; + break; + case TTN_LINKCLICK: + //System.Diagnostics.Trace.WriteLine("reflect TTN_LINKCLICK"); + if (this.HandleLinkClick(ref msg)) + return true; + break; + case TTN_GETDISPINFO: + //System.Diagnostics.Trace.WriteLine("reflect TTN_GETDISPINFO"); + if (this.HandleGetDispInfo(ref msg)) + return true; + break; + } + + return false; + } + + /// + /// Mess with the basic message pump of the tooltip + /// + /// + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + override protected void WndProc(ref Message msg) { + //System.Diagnostics.Trace.WriteLine(String.Format("xx {0:x}", msg.Msg)); + switch (msg.Msg) { + case 0x4E: // WM_NOTIFY + if (!this.HandleNotify(ref msg)) + return; + break; + + case 0x204E: // WM_REFLECT_NOTIFY + if (!this.HandleReflectNotify(ref msg)) + return; + break; + } + + base.WndProc(ref msg); + } + + #endregion + + #region Events + + /// + /// Tell the world that a tooltip is about to show + /// + public event EventHandler Showing; + + /// + /// Tell the world that a tooltip is about to disappear + /// + public event EventHandler Pop; + + /// + /// + /// + /// + protected virtual void OnShowing(ToolTipShowingEventArgs e) { + if (this.Showing != null) + this.Showing(this, e); + } + + /// + /// + /// + /// + protected virtual void OnPop(EventArgs e) { + if (this.Pop != null) + this.Pop(this, e); + } + + #endregion + } + +} \ No newline at end of file diff --git a/ObjectListView/TreeListView.cs b/ObjectListView/TreeListView.cs new file mode 100644 index 0000000..494bb5a --- /dev/null +++ b/ObjectListView/TreeListView.cs @@ -0,0 +1,2128 @@ +/* + * TreeListView - A listview that can show a tree of objects in a column + * + * Author: Phillip Piper + * Date: 23/09/2008 11:15 AM + * + * Change log: + * 2014-10-08 JPP - Fixed an issue where pre-expanded branches would not initially expand properly + * 2014-09-29 JPP - Fixed issue where RefreshObject() on a root object could cause exceptions + * - Fixed issue where CollapseAll() while filtering could cause exception + * 2014-03-09 JPP - Fixed issue where removing a branches only child and then calling RefreshObject() + * could throw an exception. + * v2.7 + * 2014-02-23 JPP - Added Reveal() method to show a deeply nested models. + * 2014-02-05 JPP - Fix issue where refreshing a non-root item would collapse all expanded children of that item + * 2014-02-01 JPP - ClearObjects() now actually, you know, clears objects :) + * - Corrected issue where Expanded event was being raised twice. + * - RebuildChildren() no longer checks if CanExpand is true before rebuilding. + * 2014-01-16 JPP - Corrected an off-by-1 error in hit detection, which meant that clicking in the last 16 pixels + * of an items label was being ignored. + * 2013-11-20 JPP - Moved event triggers into Collapse() and Expand() so that the events are always triggered. + * - CheckedObjects now includes objects that are in a branch that is currently collapsed + * - CollapseAll() and ExpandAll() now trigger cancellable events + * 2013-09-29 JPP - Added TreeFactory to allow the underlying Tree to be replaced by another implementation. + * 2013-09-23 JPP - Fixed long standing issue where RefreshObject() would not work on root objects + * which overrode Equals()/GetHashCode(). + * 2013-02-23 JPP - Added HierarchicalCheckboxes. When this is true, the checkedness of a parent + * is an synopsis of the checkedness of its children. When all children are checked, + * the parent is checked. When all children are unchecked, the parent is unchecked. + * If some children are checked and some are not, the parent is indeterminate. + * v2.6 + * 2012-10-25 JPP - Circumvent annoying issue in ListView control where changing + * selection would leave artifacts on the control. + * 2012-08-10 JPP - Don't trigger selection changed events during expands + * + * v2.5.1 + * 2012-04-30 JPP - Fixed issue where CheckedObjects would return model objects that had been filtered out. + * - Allow any column to render the tree, not just column 0 (still not sure about this one) + * v2.5.0 + * 2011-04-20 JPP - Added ExpandedObjects property and RebuildAll() method. + * 2011-04-09 JPP - Added Expanding, Collapsing, Expanded and Collapsed events. + * The ..ing events are cancellable. These are only fired in response + * to user actions. + * v2.4.1 + * 2010-06-15 JPP - Fixed issue in Tree.RemoveObjects() which resulted in removed objects + * being reported as still existing. + * v2.3 + * 2009-09-01 JPP - Fixed off-by-one error that was messing up hit detection + * 2009-08-27 JPP - Fixed issue when dragging a node from one place to another in the tree + * v2.2.1 + * 2009-07-14 JPP - Clicks to the left of the expander in tree cells are now ignored. + * v2.2 + * 2009-05-12 JPP - Added tree traverse operations: GetParent and GetChildren. + * - Added DiscardAllState() to completely reset the TreeListView. + * 2009-05-10 JPP - Removed all unsafe code + * 2009-05-09 JPP - Fixed issue where any command (Expand/Collapse/Refresh) on a model + * object that was once visible but that is currently in a collapsed branch + * would cause the control to crash. + * 2009-05-07 JPP - Fixed issue where RefreshObjects() would fail when none of the given + * objects were present/visible. + * 2009-04-20 JPP - Fixed issue where calling Expand() on an already expanded branch confused + * the display of the children (SF#2499313) + * 2009-03-06 JPP - Calculate edit rectangle on column 0 more accurately + * v2.1 + * 2009-02-24 JPP - All commands now work when the list is empty (SF #2631054) + * - TreeListViews can now be printed with ListViewPrinter + * 2009-01-27 JPP - Changed to use new Renderer and HitTest scheme + * 2009-01-22 JPP - Added RevealAfterExpand property. If this is true (the default), + * after expanding a branch, the control scrolls to reveal as much of the + * expanded branch as possible. + * 2009-01-13 JPP - Changed TreeRenderer to work with visual styles are disabled + * v2.0.1 + * 2009-01-07 JPP - Made all public and protected methods virtual + * - Changed some classes from 'internal' to 'protected' so that they + * can be accessed by subclasses of TreeListView. + * 2008-12-22 JPP - Added UseWaitCursorWhenExpanding property + * - Made TreeRenderer public so that it can be subclassed + * - Added LinePen property to TreeRenderer to allow the connection drawing + * pen to be changed + * - Fixed some rendering issues where the text highlight rect was miscalculated + * - Fixed connection line problem when there is only a single root + * v2.0 + * 2008-12-10 JPP - Expand/collapse with mouse now works when there is no SmallImageList. + * 2008-12-01 JPP - Search-by-typing now works. + * 2008-11-26 JPP - Corrected calculation of expand/collapse icon (SF#2338819) + * - Fixed ugliness with dotted lines in renderer (SF#2332889) + * - Fixed problem with custom selection colors (SF#2338805) + * 2008-11-19 JPP - Expand/collapse now preserve the selection -- more or less :) + * - Overrode RefreshObjects() to rebuild the given objects and their children + * 2008-11-05 JPP - Added ExpandAll() and CollapseAll() commands + * - CanExpand is no longer cached + * - Renamed InitialBranches to RootModels since it deals with model objects + * 2008-09-23 JPP Initial version + * + * TO DO: + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// A TreeListView combines an expandable tree structure with list view columns. + /// + /// + /// To support tree operations, two delegates must be provided: + /// + /// + /// + /// CanExpandGetter + /// + /// + /// This delegate must accept a model object and return a boolean indicating + /// if that model should be expandable. + /// + /// + /// + /// + /// ChildrenGetter + /// + /// + /// This delegate must accept a model object and return an IEnumerable of model + /// objects that will be displayed as children of the parent model. This delegate will only be called + /// for a model object if the CanExpandGetter has already returned true for that model. + /// + /// + /// + /// + /// ParentGetter + /// + /// + /// This delegate must accept a model object and return the parent model. + /// This delegate will only be called when HierarchicalCheckboxes is true OR when Reveal() is called. + /// + /// + /// + /// + /// The top level branches of the tree are set via the Roots property. SetObjects(), AddObjects() + /// and RemoveObjects() are interpreted as operations on this collection of roots. + /// + /// + /// To add new children to an existing branch, make changes to your model objects and then + /// call RefreshObject() on the parent. + /// + /// The tree must be a directed acyclic graph -- no cycles are allowed. Put more mundanely, + /// each model object must appear only once in the tree. If the same model object appears in two + /// places in the tree, the control will become confused. + /// + public partial class TreeListView : VirtualObjectListView + { + /// + /// Make a default TreeListView + /// + public TreeListView() { + this.OwnerDraw = true; + this.View = View.Details; + this.CheckedObjectsMustStillExistInList = false; + +// ReSharper disable DoNotCallOverridableMethodsInConstructor + this.RegenerateTree(); + this.TreeColumnRenderer = new TreeRenderer(); +// ReSharper restore DoNotCallOverridableMethodsInConstructor + + // This improves hit detection even if we don't have any state image + this.SmallImageList = new ImageList(); + // this.StateImageList.ImageSize = new Size(6, 6); + } + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// This is the delegate that will be used to decide if a model object can be expanded. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual CanExpandGetterDelegate CanExpandGetter { + get { return this.TreeModel.CanExpandGetter; } + set { this.TreeModel.CanExpandGetter = value; } + } + + /// + /// Gets whether or not this listview is capable of showing groups + /// + [Browsable(false)] + public override bool CanShowGroups { + get { + return false; + } + } + + /// + /// This is the delegate that will be used to fetch the children of a model object + /// + /// This delegate will only be called if the CanExpand delegate has + /// returned true for the model object. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual ChildrenGetterDelegate ChildrenGetter { + get { return this.TreeModel.ChildrenGetter; } + set { this.TreeModel.ChildrenGetter = value; } + } + + /// + /// This is the delegate that will be used to fetch the parent of a model object + /// + /// The parent of the given model, or null if the model doesn't exist or + /// if the model is a root + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ParentGetterDelegate ParentGetter { + get { return parentGetter; } + set { parentGetter = value; } + } + private ParentGetterDelegate parentGetter; + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivalent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects. + /// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus + /// the number of objects to be checked. + /// + /// + /// If the ListView is not currently showing CheckBoxes, this property does nothing. It does + /// not remember any check box settings made. + /// + /// + public override IList CheckedObjects { + get { + return base.CheckedObjects; + } + set { + ArrayList objectsToRecalculate = new ArrayList(this.CheckedObjects); + if (value != null) + objectsToRecalculate.AddRange(value); + + base.CheckedObjects = value; + + if (this.HierarchicalCheckboxes) + RecalculateHierarchicalCheckBoxGraph(objectsToRecalculate); + } + } + + /// + /// Gets or sets the model objects that are expanded. + /// + /// + /// This can be used to expand model objects before they are seen. + /// + /// Setting this does *not* force the control to rebuild + /// its display. You need to call RebuildAll(true). + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IEnumerable ExpandedObjects { + get { + return this.TreeModel.mapObjectToExpanded.Keys; + } + set { + this.TreeModel.mapObjectToExpanded.Clear(); + if (value != null) { + foreach (object x in value) + this.TreeModel.SetModelExpanded(x, true); + } + } + } + + /// + /// Gets or sets the filter that is applied to our whole list of objects. + /// TreeListViews do not currently support whole list filters. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IListFilter ListFilter { + get { return null; } + set { + System.Diagnostics.Debug.Assert(value == null, "TreeListView do not support ListFilters"); + } + } + + /// + /// Gets or sets whether this tree list view will display hierarchical checkboxes. + /// Hierarchical checkboxes is when a parent's "checkedness" is calculated from + /// the "checkedness" of its children. If all children are checked, the parent + /// will be checked. If all children are unchecked, the parent will also be unchecked. + /// If some children are checked and others are not, the parent will be indeterminate. + /// + [Category("ObjectListView"), + Description("Show hierarchical checkboxes be enabled?"), + DefaultValue(false)] + public virtual bool HierarchicalCheckboxes { + get { return this.hierarchicalCheckboxes; } + set { + if (this.hierarchicalCheckboxes == value) + return; + + this.hierarchicalCheckboxes = value; + this.CheckBoxes = value; + if (value) + this.TriStateCheckBoxes = false; + } + } + private bool hierarchicalCheckboxes; + + /// + /// Gets or sets the collection of root objects of the tree + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { return this.Roots; } + set { this.Roots = value; } + } + + /// + /// Gets the collection of objects that will be considered when creating clusters + /// (which are used to generate Excel-like column filters) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable ObjectsForClustering { + get { + for (int i = 0; i < this.TreeModel.GetObjectCount(); i++) + yield return this.TreeModel.GetNthObject(i); + } + } + + /// + /// After expanding a branch, should the TreeListView attempts to show as much of the + /// revealed descendents as possible. + /// + [Category("ObjectListView"), + Description("Should the parent of an expand subtree be scrolled to the top revealing the children?"), + DefaultValue(true)] + public bool RevealAfterExpand { + get { return revealAfterExpand; } + set { revealAfterExpand = value; } + } + private bool revealAfterExpand = true; + + /// + /// The model objects that form the top level branches of the tree. + /// + /// Setting this does NOT reset the state of the control. + /// In particular, it does not collapse branches. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IEnumerable Roots { + get { return this.TreeModel.RootObjects; } + set { + this.TreeColumnRenderer = this.TreeColumnRenderer; + this.TreeModel.RootObjects = value ?? new ArrayList(); + this.UpdateVirtualListSize(); + } + } + + /// + /// Make sure that at least one column is displaying a tree. + /// If no columns is showing the tree, make column 0 do it. + /// + protected virtual void EnsureTreeRendererPresent(TreeRenderer renderer) { + if (this.Columns.Count == 0) + return; + + foreach (OLVColumn col in this.Columns) { + if (col.Renderer is TreeRenderer) { + col.Renderer = renderer; + return; + } + } + + // No column held a tree renderer, so give column 0 one + OLVColumn columnZero = this.GetColumn(0); + columnZero.Renderer = renderer; + columnZero.WordWrap = columnZero.WordWrap; + } + + /// + /// Gets or sets the renderer that will be used to draw the tree structure. + /// Setting this to null resets the renderer to default. + /// + /// If a column is currently rendering the tree, the renderer + /// for that column will be replaced. If no column is rendering the tree, + /// column 0 will be given this renderer. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual TreeRenderer TreeColumnRenderer { + get { return treeRenderer ?? (treeRenderer = new TreeRenderer()); } + set { + treeRenderer = value ?? new TreeRenderer(); + EnsureTreeRendererPresent(treeRenderer); + } + } + private TreeRenderer treeRenderer; + + /// + /// This is the delegate that will be used to create the underlying Tree structure + /// that the TreeListView uses to manage the information about the tree. + /// + /// + /// The factory must not return null. + /// + /// Most users of TreeListView will never have to use this delegate. + /// + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public TreeFactoryDelegate TreeFactory { + get { return treeFactory; } + set { treeFactory = value; } + } + private TreeFactoryDelegate treeFactory; + + /// + /// Should a wait cursor be shown when a branch is being expanded? + /// + /// When this is true, the wait cursor will be shown whilst the children of the + /// branch are being fetched. If the children of the branch have already been cached, + /// the cursor will not change. + [Category("ObjectListView"), + Description("Should a wait cursor be shown when a branch is being expanded?"), + DefaultValue(true)] + public virtual bool UseWaitCursorWhenExpanding { + get { return useWaitCursorWhenExpanding; } + set { useWaitCursorWhenExpanding = value; } + } + private bool useWaitCursorWhenExpanding = true; + + /// + /// Gets the model that is used to manage the tree structure + /// + /// + /// Don't mess with this property unless you really know what you are doing. + /// If you don't already know what it's for, you don't need it. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Tree TreeModel { + get { return this.treeModel; } + protected set { this.treeModel = value; } + } + private Tree treeModel; + + //------------------------------------------------------------------------------------------ + // Accessing + + /// + /// Return true if the branch at the given model is expanded + /// + /// + /// + public virtual bool IsExpanded(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return (br != null && br.IsExpanded); + } + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Collapse the subtree underneath the given model + /// + /// + public virtual void Collapse(Object model) { + if (this.GetItemCount() == 0) + return; + + OLVListItem item = this.ModelToItem(model); + TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(model, item); + this.OnCollapsing(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.Collapse(model); + if (index >= 0) { + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + if (index < this.GetItemCount()) + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnCollapsed(new TreeBranchCollapsedEventArgs(model, item)); + } + } + + /// + /// Collapse all subtrees within this control + /// + public virtual void CollapseAll() { + if (this.GetItemCount() == 0) + return; + + TreeBranchCollapsingEventArgs args = new TreeBranchCollapsingEventArgs(null, null); + this.OnCollapsing(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.CollapseAll(); + if (index >= 0) { + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + if (index < this.GetItemCount()) + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnCollapsed(new TreeBranchCollapsedEventArgs(null, null)); + } + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public override void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else { + this.Roots = null; + this.DiscardAllState(); + } + } + + /// + /// Collapse all roots and forget everything we know about all models + /// + public virtual void DiscardAllState() { + this.CheckStateMap.Clear(); + this.RebuildAll(false); + } + + /// + /// Expand the subtree underneath the given model object + /// + /// + public virtual void Expand(Object model) { + if (this.GetItemCount() == 0) + return; + + // Give the world a chance to cancel the expansion + OLVListItem item = this.ModelToItem(model); + TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(model, item); + this.OnExpanding(args); + if (args.Canceled) + return; + + // Remember the selection so we can put it back later + IList selection = this.SelectedObjects; + + // Expand the model first + int index = this.TreeModel.Expand(model); + if (index < 0) + return; + + // Update the size of the list and restore the selection + this.UpdateVirtualListSize(); + using (this.SuspendSelectionEventsDuring()) + this.SelectedObjects = selection; + + // Redraw the items that were changed by the expand operation + this.RedrawItems(index, this.GetItemCount() - 1, true); + + this.OnExpanded(new TreeBranchExpandedEventArgs(model, item)); + + if (this.RevealAfterExpand && index > 0) { + // TODO: This should be a separate method + this.BeginUpdate(); + try { + int countPerPage = NativeMethods.GetCountPerPage(this); + int descedentCount = this.TreeModel.GetVisibleDescendentCount(model); + // If all of the descendents can be shown in the window, make sure that last one is visible. + // If all the descendents can't fit into the window, move the model to the top of the window + // (which will show as many of the descendents as possible) + if (descedentCount < countPerPage) { + this.EnsureVisible(index + descedentCount); + } else { + this.TopItemIndex = index; + } + } + finally { + this.EndUpdate(); + } + } + } + + /// + /// Expand all the branches within this tree recursively. + /// + /// Be careful: this method could take a long time for large trees. + public virtual void ExpandAll() { + if (this.GetItemCount() == 0) + return; + + // Give the world a chance to cancel the expansion + TreeBranchExpandingEventArgs args = new TreeBranchExpandingEventArgs(null, null); + this.OnExpanding(args); + if (args.Canceled) + return; + + IList selection = this.SelectedObjects; + int index = this.TreeModel.ExpandAll(); + if (index < 0) + return; + + this.UpdateVirtualListSize(); + using (this.SuspendSelectionEventsDuring()) + this.SelectedObjects = selection; + this.RedrawItems(index, this.GetItemCount() - 1, true); + this.OnExpanded(new TreeBranchExpandedEventArgs(null, null)); + } + + /// + /// Completely rebuild the tree structure + /// + /// If true, the control will try to preserve selection and expansion + public virtual void RebuildAll(bool preserveState) { + int previousTopItemIndex = preserveState ? this.TopItemIndex : -1; + + this.RebuildAll( + preserveState ? this.SelectedObjects : null, + preserveState ? this.ExpandedObjects : null, + preserveState ? this.CheckedObjects : null); + + if (preserveState) + this.TopItemIndex = previousTopItemIndex; + } + + /// + /// Completely rebuild the tree structure + /// + /// If not null, this list of objects will be selected after the tree is rebuilt + /// If not null, this collection of objects will be expanded after the tree is rebuilt + /// If not null, this collection of objects will be checked after the tree is rebuilt + protected virtual void RebuildAll(IList selected, IEnumerable expanded, IList checkedObjects) { + // Remember the bits of info we don't want to forget (anyone ever see Memento?) + IEnumerable roots = this.Roots; + CanExpandGetterDelegate canExpand = this.CanExpandGetter; + ChildrenGetterDelegate childrenGetter = this.ChildrenGetter; + + try { + this.BeginUpdate(); + + // Give ourselves a new data structure + this.RegenerateTree(); + + // Put back the bits we didn't want to forget + this.CanExpandGetter = canExpand; + this.ChildrenGetter = childrenGetter; + if (expanded != null) + this.ExpandedObjects = expanded; + this.Roots = roots; + if (selected != null) + this.SelectedObjects = selected; + if (checkedObjects != null) + this.CheckedObjects = checkedObjects; + } + finally { + this.EndUpdate(); + } + } + + /// + /// Unroll all the ancestors of the given model and make sure it is then visible. + /// + /// This works best when a ParentGetter is installed. + /// The object to be revealed + /// If true, the model will be selected and focused after being revealed + /// True if the object was found and revealed. False if it was not found. + public virtual void Reveal(object modelToReveal, bool selectAfterReveal) { + // Collect all the ancestors of the model + ArrayList ancestors = new ArrayList(); + foreach (object ancestor in this.GetAncestors(modelToReveal)) + ancestors.Add(ancestor); + + // Arrange them from root down to the model's immediate parent + ancestors.Reverse(); + try { + this.BeginUpdate(); + foreach (object ancestor in ancestors) + this.Expand(ancestor); + this.EnsureModelVisible(modelToReveal); + if (selectAfterReveal) + this.SelectObject(modelToReveal, true); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Update the rows that are showing the given objects + /// + public override void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker) delegate { this.RefreshObjects(modelObjects); }); + return; + } + // There is no point in refreshing anything if the list is empty + if (this.GetItemCount() == 0) + return; + + // Remember the selection so we can put it back later + IList selection = this.SelectedObjects; + + // We actually need to refresh the parents. + // Refreshes on root objects have to be handled differently + ArrayList updatedRoots = new ArrayList(); + Hashtable modelsAndParents = new Hashtable(); + foreach (Object model in modelObjects) { + if (model == null) + continue; + modelsAndParents[model] = true; + object parent = GetParent(model); + if (parent == null) { + updatedRoots.Add(model); + } else { + modelsAndParents[parent] = true; + } + } + + // Update any changed roots + if (updatedRoots.Count > 0) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.Roots, false); + bool changed = false; + foreach (Object model in updatedRoots) { + int index = newRoots.IndexOf(model); + if (index >= 0 && !ReferenceEquals(newRoots[index], model)) { + newRoots[index] = model; + changed = true; + } + } + if (changed) + this.Roots = newRoots; + } + + // Refresh each object, remembering where the first update occurred + int firstChange = Int32.MaxValue; + foreach (Object model in modelsAndParents.Keys) { + if (model != null) { + int index = this.TreeModel.RebuildChildren(model); + if (index >= 0) + firstChange = Math.Min(firstChange, index); + } + } + + // If we didn't refresh any objects, don't do anything else + if (firstChange >= this.GetItemCount()) + return; + + this.ClearCachedInfo(); + this.UpdateVirtualListSize(); + this.SelectedObjects = selection; + + // Redraw everything from the first update to the end of the list + this.RedrawItems(firstChange, this.GetItemCount() - 1, true); + } + + /// + /// Change the check state of the given object to be the given state. + /// + /// + /// If the given model object isn't in the list, we still try to remember + /// its state, in case it is referenced in the future. + /// + /// + /// True if the checkedness of the model changed + protected override bool SetObjectCheckedness(object modelObject, CheckState state) { + // If the checkedness of the given model changes AND this tree has + // hierarchical checkboxes, then we need to update the checkedness of + // its children, and recalculate the checkedness of the parent (recursively) + if (!base.SetObjectCheckedness(modelObject, state)) + return false; + + if (!this.HierarchicalCheckboxes) + return true; + + // Give each child the same checkedness as the model + + CheckState? checkedness = this.GetCheckState(modelObject); + if (!checkedness.HasValue || checkedness.Value == CheckState.Indeterminate) + return true; + + foreach (object child in this.GetChildrenWithoutExpanding(modelObject)) { + this.SetObjectCheckedness(child, checkedness.Value); + } + + ArrayList args = new ArrayList(); + args.Add(modelObject); + this.RecalculateHierarchicalCheckBoxGraph(args); + + return true; + } + + + private IEnumerable GetChildrenWithoutExpanding(Object model) { + Branch br = this.TreeModel.GetBranch(model); + if (br == null || !br.CanExpand) + return new ArrayList(); + + return br.Children; + } + + /// + /// Toggle the expanded state of the branch at the given model object + /// + /// + public virtual void ToggleExpansion(Object model) { + if (this.IsExpanded(model)) + this.Collapse(model); + else + this.Expand(model); + } + + //------------------------------------------------------------------------------------------ + // Commands - Tree traversal + + /// + /// Return whether or not the given model can expand. + /// + /// + /// The given model must have already been seen in the tree + public virtual bool CanExpand(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return (br != null && br.CanExpand); + } + + /// + /// Return the model object that is the parent of the given model object. + /// + /// + /// + /// The given model must have already been seen in the tree. + public virtual Object GetParent(Object model) { + Branch br = this.TreeModel.GetBranch(model); + return br == null || br.ParentBranch == null ? null : br.ParentBranch.Model; + } + + /// + /// Return the collection of model objects that are the children of the + /// given model as they exist in the tree at the moment. + /// + /// + /// + /// + /// This method returns the collection of children as the tree knows them. If the given + /// model has never been presented to the user (e.g. it belongs to a parent that has + /// never been expanded), then this method will return an empty collection. + /// + /// Because of this, if you want to traverse the whole tree, this is not the method to use. + /// It's better to traverse the your data model directly. + /// + /// + /// If the given model has not already been seen in the tree or + /// if it is not expandable, an empty collection will be returned. + /// + /// + public virtual IEnumerable GetChildren(Object model) { + Branch br = this.TreeModel.GetBranch(model); + if (br == null || !br.CanExpand) + return new ArrayList(); + + br.FetchChildren(); + + return br.Children; + } + + //------------------------------------------------------------------------------------------ + // Delegates + + /// + /// Delegates of this type are use to decide if the given model object can be expanded + /// + /// The model under consideration + /// Can the given model be expanded? + public delegate bool CanExpandGetterDelegate(Object model); + + /// + /// Delegates of this type are used to fetch the children of the given model object + /// + /// The parent whose children should be fetched + /// An enumerable over the children + public delegate IEnumerable ChildrenGetterDelegate(Object model); + + /// + /// Delegates of this type are used to fetch the parent of the given model object. + /// + /// The child whose parent should be fetched + /// The parent of the child or null if the child is a root + public delegate Object ParentGetterDelegate(Object model); + + /// + /// Delegates of this type are used to create a new underlying Tree structure. + /// + /// The view for which the Tree is being created + /// A subclass of Tree + public delegate Tree TreeFactoryDelegate(TreeListView view); + + //------------------------------------------------------------------------------------------ + #region Implementation + + /// + /// Handle a left button down event + /// + /// + /// + protected override bool ProcessLButtonDown(OlvListViewHitTestInfo hti) { + // Did they click in the expander? + if (hti.HitTestLocation == HitTestLocation.ExpandButton) { + this.PossibleFinishCellEditing(); + this.ToggleExpansion(hti.RowObject); + return true; + } + + return base.ProcessLButtonDown(hti); + } + + /// + /// Create a OLVListItem for given row index + /// + /// The index of the row that is needed + /// An OLVListItem + /// This differs from the base method by also setting up the IndentCount property. + public override OLVListItem MakeListViewItem(int itemIndex) { + OLVListItem olvItem = base.MakeListViewItem(itemIndex); + Branch br = this.TreeModel.GetBranch(olvItem.RowObject); + if (br != null) + olvItem.IndentCount = br.Level; + return olvItem; + } + + /// + /// Reinitialize the Tree structure + /// + protected virtual void RegenerateTree() { + this.TreeModel = this.TreeFactory == null ? new Tree(this) : this.TreeFactory(this); + Trace.Assert(this.TreeModel != null); + this.VirtualListDataSource = this.TreeModel; + } + + /// + /// Recalculate the state of the checkboxes of all the items in the given list + /// and their ancestors. + /// + /// This only makes sense when HierarchicalCheckboxes is true. + /// + protected virtual void RecalculateHierarchicalCheckBoxGraph(IList toCheck) { + if (toCheck == null || toCheck.Count == 0) + return; + + // Avoid recursive calculations + if (isRecalculatingHierarchicalCheckBox) + return; + + try { + isRecalculatingHierarchicalCheckBox = true; + foreach (object ancestor in CalculateDistinctAncestors(toCheck)) + this.RecalculateSingleHierarchicalCheckBox(ancestor); + } + finally { + isRecalculatingHierarchicalCheckBox = false; + } + + } + private bool isRecalculatingHierarchicalCheckBox; + + /// + /// Recalculate the hierarchy state of the given item and its ancestors + /// + /// This only makes sense when HierarchicalCheckboxes is true. + /// + protected virtual void RecalculateSingleHierarchicalCheckBox(object modelObject) { + + if (modelObject == null) + return; + + // Only branches have calculated check states. Leaf node checkedness is not calculated + if (!this.CanExpand(modelObject)) + return; + + // Set the checkedness of the given model based on the state of its children. + CheckState? aggregate = null; + foreach (object child in this.GetChildren(modelObject)) { + CheckState? checkedness = this.GetCheckState(child); + if (!checkedness.HasValue) + continue; + + if (aggregate.HasValue) { + if (aggregate.Value != checkedness.Value) { + aggregate = CheckState.Indeterminate; + break; + } + } else + aggregate = checkedness; + } + + base.SetObjectCheckedness(modelObject, aggregate ?? CheckState.Indeterminate); + } + + /// + /// Yield the unique ancestors of the given collection of objects. + /// The order of the ancestors is guaranteed to be deeper objects first. + /// Roots will always be last. + /// + /// + /// Unique ancestors of the given objects + protected virtual IEnumerable CalculateDistinctAncestors(IList toCheck) { + + if (toCheck.Count == 1) { + foreach (object ancestor in this.GetAncestors(toCheck[0])) { + yield return ancestor; + } + } else { + // WARNING - Clever code + + // Example: Calculate ancestors of A, B, X and Y + // A and B are children of P, child of GP, child of Root + // X and Y are children of Q, child of GP, child of Root + + // Build a list of all ancestors of all objects we need to check + ArrayList allAncestors = new ArrayList(); + foreach (object child in toCheck) { + foreach (object ancestor in this.GetAncestors(child)) { + allAncestors.Add(ancestor); + } + } + + // allAncestors = { P, GP, Root, P, GP, Root, Q, GP, Root, Q, GP, Root } + + ArrayList uniqueAncestors = new ArrayList(); + Dictionary alreadySeen = new Dictionary(); + allAncestors.Reverse(); + foreach (object ancestor in allAncestors) { + if (!alreadySeen.ContainsKey(ancestor)) { + alreadySeen[ancestor] = true; + uniqueAncestors.Add(ancestor); + } + } + + // uniqueAncestors = { Root, GP, Q, P } + + uniqueAncestors.Reverse(); + foreach (object x in uniqueAncestors) + yield return x; + } + } + + /// + /// Return all the ancestors of the given model + /// + /// + /// + /// This uses ParentGetter if possible. + /// + /// If the given model is a root OR if the model doesn't exist, the collection will be empty + /// + /// The model whose ancestors should be calculated + /// Return a collection of ancestors of the given model. + protected virtual IEnumerable GetAncestors(object model) { + ParentGetterDelegate parentGetterDelegate = this.ParentGetter ?? this.GetParent; + + object parent = parentGetterDelegate(model); + while (parent != null) { + yield return parent; + parent = parentGetterDelegate(parent); + } + } + + #endregion + + //------------------------------------------------------------------------------------------ + #region Event handlers + + /// + /// The application is idle and a SelectionChanged event has been scheduled + /// + /// + /// + protected override void HandleApplicationIdle(object sender, EventArgs e) { + base.HandleApplicationIdle(sender, e); + + // There is an annoying redraw issue on ListViews that use indentation and + // that have full row select enabled. When the selection reduces to a subset + // of previously selected rows, or when the selection is extended using + // shift-pageup/down, then the space occupied by the indentation is not + // invalidated, and hence remains highlighted. + // Ideally we'd want to know exactly which rows were selected or deselected + // and then invalidate just the indentation region of those rows, + // but that's too much work. So just redraw the control. + // Actually... the selection issues show just slightly for non-full row select + // controls as well. So, always redraw the control after the selection + // changes. + this.Invalidate(); + } + + /// + /// Decide if the given key event should be handled as a normal key input to the control? + /// + /// + /// + protected override bool IsInputKey(Keys keyData) { + // We want to handle Left and Right keys within the control + Keys key = keyData & Keys.KeyCode; + if (key == Keys.Left || key == Keys.Right) + return true; + + return base.IsInputKey(keyData); + } + + /// + /// Handle the keyboard input to mimic a TreeView. + /// + /// + /// Was the key press handled? + protected override void OnKeyDown(KeyEventArgs e) { + OLVListItem focused = this.FocusedItem as OLVListItem; + if (focused == null) { + base.OnKeyDown(e); + return; + } + + Object modelObject = focused.RowObject; + Branch br = this.TreeModel.GetBranch(modelObject); + + switch (e.KeyCode) { + case Keys.Left: + // If the branch is expanded, collapse it. If it's collapsed, + // select the parent of the branch. + if (br.IsExpanded) + this.Collapse(modelObject); + else { + if (br.ParentBranch != null && br.ParentBranch.Model != null) + this.SelectObject(br.ParentBranch.Model, true); + } + e.Handled = true; + break; + + case Keys.Right: + // If the branch is expanded, select the first child. + // If it isn't expanded and can be, expand it. + if (br.IsExpanded) { + List filtered = br.FilteredChildBranches; + if (filtered.Count > 0) + this.SelectObject(filtered[0].Model, true); + } else { + if (br.CanExpand) + this.Expand(modelObject); + } + e.Handled = true; + break; + } + + base.OnKeyDown(e); + } + + #endregion + + //------------------------------------------------------------------------------------------ + // Support classes + + /// + /// A Tree object represents a tree structure data model that supports both + /// tree and flat list operations as well as fast access to branches. + /// + /// If you create a subclass of Tree, you must install it in the TreeListView + /// via the TreeFactory delegate. + public class Tree : IVirtualListDataSource, IFilterableDataSource + { + /// + /// Create a Tree + /// + /// + public Tree(TreeListView treeView) { + this.treeView = treeView; + this.trunk = new Branch(null, this, null); + this.trunk.IsExpanded = true; + } + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// This is the delegate that will be used to decide if a model object can be expanded. + /// + public CanExpandGetterDelegate CanExpandGetter { + get { return canExpandGetter; } + set { canExpandGetter = value; } + } + private CanExpandGetterDelegate canExpandGetter; + + /// + /// This is the delegate that will be used to fetch the children of a model object + /// + /// This delegate will only be called if the CanExpand delegate has + /// returned true for the model object. + public ChildrenGetterDelegate ChildrenGetter { + get { return childrenGetter; } + set { childrenGetter = value; } + } + private ChildrenGetterDelegate childrenGetter; + + + /// + /// Get or return the top level model objects in the tree + /// + public IEnumerable RootObjects { + get { return this.trunk.Children; } + set { + this.trunk.Children = value; + foreach (Branch br in this.trunk.ChildBranches) + br.RefreshChildren(); + this.RebuildList(); + } + } + + /// + /// What tree view is this Tree the model for? + /// + public TreeListView TreeView { + get { return this.treeView; } + } + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Collapse the subtree underneath the given model + /// + /// The model to be collapsed. If the model isn't in the tree, + /// or if it is already collapsed, the command does nothing. + /// The index of the model in flat list version of the tree + public virtual int Collapse(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.IsExpanded) + return -1; + + // Remember that the branch is collapsed, even if it's currently not visible + if (!br.Visible) { + br.Collapse(); + return -1; + } + + int count = br.NumberVisibleDescendents; + br.Collapse(); + + // Remove the visible descendents from after the branch itself + int index = this.GetObjectIndex(model); + this.objectList.RemoveRange(index + 1, count); + this.RebuildObjectMap(index + 1); + return index; + } + + /// + /// Collapse all branches in this tree + /// + /// Nothing useful + public virtual int CollapseAll() { + this.trunk.CollapseAll(); + this.RebuildList(); + return 0; + } + + /// + /// Expand the subtree underneath the given model object + /// + /// The model to be expanded. + /// The index of the model in flat list version of the tree + /// + /// If the model isn't in the tree, + /// if it cannot be expanded or if it is already expanded, the command does nothing. + /// + public virtual int Expand(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.CanExpand || br.IsExpanded) + return -1; + + // Remember that the branch is expanded, even if it's currently not visible + br.Expand(); + if (!br.Visible) + { + return -1; + } + + int index = this.GetObjectIndex(model); + this.InsertChildren(br, index + 1); + return index; + } + + /// + /// Expand all branches in this tree + /// + /// Return the index of the first branch that was expanded + public virtual int ExpandAll() { + this.trunk.ExpandAll(); + this.Sort(this.lastSortColumn, this.lastSortOrder); + return 0; + } + + /// + /// Return the Branch object that represents the given model in the tree + /// + /// The model whose branches is to be returned + /// The branch that represents the given model, or null if the model + /// isn't in the tree. + public virtual Branch GetBranch(object model) { + if (model == null) + return null; + + Branch br; + this.mapObjectToBranch.TryGetValue(model, out br); + return br; + } + + /// + /// Return the number of visible descendents that are below the given model. + /// + /// The model whose descendent count is to be returned + /// The number of visible descendents. 0 if the model doesn't exist or is collapsed + public virtual int GetVisibleDescendentCount(object model) + { + Branch br = this.GetBranch(model); + return br == null || !br.IsExpanded ? 0 : br.NumberVisibleDescendents; + } + + /// + /// Rebuild the children of the given model, refreshing any cached information held about the given object + /// + /// + /// The index of the model in flat list version of the tree + public virtual int RebuildChildren(Object model) { + Branch br = this.GetBranch(model); + if (br == null || !br.Visible) + return -1; + + int count = br.NumberVisibleDescendents; + + // Remove the visible descendents from after the branch itself + int index = this.GetObjectIndex(model); + if (count > 0) + this.objectList.RemoveRange(index + 1, count); + + // Refresh our knowledge of our children (do this even if CanExpand is false, because + // the branch have already collected some children and that information could be stale) + br.RefreshChildren(); + + // Insert the refreshed children if the branch can expand and is expanded + if (br.CanExpand && br.IsExpanded) + this.InsertChildren(br, index + 1); + return index; + } + + //------------------------------------------------------------------------------------------ + // Implementation + + /// + /// Is the given model expanded? + /// + /// + /// + internal bool IsModelExpanded(object model) { + // Special case: model == null is the container for the roots. This is always expanded + if (model == null) + return true; + bool isExpanded; + this.mapObjectToExpanded.TryGetValue(model, out isExpanded); + return isExpanded; + } + + /// + /// Remember whether or not the given model was expanded + /// + /// + /// + internal void SetModelExpanded(object model, bool isExpanded) { + if (model == null) return; + + if (isExpanded) + this.mapObjectToExpanded[model] = true; + else + this.mapObjectToExpanded.Remove(model); + } + + /// + /// Insert the children of the given branch into the given position + /// + /// The branch whose children should be inserted + /// The index where the children should be inserted + protected virtual void InsertChildren(Branch br, int index) { + // Expand the branch + br.Expand(); + br.Sort(this.GetBranchComparer()); + + // Insert the branch's visible descendents after the branch itself + this.objectList.InsertRange(index, br.Flatten()); + this.RebuildObjectMap(index); + } + + /// + /// Rebuild our flat internal list of objects. + /// + protected virtual void RebuildList() { + this.objectList = ArrayList.Adapter(this.trunk.Flatten()); + List filtered = this.trunk.FilteredChildBranches; + if (filtered.Count > 0) { + filtered[0].IsFirstBranch = true; + filtered[0].IsOnlyBranch = (filtered.Count == 1); + } + this.RebuildObjectMap(0); + } + + /// + /// Rebuild our reverse index that maps an object to its location + /// in the filteredObjectList array. + /// + /// + protected virtual void RebuildObjectMap(int startIndex) { + if (startIndex == 0) + this.mapObjectToIndex.Clear(); + for (int i = startIndex; i < this.objectList.Count; i++) + this.mapObjectToIndex[this.objectList[i]] = i; + } + + /// + /// Create a new branch within this tree + /// + /// + /// + /// + internal Branch MakeBranch(Branch parent, object model) { + Branch br = new Branch(parent, this, model); + + // Remember that the given branch is part of this tree. + this.mapObjectToBranch[model] = br; + return br; + } + + //------------------------------------------------------------------------------------------ + + #region IVirtualListDataSource Members + + /// + /// + /// + /// + /// + public virtual object GetNthObject(int n) { + return this.objectList[n]; + } + + /// + /// + /// + /// + public virtual int GetObjectCount() { + return this.trunk.NumberVisibleDescendents; + } + + /// + /// + /// + /// + /// + public virtual int GetObjectIndex(object model) + { + int index; + if (model != null && this.mapObjectToIndex.TryGetValue(model, out index)) + return index; + + return -1; + } + + /// + /// + /// + /// + /// + public virtual void PrepareCache(int first, int last) { + } + + /// + /// + /// + /// + /// + /// + /// + /// + public virtual int SearchText(string value, int first, int last, OLVColumn column) { + return AbstractVirtualListDataSource.DefaultSearchText(value, first, last, column, this); + } + + /// + /// Sort the tree on the given column and in the given order + /// + /// + /// + public virtual void Sort(OLVColumn column, SortOrder order) { + this.lastSortColumn = column; + this.lastSortOrder = order; + + // TODO: Need to raise an AboutToSortEvent here + + // Sorting is going to change the order of the branches so clear + // the "first branch" flag + foreach (Branch b in this.trunk.ChildBranches) + b.IsFirstBranch = false; + + this.trunk.Sort(this.GetBranchComparer()); + this.RebuildList(); + } + + /// + /// + /// + /// + protected virtual BranchComparer GetBranchComparer() { + if (this.lastSortColumn == null) + return null; + + return new BranchComparer(new ModelObjectComparer( + this.lastSortColumn, + this.lastSortOrder, + this.treeView.SecondarySortColumn ?? this.treeView.GetColumn(0), + this.treeView.SecondarySortColumn == null ? this.lastSortOrder : this.treeView.SecondarySortOrder)); + } + + /// + /// Add the given collection of objects to the roots of this tree + /// + /// + public virtual void AddObjects(ICollection modelObjects) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, true); + foreach (Object x in modelObjects) + newRoots.Add(x); + this.SetObjects(newRoots); + } + + /// + /// Remove all of the given objects from the roots of the tree. + /// Any objects that is not already in the roots collection is ignored. + /// + /// + public virtual void RemoveObjects(ICollection modelObjects) { + ArrayList newRoots = new ArrayList(); + foreach (Object x in this.treeView.Roots) + newRoots.Add(x); + foreach (Object x in modelObjects) { + newRoots.Remove(x); + this.mapObjectToIndex.Remove(x); + } + this.SetObjects(newRoots); + } + + /// + /// Set the roots of this tree to be the given collection + /// + /// + public virtual void SetObjects(IEnumerable collection) { + // We interpret a SetObjects() call as setting the roots of the tree + this.treeView.Roots = collection; + } + + /// + /// Update/replace the nth object with the given object + /// + /// + /// + public void UpdateObject(int index, object modelObject) { + ArrayList newRoots = ObjectListView.EnumerableToArray(this.treeView.Roots, false); + if (index < newRoots.Count) + newRoots[index] = modelObject; + SetObjects(newRoots); + } + + #endregion + + #region IFilterableDataSource Members + + /// + /// + /// + /// + /// + public void ApplyFilters(IModelFilter mFilter, IListFilter lFilter) { + this.modelFilter = mFilter; + this.listFilter = lFilter; + this.RebuildList(); + } + + /// + /// Is this list currently being filtered? + /// + internal bool IsFiltering { + get { + return this.treeView.UseFiltering && (this.modelFilter != null || this.listFilter != null); + } + } + + /// + /// Should the given model be included in this control? + /// + /// The model to consider + /// True if it will be included + internal bool IncludeModel(object model) { + if (!this.treeView.UseFiltering) + return true; + + if (this.modelFilter == null) + return true; + + return this.modelFilter.Filter(model); + } + + #endregion + + //------------------------------------------------------------------------------------------ + // Private instance variables + + private OLVColumn lastSortColumn; + private SortOrder lastSortOrder; + private readonly Dictionary mapObjectToBranch = new Dictionary(); +// ReSharper disable once InconsistentNaming + internal Dictionary mapObjectToExpanded = new Dictionary(); + private readonly Dictionary mapObjectToIndex = new Dictionary(); + private ArrayList objectList = new ArrayList(); + private readonly TreeListView treeView; + private readonly Branch trunk; + + /// + /// + /// +// ReSharper disable once InconsistentNaming + protected IModelFilter modelFilter; + /// + /// + /// +// ReSharper disable once InconsistentNaming + protected IListFilter listFilter; + } + + /// + /// A Branch represents a sub-tree within a tree + /// + public class Branch + { + /// + /// Indicators for branches + /// + [Flags] + public enum BranchFlags + { + /// + /// FirstBranch of tree + /// + FirstBranch = 1, + + /// + /// LastChild of parent + /// + LastChild = 2, + + /// + /// OnlyBranch of tree + /// + OnlyBranch = 4 + } + + #region Life and death + + /// + /// Create a Branch + /// + /// + /// + /// + public Branch(Branch parent, Tree tree, Object model) { + this.ParentBranch = parent; + this.Tree = tree; + this.Model = model; + } + + #endregion + + #region Public properties + + //------------------------------------------------------------------------------------------ + // Properties + + /// + /// Get the ancestor branches of this branch, with the 'oldest' ancestor first. + /// + public virtual IList Ancestors { + get { + List ancestors = new List(); + if (this.ParentBranch != null) + this.ParentBranch.PushAncestors(ancestors); + return ancestors; + } + } + + private void PushAncestors(IList list) { + // This is designed to ignore the trunk (which has no parent) + if (this.ParentBranch != null) { + this.ParentBranch.PushAncestors(list); + list.Add(this); + } + } + + /// + /// Can this branch be expanded? + /// + public virtual bool CanExpand { + get { + if (this.Tree.CanExpandGetter == null || this.Model == null) + return false; + + return this.Tree.CanExpandGetter(this.Model); + } + } + + /// + /// Gets or sets our children + /// + public List ChildBranches { + get { return this.childBranches; } + set { this.childBranches = value; } + } + private List childBranches = new List(); + + /// + /// Get/set the model objects that are beneath this branch + /// + public virtual IEnumerable Children { + get { + ArrayList children = new ArrayList(); + foreach (Branch x in this.ChildBranches) + children.Add(x.Model); + return children; + } + set { + this.ChildBranches.Clear(); + + TreeListView treeListView = this.Tree.TreeView; + CheckState? checkedness = null; + if (treeListView != null && treeListView.HierarchicalCheckboxes) + checkedness = treeListView.GetCheckState(this.Model); + foreach (Object x in value) { + this.AddChild(x); + + // If the tree view is showing hierarchical checkboxes, then + // when a child object is first added, it has the same checkedness as this branch + if (checkedness.HasValue && checkedness.Value == CheckState.Checked) + treeListView.SetObjectCheckedness(x, checkedness.Value); + } + } + } + + private void AddChild(object childModel) { + Branch br = this.Tree.GetBranch(childModel); + if (br == null) + br = this.Tree.MakeBranch(this, childModel); + else { + br.ParentBranch = this; + br.Model = childModel; + br.ClearCachedInfo(); + } + this.ChildBranches.Add(br); + } + + /// + /// Gets a list of all the branches that survive filtering + /// + public List FilteredChildBranches { + get { + if (!this.IsExpanded) + return new List(); + + if (!this.Tree.IsFiltering) + return this.ChildBranches; + + List filtered = new List(); + foreach (Branch b in this.ChildBranches) { + if (this.Tree.IncludeModel(b.Model)) + filtered.Add(b); + else { + // Also include this branch if it has any filtered branches (yes, its recursive) + if (b.FilteredChildBranches.Count > 0) + filtered.Add(b); + } + } + return filtered; + } + } + + /// + /// Gets or set whether this branch is expanded + /// + public bool IsExpanded { + get { return this.Tree.IsModelExpanded(this.Model); } + set { this.Tree.SetModelExpanded(this.Model, value); } + } + + /// + /// Return true if this branch is the first branch of the entire tree + /// + public virtual bool IsFirstBranch { + get { + return ((this.flags & Branch.BranchFlags.FirstBranch) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.FirstBranch; + else + this.flags &= ~Branch.BranchFlags.FirstBranch; + } + } + + /// + /// Return true if this branch is the last child of its parent + /// + public virtual bool IsLastChild { + get { + return ((this.flags & Branch.BranchFlags.LastChild) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.LastChild; + else + this.flags &= ~Branch.BranchFlags.LastChild; + } + } + + /// + /// Return true if this branch is the only top level branch + /// + public virtual bool IsOnlyBranch { + get { + return ((this.flags & Branch.BranchFlags.OnlyBranch) != 0); + } + set { + if (value) + this.flags |= Branch.BranchFlags.OnlyBranch; + else + this.flags &= ~Branch.BranchFlags.OnlyBranch; + } + } + + /// + /// Gets the depth level of this branch + /// + public int Level { + get { + if (this.ParentBranch == null) + return 0; + + return this.ParentBranch.Level + 1; + } + } + + /// + /// Gets or sets which model is represented by this branch + /// + public Object Model { + get { return model; } + set { model = value; } + } + private Object model; + + /// + /// Return the number of descendents of this branch that are currently visible + /// + /// + public virtual int NumberVisibleDescendents { + get { + if (!this.IsExpanded) + return 0; + + List filtered = this.FilteredChildBranches; + int count = filtered.Count; + foreach (Branch br in filtered) + count += br.NumberVisibleDescendents; + return count; + } + } + + /// + /// Gets or sets our parent branch + /// + public Branch ParentBranch { + get { return parentBranch; } + set { parentBranch = value; } + } + private Branch parentBranch; + + /// + /// Gets or sets our overall tree + /// + public Tree Tree { + get { return tree; } + set { tree = value; } + } + private Tree tree; + + /// + /// Is this branch currently visible? A branch is visible + /// if it has no parent (i.e. it's a root), or its parent + /// is visible and expanded. + /// + public virtual bool Visible { + get { + if (this.ParentBranch == null) + return true; + + return this.ParentBranch.IsExpanded && this.ParentBranch.Visible; + } + } + + #endregion + + #region Commands + + //------------------------------------------------------------------------------------------ + // Commands + + /// + /// Clear any cached information that this branch is holding + /// + public virtual void ClearCachedInfo() { + this.Children = new ArrayList(); + this.alreadyHasChildren = false; + } + + /// + /// Collapse this branch + /// + public virtual void Collapse() { + this.IsExpanded = false; + } + + /// + /// Expand this branch + /// + public virtual void Expand() { + if (this.CanExpand) { + this.IsExpanded = true; + this.FetchChildren(); + } + } + + /// + /// Expand this branch recursively + /// + public virtual void ExpandAll() { + this.Expand(); + foreach (Branch br in this.ChildBranches) { + if (br.CanExpand) + br.ExpandAll(); + } + } + + /// + /// Collapse all branches in this tree + /// + /// Nothing useful + public virtual void CollapseAll() + { + this.Collapse(); + foreach (Branch br in this.ChildBranches) { + if (br.IsExpanded) + br.CollapseAll(); + } + } + + /// + /// Fetch the children of this branch. + /// + /// This should only be called when CanExpand is true. + public virtual void FetchChildren() { + if (this.alreadyHasChildren) + return; + + this.alreadyHasChildren = true; + + if (this.Tree.ChildrenGetter == null) + return; + + Cursor previous = Cursor.Current; + try { + if (this.Tree.TreeView.UseWaitCursorWhenExpanding) + Cursor.Current = Cursors.WaitCursor; + this.Children = this.Tree.ChildrenGetter(this.Model); + } + finally { + Cursor.Current = previous; + } + } + + /// + /// Collapse the visible descendents of this branch into list of model objects + /// + /// + public virtual IList Flatten() { + ArrayList flatList = new ArrayList(); + if (this.IsExpanded) + this.FlattenOnto(flatList); + return flatList; + } + + /// + /// Flatten this branch's visible descendents onto the given list. + /// + /// + /// The branch itself is not included in the list. + public virtual void FlattenOnto(IList flatList) { + Branch lastBranch = null; + foreach (Branch br in this.FilteredChildBranches) { + lastBranch = br; + br.IsLastChild = false; + flatList.Add(br.Model); + if (br.IsExpanded) { + br.FetchChildren(); // make sure we have the branches children + br.FlattenOnto(flatList); + } + } + if (lastBranch != null) + lastBranch.IsLastChild = true; + } + + /// + /// Force a refresh of all children recursively + /// + public virtual void RefreshChildren() { + + // Forget any previous children. We always do this so that if + // IsExpanded or CanExpand have changed, we aren't left with stale information. + this.ClearCachedInfo(); + + if (!this.IsExpanded || !this.CanExpand) + return; + + this.FetchChildren(); + foreach (Branch br in this.ChildBranches) + br.RefreshChildren(); + } + + /// + /// Sort the sub-branches and their descendents so they are ordered according + /// to the given comparer. + /// + /// The comparer that orders the branches + public virtual void Sort(BranchComparer comparer) { + if (this.ChildBranches.Count == 0) + return; + + if (comparer != null) + this.ChildBranches.Sort(comparer); + + foreach (Branch br in this.ChildBranches) + br.Sort(comparer); + } + + #endregion + + + //------------------------------------------------------------------------------------------ + // Private instance variables + + private bool alreadyHasChildren; + private BranchFlags flags; + } + + /// + /// This class sorts branches according to how their respective model objects are sorted + /// + public class BranchComparer : IComparer + { + /// + /// Create a BranchComparer + /// + /// + public BranchComparer(IComparer actualComparer) { + this.actualComparer = actualComparer; + } + + /// + /// Order the two branches + /// + /// + /// + /// + public int Compare(Branch x, Branch y) { + return this.actualComparer.Compare(x.Model, y.Model); + } + + private readonly IComparer actualComparer; + } + + } +} diff --git a/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs b/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs new file mode 100644 index 0000000..e8e520e --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.Designer.cs @@ -0,0 +1,190 @@ +namespace BrightIdeasSoftware +{ + partial class ColumnSelectionForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.buttonMoveUp = new System.Windows.Forms.Button(); + this.buttonMoveDown = new System.Windows.Forms.Button(); + this.buttonShow = new System.Windows.Forms.Button(); + this.buttonHide = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.buttonOK = new System.Windows.Forms.Button(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.objectListView1 = new BrightIdeasSoftware.ObjectListView(); + this.olvColumn1 = new BrightIdeasSoftware.OLVColumn(); + ((System.ComponentModel.ISupportInitialize)(this.objectListView1)).BeginInit(); + this.SuspendLayout(); + // + // buttonMoveUp + // + this.buttonMoveUp.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonMoveUp.Location = new System.Drawing.Point(295, 31); + this.buttonMoveUp.Name = "buttonMoveUp"; + this.buttonMoveUp.Size = new System.Drawing.Size(87, 23); + this.buttonMoveUp.TabIndex = 1; + this.buttonMoveUp.Text = "Move &Up"; + this.buttonMoveUp.UseVisualStyleBackColor = true; + this.buttonMoveUp.Click += new System.EventHandler(this.buttonMoveUp_Click); + // + // buttonMoveDown + // + this.buttonMoveDown.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonMoveDown.Location = new System.Drawing.Point(295, 60); + this.buttonMoveDown.Name = "buttonMoveDown"; + this.buttonMoveDown.Size = new System.Drawing.Size(87, 23); + this.buttonMoveDown.TabIndex = 2; + this.buttonMoveDown.Text = "Move &Down"; + this.buttonMoveDown.UseVisualStyleBackColor = true; + this.buttonMoveDown.Click += new System.EventHandler(this.buttonMoveDown_Click); + // + // buttonShow + // + this.buttonShow.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonShow.Location = new System.Drawing.Point(295, 89); + this.buttonShow.Name = "buttonShow"; + this.buttonShow.Size = new System.Drawing.Size(87, 23); + this.buttonShow.TabIndex = 3; + this.buttonShow.Text = "&Show"; + this.buttonShow.UseVisualStyleBackColor = true; + this.buttonShow.Click += new System.EventHandler(this.buttonShow_Click); + // + // buttonHide + // + this.buttonHide.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.buttonHide.Location = new System.Drawing.Point(295, 118); + this.buttonHide.Name = "buttonHide"; + this.buttonHide.Size = new System.Drawing.Size(87, 23); + this.buttonHide.TabIndex = 4; + this.buttonHide.Text = "&Hide"; + this.buttonHide.UseVisualStyleBackColor = true; + this.buttonHide.Click += new System.EventHandler(this.buttonHide_Click); + // + // label1 + // + this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.label1.BackColor = System.Drawing.SystemColors.Control; + this.label1.Location = new System.Drawing.Point(13, 9); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(366, 19); + this.label1.TabIndex = 5; + this.label1.Text = "Choose the columns you want to see in this list. "; + // + // buttonOK + // + this.buttonOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonOK.Location = new System.Drawing.Point(198, 304); + this.buttonOK.Name = "buttonOK"; + this.buttonOK.Size = new System.Drawing.Size(87, 23); + this.buttonOK.TabIndex = 6; + this.buttonOK.Text = "&OK"; + this.buttonOK.UseVisualStyleBackColor = true; + this.buttonOK.Click += new System.EventHandler(this.buttonOK_Click); + // + // buttonCancel + // + this.buttonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonCancel.Location = new System.Drawing.Point(295, 304); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(87, 23); + this.buttonCancel.TabIndex = 7; + this.buttonCancel.Text = "&Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + // + // objectListView1 + // + this.objectListView1.AllColumns.Add(this.olvColumn1); + this.objectListView1.AlternateRowBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); + this.objectListView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.objectListView1.CellEditActivation = BrightIdeasSoftware.ObjectListView.CellEditActivateMode.SingleClick; + this.objectListView1.CheckBoxes = true; + this.objectListView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.olvColumn1}); + this.objectListView1.FullRowSelect = true; + this.objectListView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; + this.objectListView1.HideSelection = false; + this.objectListView1.Location = new System.Drawing.Point(12, 31); + this.objectListView1.MultiSelect = false; + this.objectListView1.Name = "objectListView1"; + this.objectListView1.ShowGroups = false; + this.objectListView1.ShowSortIndicators = false; + this.objectListView1.Size = new System.Drawing.Size(273, 259); + this.objectListView1.TabIndex = 0; + this.objectListView1.UseCompatibleStateImageBehavior = false; + this.objectListView1.View = System.Windows.Forms.View.Details; + this.objectListView1.SelectionChanged += new System.EventHandler(this.objectListView1_SelectionChanged); + // + // olvColumn1 + // + this.olvColumn1.AspectName = "Text"; + this.olvColumn1.IsVisible = true; + this.olvColumn1.Text = "Column"; + this.olvColumn1.Width = 267; + // + // ColumnSelectionForm + // + this.AcceptButton = this.buttonOK; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.buttonCancel; + this.ClientSize = new System.Drawing.Size(391, 339); + this.Controls.Add(this.buttonCancel); + this.Controls.Add(this.buttonOK); + this.Controls.Add(this.label1); + this.Controls.Add(this.buttonHide); + this.Controls.Add(this.buttonShow); + this.Controls.Add(this.buttonMoveDown); + this.Controls.Add(this.buttonMoveUp); + this.Controls.Add(this.objectListView1); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ColumnSelectionForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "Column Selection"; + ((System.ComponentModel.ISupportInitialize)(this.objectListView1)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private BrightIdeasSoftware.ObjectListView objectListView1; + private System.Windows.Forms.Button buttonMoveUp; + private System.Windows.Forms.Button buttonMoveDown; + private System.Windows.Forms.Button buttonShow; + private System.Windows.Forms.Button buttonHide; + private BrightIdeasSoftware.OLVColumn olvColumn1; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button buttonOK; + private System.Windows.Forms.Button buttonCancel; + } +} \ No newline at end of file diff --git a/ObjectListView/Utilities/ColumnSelectionForm.cs b/ObjectListView/Utilities/ColumnSelectionForm.cs new file mode 100644 index 0000000..eb0c234 --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.cs @@ -0,0 +1,263 @@ +/* + * ColumnSelectionForm - A utility form that allows columns to be rearranged and/or hidden + * + * Author: Phillip Piper + * Date: 1/04/2011 11:15 AM + * + * Change log: + * 2013-04-21 JPP - Fixed obscure bug in column re-ordered. Thanks to Edwin Chen. + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// This form is an example of how an application could allows the user to select which columns + /// an ObjectListView will display, as well as select which order the columns are displayed in. + /// + /// + /// In Tile view, ColumnHeader.DisplayIndex does nothing. To reorder the columns you have + /// to change the order of objects in the Columns property. + /// Remember that the first column is special! + /// It has to remain the first column. + /// + public partial class ColumnSelectionForm : Form + { + /// + /// Make a new ColumnSelectionForm + /// + public ColumnSelectionForm() + { + InitializeComponent(); + } + + /// + /// Open this form so it will edit the columns that are available in the listview's current view + /// + /// The ObjectListView whose columns are to be altered + public void OpenOn(ObjectListView olv) + { + this.OpenOn(olv, olv.View); + } + + /// + /// Open this form so it will edit the columns that are available in the given listview + /// when the listview is showing the given type of view. + /// + /// The ObjectListView whose columns are to be altered + /// The view that is to be altered. Must be View.Details or View.Tile + public void OpenOn(ObjectListView olv, View view) + { + if (view != View.Details && view != View.Tile) + return; + + this.InitializeForm(olv, view); + if (this.ShowDialog() == DialogResult.OK) + this.Apply(olv, view); + } + + /// + /// Initialize the form to show the columns of the given view + /// + /// + /// + protected void InitializeForm(ObjectListView olv, View view) + { + this.AllColumns = olv.AllColumns; + this.RearrangableColumns = new List(this.AllColumns); + foreach (OLVColumn col in this.RearrangableColumns) { + if (view == View.Details) + this.MapColumnToVisible[col] = col.IsVisible; + else + this.MapColumnToVisible[col] = col.IsTileViewColumn; + } + this.RearrangableColumns.Sort(new SortByDisplayOrder(this)); + + this.objectListView1.BooleanCheckStateGetter = delegate(Object rowObject) { + return this.MapColumnToVisible[(OLVColumn)rowObject]; + }; + + this.objectListView1.BooleanCheckStatePutter = delegate(Object rowObject, bool newValue) { + // Some columns should always be shown, so ignore attempts to hide them + OLVColumn column = (OLVColumn)rowObject; + if (!column.CanBeHidden) + return true; + + this.MapColumnToVisible[column] = newValue; + EnableControls(); + return newValue; + }; + + this.objectListView1.SetObjects(this.RearrangableColumns); + this.EnableControls(); + } + private List AllColumns = null; + private List RearrangableColumns = new List(); + private Dictionary MapColumnToVisible = new Dictionary(); + + /// + /// The user has pressed OK. Do what's requied. + /// + /// + /// + protected void Apply(ObjectListView olv, View view) + { + olv.Freeze(); + + // Update the column definitions to reflect whether they have been hidden + if (view == View.Details) { + foreach (OLVColumn col in olv.AllColumns) + col.IsVisible = this.MapColumnToVisible[col]; + } else { + foreach (OLVColumn col in olv.AllColumns) + col.IsTileViewColumn = this.MapColumnToVisible[col]; + } + + // Collect the columns are still visible + List visibleColumns = this.RearrangableColumns.FindAll( + delegate(OLVColumn x) { return this.MapColumnToVisible[x]; }); + + // Detail view and Tile view have to be handled in different ways. + if (view == View.Details) { + // Of the still visible columns, change DisplayIndex to reflect their position in the rearranged list + olv.ChangeToFilteredColumns(view); + foreach (OLVColumn col in visibleColumns) { + col.DisplayIndex = visibleColumns.IndexOf((OLVColumn)col); + col.LastDisplayIndex = col.DisplayIndex; + } + } else { + // In Tile view, DisplayOrder does nothing. So to change the display order, we have to change the + // order of the columns in the Columns property. + // Remember, the primary column is special and has to remain first! + OLVColumn primaryColumn = this.AllColumns[0]; + visibleColumns.Remove(primaryColumn); + + olv.Columns.Clear(); + olv.Columns.Add(primaryColumn); + olv.Columns.AddRange(visibleColumns.ToArray()); + olv.CalculateReasonableTileSize(); + } + + olv.Unfreeze(); + } + + #region Event handlers + + private void buttonMoveUp_Click(object sender, EventArgs e) + { + int selectedIndex = this.objectListView1.SelectedIndices[0]; + OLVColumn col = this.RearrangableColumns[selectedIndex]; + this.RearrangableColumns.RemoveAt(selectedIndex); + this.RearrangableColumns.Insert(selectedIndex-1, col); + + this.objectListView1.BuildList(); + + EnableControls(); + } + + private void buttonMoveDown_Click(object sender, EventArgs e) + { + int selectedIndex = this.objectListView1.SelectedIndices[0]; + OLVColumn col = this.RearrangableColumns[selectedIndex]; + this.RearrangableColumns.RemoveAt(selectedIndex); + this.RearrangableColumns.Insert(selectedIndex + 1, col); + + this.objectListView1.BuildList(); + + EnableControls(); + } + + private void buttonShow_Click(object sender, EventArgs e) + { + this.objectListView1.SelectedItem.Checked = true; + } + + private void buttonHide_Click(object sender, EventArgs e) + { + this.objectListView1.SelectedItem.Checked = false; + } + + private void buttonOK_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.OK; + this.Close(); + } + + private void buttonCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } + + private void objectListView1_SelectionChanged(object sender, EventArgs e) + { + EnableControls(); + } + + #endregion + + #region Control enabling + + /// + /// Enable the controls on the dialog to match the current state + /// + protected void EnableControls() + { + if (this.objectListView1.SelectedIndices.Count == 0) { + this.buttonMoveUp.Enabled = false; + this.buttonMoveDown.Enabled = false; + this.buttonShow.Enabled = false; + this.buttonHide.Enabled = false; + } else { + // Can't move the first row up or the last row down + this.buttonMoveUp.Enabled = (this.objectListView1.SelectedIndices[0] != 0); + this.buttonMoveDown.Enabled = (this.objectListView1.SelectedIndices[0] < (this.objectListView1.GetItemCount() - 1)); + + OLVColumn selectedColumn = (OLVColumn)this.objectListView1.SelectedObject; + + // Some columns cannot be hidden (and hence cannot be Shown) + this.buttonShow.Enabled = !this.MapColumnToVisible[selectedColumn] && selectedColumn.CanBeHidden; + this.buttonHide.Enabled = this.MapColumnToVisible[selectedColumn] && selectedColumn.CanBeHidden; + } + } + #endregion + + /// + /// A Comparer that will sort a list of columns so that visible ones come before hidden ones, + /// and that are ordered by their display order. + /// + private class SortByDisplayOrder : IComparer + { + public SortByDisplayOrder(ColumnSelectionForm form) + { + this.Form = form; + } + private ColumnSelectionForm Form; + + #region IComparer Members + + int IComparer.Compare(OLVColumn x, OLVColumn y) + { + if (this.Form.MapColumnToVisible[x] && !this.Form.MapColumnToVisible[y]) + return -1; + + if (!this.Form.MapColumnToVisible[x] && this.Form.MapColumnToVisible[y]) + return 1; + + if (x.DisplayIndex == y.DisplayIndex) + return x.Text.CompareTo(y.Text); + else + return x.DisplayIndex - y.DisplayIndex; + } + + #endregion + } + } +} diff --git a/ObjectListView/Utilities/ColumnSelectionForm.resx b/ObjectListView/Utilities/ColumnSelectionForm.resx new file mode 100644 index 0000000..19dc0dd --- /dev/null +++ b/ObjectListView/Utilities/ColumnSelectionForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ObjectListView/Utilities/Generator.cs b/ObjectListView/Utilities/Generator.cs new file mode 100644 index 0000000..c8dafcc --- /dev/null +++ b/ObjectListView/Utilities/Generator.cs @@ -0,0 +1,561 @@ +/* + * Generator - Utility methods that generate columns or methods + * + * Author: Phillip Piper + * Date: 15/08/2009 22:37 + * + * Change log: + * 2012-08-16 JPP - Generator now considers [OLVChildren] and [OLVIgnore] attributes. + * 2012-06-14 JPP - Allow columns to be generated even if they are not marked with [OLVColumn] + * - Converted class from static to instance to allow it to be subclassed. + * Also, added IGenerator to allow it to be completely reimplemented. + * v2.5.1 + * 2010-11-01 JPP - DisplayIndex is now set correctly for columns that lack that attribute + * v2.4.1 + * 2010-08-25 JPP - Generator now also resets sort columns + * v2.4 + * 2010-04-14 JPP - Allow Name property to be set + * - Don't double set the Text property + * v2.3 + * 2009-08-15 JPP - Initial version + * + * To do: + * + * Copyright (C) 2009-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Reflection.Emit; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace BrightIdeasSoftware +{ + /// + /// An object that implements the IGenerator interface provides the ability + /// to dynamically create columns + /// for an ObjectListView based on the characteristics of a given collection + /// of model objects. + /// + public interface IGenerator { + /// + /// Generate columns into the given ObjectListView that come from the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + void GenerateAndReplaceColumns(ObjectListView olv, Type type, bool allProperties); + + /// + /// Generate a list of OLVColumns based on the attributes of the given type + /// If allProperties to true, all public properties will have a matching column generated. + /// If allProperties is false, only properties that have a OLVColumn attribute will have a column generated. + /// + /// + /// Will columns be generated for properties that are not marked with [OLVColumn]. + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + IList GenerateColumns(Type type, bool allProperties); + } + + /// + /// The Generator class provides methods to dynamically create columns + /// for an ObjectListView based on the characteristics of a given collection + /// of model objects. + /// + /// + /// For a given type, a Generator can create columns to match the public properties + /// of that type. The generator can consider all public properties or only those public properties marked with + /// [OLVColumn] attribute. + /// + public class Generator : IGenerator { + #region Static convenience methods + + /// + /// Gets or sets the actual generator used by the static convinence methods. + /// + /// If you subclass the standard generator or implement IGenerator yourself, + /// you should install an instance of your subclass/implementation here. + public static IGenerator Instance { + get { return Generator.instance ?? (Generator.instance = new Generator()); } + set { Generator.instance = value; } + } + private static IGenerator instance; + + /// + /// Replace all columns of the given ObjectListView with columns generated + /// from the first member of the given enumerable. If the enumerable is + /// empty or null, the ObjectListView will be cleared. + /// + /// The ObjectListView to modify + /// The collection whose first element will be used to generate columns. + static public void GenerateColumns(ObjectListView olv, IEnumerable enumerable) { + Generator.GenerateColumns(olv, enumerable, false); + } + + /// + /// Replace all columns of the given ObjectListView with columns generated + /// from the first member of the given enumerable. If the enumerable is + /// empty or null, the ObjectListView will be cleared. + /// + /// The ObjectListView to modify + /// The collection whose first element will be used to generate columns. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + static public void GenerateColumns(ObjectListView olv, IEnumerable enumerable, bool allProperties) { + // Generate columns based on the type of the first model in the collection and then quit + if (enumerable != null) { + foreach (object model in enumerable) { + Generator.Instance.GenerateAndReplaceColumns(olv, model.GetType(), allProperties); + return; + } + } + + // If we reach here, the collection was empty, so we clear the list + Generator.Instance.GenerateAndReplaceColumns(olv, null, allProperties); + } + + /// + /// Generate columns into the given ObjectListView that come from the public properties of the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + static public void GenerateColumns(ObjectListView olv, Type type) { + Generator.Instance.GenerateAndReplaceColumns(olv, type, false); + } + + /// + /// Generate columns into the given ObjectListView that come from the public properties of the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + static public void GenerateColumns(ObjectListView olv, Type type, bool allProperties) { + Generator.Instance.GenerateAndReplaceColumns(olv, type, allProperties); + } + + /// + /// Generate a list of OLVColumns based on the public properties of the given type + /// that have a OLVColumn attribute. + /// + /// + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + static public IList GenerateColumns(Type type) { + return Generator.Instance.GenerateColumns(type, false); + } + + #endregion + + #region Public interface + + /// + /// Generate columns into the given ObjectListView that come from the given + /// model object type. + /// + /// The ObjectListView to modify + /// The model type whose attributes will be considered. + /// Will columns be generated for properties that are not marked with [OLVColumn]. + public virtual void GenerateAndReplaceColumns(ObjectListView olv, Type type, bool allProperties) { + IList columns = this.GenerateColumns(type, allProperties); + TreeListView tlv = olv as TreeListView; + if (tlv != null) + this.TryGenerateChildrenDelegates(tlv, type); + this.ReplaceColumns(olv, columns); + } + + /// + /// Generate a list of OLVColumns based on the attributes of the given type + /// If allProperties to true, all public properties will have a matching column generated. + /// If allProperties is false, only properties that have a OLVColumn attribute will have a column generated. + /// + /// + /// Will columns be generated for properties that are not marked with [OLVColumn]. + /// A collection of OLVColumns matching the attributes of Type that have OLVColumnAttributes. + public virtual IList GenerateColumns(Type type, bool allProperties) { + List columns = new List(); + + // Sanity + if (type == null) + return columns; + + // Iterate all public properties in the class and build columns from those that have + // an OLVColumn attribute and that are not ignored. + foreach (PropertyInfo pinfo in type.GetProperties()) { + if (Attribute.GetCustomAttribute(pinfo, typeof(OLVIgnoreAttribute)) != null) + continue; + + OLVColumnAttribute attr = Attribute.GetCustomAttribute(pinfo, typeof(OLVColumnAttribute)) as OLVColumnAttribute; + if (attr == null) { + if (allProperties) + columns.Add(this.MakeColumnFromPropertyInfo(pinfo)); + } else { + columns.Add(this.MakeColumnFromAttribute(pinfo, attr)); + } + } + + // How many columns have DisplayIndex specifically set? + int countPositiveDisplayIndex = 0; + foreach (OLVColumn col in columns) { + if (col.DisplayIndex >= 0) + countPositiveDisplayIndex += 1; + } + + // Give columns that don't have a DisplayIndex an incremental index + int columnIndex = countPositiveDisplayIndex; + foreach (OLVColumn col in columns) + if (col.DisplayIndex < 0) + col.DisplayIndex = (columnIndex++); + + columns.Sort(delegate(OLVColumn x, OLVColumn y) { + return x.DisplayIndex.CompareTo(y.DisplayIndex); + }); + + return columns; + } + + #endregion + + #region Implementation + + /// + /// Replace all the columns in the given listview with the given list of columns. + /// + /// + /// + protected virtual void ReplaceColumns(ObjectListView olv, IList columns) { + olv.Reset(); + + // Are there new columns to add? + if (columns == null || columns.Count == 0) + return; + + // Setup the columns + olv.AllColumns.AddRange(columns); + this.PostCreateColumns(olv); + } + + /// + /// Post process columns after creating them and adding them to the AllColumns collection. + /// + /// + public virtual void PostCreateColumns(ObjectListView olv) { + if (olv.AllColumns.Exists(delegate(OLVColumn x) { return x.CheckBoxes; })) + olv.UseSubItemCheckBoxes = true; + if (olv.AllColumns.Exists(delegate(OLVColumn x) { return x.Index > 0 && (x.ImageGetter != null || !String.IsNullOrEmpty(x.ImageAspectName)); })) + olv.ShowImagesOnSubItems = true; + olv.RebuildColumns(); + olv.AutoSizeColumns(); + } + + /// + /// Create a column from the given PropertyInfo and OLVColumn attribute + /// + /// + /// + /// + protected virtual OLVColumn MakeColumnFromAttribute(PropertyInfo pinfo, OLVColumnAttribute attr) { + return MakeColumn(pinfo.Name, DisplayNameToColumnTitle(pinfo.Name), pinfo.CanWrite, pinfo.PropertyType, attr); + } + + /// + /// Make a column from the given PropertyInfo + /// + /// + /// + protected virtual OLVColumn MakeColumnFromPropertyInfo(PropertyInfo pinfo) { + return MakeColumn(pinfo.Name, DisplayNameToColumnTitle(pinfo.Name), pinfo.CanWrite, pinfo.PropertyType, null); + } + + /// + /// Make a column from the given PropertyDescriptor + /// + /// + /// + public virtual OLVColumn MakeColumnFromPropertyDescriptor(PropertyDescriptor pd) { + OLVColumnAttribute attr = pd.Attributes[typeof(OLVColumnAttribute)] as OLVColumnAttribute; + return MakeColumn(pd.Name, DisplayNameToColumnTitle(pd.DisplayName), !pd.IsReadOnly, pd.PropertyType, attr); + } + + /// + /// Create a column with all the given information + /// + /// + /// + /// + /// + /// + /// + protected virtual OLVColumn MakeColumn(string aspectName, string title, bool editable, Type propertyType, OLVColumnAttribute attr) { + + OLVColumn column = this.MakeColumn(aspectName, title, attr); + column.Name = (attr == null || String.IsNullOrEmpty(attr.Name)) ? aspectName : attr.Name; + this.ConfigurePossibleBooleanColumn(column, propertyType); + + if (attr == null) { + column.IsEditable = editable; + return column; + } + + column.AspectToStringFormat = attr.AspectToStringFormat; + if (attr.IsCheckBoxesSet) + column.CheckBoxes = attr.CheckBoxes; + column.DisplayIndex = attr.DisplayIndex; + column.FillsFreeSpace = attr.FillsFreeSpace; + if (attr.IsFreeSpaceProportionSet) + column.FreeSpaceProportion = attr.FreeSpaceProportion; + column.GroupWithItemCountFormat = attr.GroupWithItemCountFormat; + column.GroupWithItemCountSingularFormat = attr.GroupWithItemCountSingularFormat; + column.Hyperlink = attr.Hyperlink; + column.ImageAspectName = attr.ImageAspectName; + column.IsEditable = attr.IsEditableSet ? attr.IsEditable : editable; + column.IsTileViewColumn = attr.IsTileViewColumn; + column.IsVisible = attr.IsVisible; + column.MaximumWidth = attr.MaximumWidth; + column.MinimumWidth = attr.MinimumWidth; + column.Tag = attr.Tag; + if (attr.IsTextAlignSet) + column.TextAlign = attr.TextAlign; + column.ToolTipText = attr.ToolTipText; + if (attr.IsTriStateCheckBoxesSet) + column.TriStateCheckBoxes = attr.TriStateCheckBoxes; + column.UseInitialLetterForGroup = attr.UseInitialLetterForGroup; + column.Width = attr.Width; + if (attr.GroupCutoffs != null && attr.GroupDescriptions != null) + column.MakeGroupies(attr.GroupCutoffs, attr.GroupDescriptions); + return column; + } + + /// + /// Create a column. + /// + /// + /// + /// + /// + protected virtual OLVColumn MakeColumn(string aspectName, string title, OLVColumnAttribute attr) { + string columnTitle = (attr == null || String.IsNullOrEmpty(attr.Title)) ? title : attr.Title; + return new OLVColumn(columnTitle, aspectName); + } + + /// + /// Convert a property name to a displayable title. + /// + /// + /// + protected virtual string DisplayNameToColumnTitle(string displayName) { + string title = displayName.Replace("_", " "); + // Put a space between a lower-case letter that is followed immediately by an upper case letter + title = Regex.Replace(title, @"(\p{Ll})(\p{Lu})", @"$1 $2"); + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(title); + } + + /// + /// Configure the given column to show a checkbox if appropriate + /// + /// + /// + protected virtual void ConfigurePossibleBooleanColumn(OLVColumn column, Type propertyType) { + if (propertyType != typeof(bool) && propertyType != typeof(bool?) && propertyType != typeof(CheckState)) + return; + + column.CheckBoxes = true; + column.TextAlign = HorizontalAlignment.Center; + column.Width = 32; + column.TriStateCheckBoxes = (propertyType == typeof(bool?) || propertyType == typeof(CheckState)); + } + + /// + /// If this given type has an property marked with [OLVChildren], make delegates that will + /// traverse that property as the children of an instance of the model + /// + /// + /// + protected virtual void TryGenerateChildrenDelegates(TreeListView tlv, Type type) { + foreach (PropertyInfo pinfo in type.GetProperties()) { + OLVChildrenAttribute attr = Attribute.GetCustomAttribute(pinfo, typeof(OLVChildrenAttribute)) as OLVChildrenAttribute; + if (attr != null) { + this.GenerateChildrenDelegates(tlv, pinfo); + return; + } + } + } + + /// + /// Generate CanExpand and ChildrenGetter delegates from the given property. + /// + /// + /// + protected virtual void GenerateChildrenDelegates(TreeListView tlv, PropertyInfo pinfo) { + Munger childrenGetter = new Munger(pinfo.Name); + tlv.CanExpandGetter = delegate(object x) { + try { + IEnumerable result = childrenGetter.GetValueEx(x) as IEnumerable; + return !ObjectListView.IsEnumerableEmpty(result); + } + catch (MungerException ex) { + System.Diagnostics.Debug.WriteLine(ex); + return false; + } + }; + tlv.ChildrenGetter = delegate(object x) { + try { + return childrenGetter.GetValueEx(x) as IEnumerable; + } + catch (MungerException ex) { + System.Diagnostics.Debug.WriteLine(ex); + return null; + } + }; + } + #endregion + + /* + #region Dynamic methods + + /// + /// Generate methods so that reflection is not needed. + /// + /// + /// + public static void GenerateMethods(ObjectListView olv, Type type) { + foreach (OLVColumn column in olv.Columns) { + GenerateColumnMethods(column, type); + } + } + + public static void GenerateColumnMethods(OLVColumn column, Type type) { + if (column.AspectGetter == null && !String.IsNullOrEmpty(column.AspectName)) + column.AspectGetter = Generator.GenerateAspectGetter(type, column.AspectName); + } + + /// + /// Generates an aspect getter method dynamically. The method will execute + /// the given dotted chain of selectors against a model object given at runtime. + /// + /// The type of model object to be passed to the generated method + /// A dotted chain of selectors. Each selector can be the name of a + /// field, property or parameter-less method. + /// A typed delegate + /// + /// + /// If you have an AspectName of "Owner.Address.Postcode", this will generate + /// the equivilent of: this.AspectGetter = delegate (object x) { + /// return x.Owner.Address.Postcode; + /// } + /// + /// + /// + private static AspectGetterDelegate GenerateAspectGetter(Type type, string path) { + DynamicMethod getter = new DynamicMethod(String.Empty, typeof(Object), new Type[] { type }, type, true); + Generator.GenerateIL(type, path, getter.GetILGenerator()); + return (AspectGetterDelegate)getter.CreateDelegate(typeof(AspectGetterDelegate)); + } + + /// + /// This method generates the actual IL for the method. + /// + /// + /// + /// + private static void GenerateIL(Type modelType, string path, ILGenerator il) { + // Push our model object onto the stack + il.Emit(OpCodes.Ldarg_0); + OpCodes.Castclass + // Generate the IL to access each part of the dotted chain + Type type = modelType; + string[] parts = path.Split('.'); + for (int i = 0; i < parts.Length; i++) { + type = Generator.GeneratePart(il, type, parts[i], (i == parts.Length - 1)); + if (type == null) + break; + } + + // If the object to be returned is a value type (e.g. int, bool), it + // must be boxed, since the delegate returns an Object + if (type != null && type.IsValueType && !modelType.IsValueType) + il.Emit(OpCodes.Box, type); + + il.Emit(OpCodes.Ret); + } + + private static Type GeneratePart(ILGenerator il, Type type, string pathPart, bool isLastPart) { + // TODO: Generate check for null + + // Find the first member with the given nam that is a field, property, or parameter-less method + List infos = new List(type.GetMember(pathPart)); + MemberInfo info = infos.Find(delegate(MemberInfo x) { + if (x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property) + return true; + if (x.MemberType == MemberTypes.Method) + return ((MethodInfo)x).GetParameters().Length == 0; + else + return false; + }); + + // If we couldn't find anything with that name, pop the current result and return an error + if (info == null) { + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldstr, String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", pathPart, type.FullName)); + return null; + } + + // Generate the correct IL to access the member. We remember the type of object that is going to be returned + // so that we can do a method lookup on it at the next iteration + Type resultType = null; + switch (info.MemberType) { + case MemberTypes.Method: + MethodInfo mi = (MethodInfo)info; + if (mi.IsVirtual) + il.Emit(OpCodes.Callvirt, mi); + else + il.Emit(OpCodes.Call, mi); + resultType = mi.ReturnType; + break; + case MemberTypes.Property: + PropertyInfo pi = (PropertyInfo)info; + il.Emit(OpCodes.Call, pi.GetGetMethod()); + resultType = pi.PropertyType; + break; + case MemberTypes.Field: + FieldInfo fi = (FieldInfo)info; + il.Emit(OpCodes.Ldfld, fi); + resultType = fi.FieldType; + break; + } + + // If the method returned a value type, and something is going to call a method on that value, + // we need to load its address onto the stack, rather than the object itself. + if (resultType.IsValueType && !isLastPart) { + LocalBuilder lb = il.DeclareLocal(resultType); + il.Emit(OpCodes.Stloc, lb); + il.Emit(OpCodes.Ldloca, lb); + } + + return resultType; + } + + #endregion + */ + } +} diff --git a/ObjectListView/Utilities/OLVExporter.cs b/ObjectListView/Utilities/OLVExporter.cs new file mode 100644 index 0000000..c5ab719 --- /dev/null +++ b/ObjectListView/Utilities/OLVExporter.cs @@ -0,0 +1,297 @@ +/* + * OLVExporter - Export the contents of an ObjectListView into various text-based formats + * + * Author: Phillip Piper + * Date: 7 August 2012, 10:35pm + * + * Change log: + * 2012-08-07 JPP Initial code + * + * Copyright (C) 2012 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace BrightIdeasSoftware { + /// + /// An OLVExporter converts a collection of rows from an ObjectListView + /// into a variety of textual formats. + /// + public class OLVExporter { + + /// + /// What format will be used for exporting + /// + public enum ExportFormat { + + /// + /// Tab separated values, according to http://www.iana.org/assignments/media-types/text/tab-separated-values + /// + TabSeparated = 1, + + /// + /// Alias for TabSeparated + /// + TSV = 1, + + /// + /// Comma separated values, according to http://www.ietf.org/rfc/rfc4180.txt + /// + CSV, + + /// + /// HTML table, according to me + /// + HTML + } + + #region Life and death + + /// + /// Create an empty exporter + /// + public OLVExporter() {} + + /// + /// Create an exporter that will export all the rows of the given ObjectListView + /// + /// + public OLVExporter(ObjectListView olv) : this(olv, olv.Objects) {} + + /// + /// Create an exporter that will export all the given rows from the given ObjectListView + /// + /// + /// + public OLVExporter(ObjectListView olv, IEnumerable objectsToExport) { + if (olv == null) throw new ArgumentNullException("olv"); + if (objectsToExport == null) throw new ArgumentNullException("objectsToExport"); + + this.ListView = olv; + this.ModelObjects = ObjectListView.EnumerableToArray(objectsToExport, true); + } + + #endregion + + #region Properties + + /// + /// Gets or sets whether hidden columns will also be included in the textual + /// representation. If this is false (the default), only visible columns will + /// be included. + /// + public bool IncludeHiddenColumns { + get { return includeHiddenColumns; } + set { includeHiddenColumns = value; } + } + private bool includeHiddenColumns; + + /// + /// Gets or sets whether column headers will also be included in the text + /// and HTML representation. Default is true. + /// + public bool IncludeColumnHeaders { + get { return includeColumnHeaders; } + set { includeColumnHeaders = value; } + } + private bool includeColumnHeaders = true; + + /// + /// Gets the ObjectListView that is being used as the source of the data + /// to be exported + /// + public ObjectListView ListView { + get { return objectListView; } + set { objectListView = value; } + } + private ObjectListView objectListView; + + /// + /// Gets the model objects that are to be placed in the data object + /// + public IList ModelObjects { + get { return modelObjects; } + set { modelObjects = value; } + } + private IList modelObjects = new ArrayList(); + + #endregion + + #region Commands + + /// + /// Export the nominated rows from the nominated ObjectListView. + /// Returns the result in the expected format. + /// + /// + /// + /// This will perform only one conversion, even if called multiple times with different formats. + public string ExportTo(ExportFormat format) { + if (results == null) + this.Convert(); + + return results[format]; + } + + /// + /// Convert + /// + public void Convert() { + + IList columns = this.IncludeHiddenColumns ? this.ListView.AllColumns : this.ListView.ColumnsInDisplayOrder; + + StringBuilder sbText = new StringBuilder(); + StringBuilder sbCsv = new StringBuilder(); + StringBuilder sbHtml = new StringBuilder(""); + + // Include column headers + if (this.IncludeColumnHeaders) { + List strings = new List(); + List columnFormats = new List(); + foreach (OLVColumn col in columns) + { + strings.Add(col.Text); + columnFormats.Add(""); + } + + WriteOneRow(sbText, strings, "", "\t", "", null, columnFormats); + WriteOneRow(sbHtml, strings, "", HtmlEncode, columnFormats); + WriteOneRow(sbCsv, strings, "", ",", "", CsvEncode, columnFormats); + } + + foreach (object modelObject in this.ModelObjects) { + List strings = new List(); + List columnFormats = new List(); + foreach (OLVColumn col in columns) + { + strings.Add(col.GetStringValue(modelObject)); + columnFormats.Add(col.TextCopyFormat); + } + + WriteOneRow(sbText, strings, "", "\t", "", TextEncode, columnFormats); + WriteOneRow(sbHtml, strings, "", HtmlEncode, columnFormats); + WriteOneRow(sbCsv, strings, "", ",", "", CsvEncode, columnFormats); + } + sbHtml.AppendLine("
", "", "
", "", "
"); + + results = new Dictionary(); + results[ExportFormat.TabSeparated] = sbText.ToString(); + results[ExportFormat.CSV] = sbCsv.ToString(); + results[ExportFormat.HTML] = sbHtml.ToString(); + } + + private delegate string StringToString(string str,string format); + + private void WriteOneRow(StringBuilder sb, IEnumerable strings, string startRow, string betweenCells, string endRow, StringToString encoder, List rowFormats) { + sb.Append(startRow); + bool first = true; + int i = 0; + foreach (string s in strings) { + if (!first) + sb.Append(betweenCells); + sb.Append(encoder == null ? s : encoder(s,rowFormats[i++])); + first = false; + } + sb.AppendLine(endRow); + } + + private Dictionary results; + + #endregion + + #region Encoding + + private static string TextEncode(string text, string format) + { + if (text == null) + return null; + if (string.IsNullOrEmpty(format)) + return text; + StringBuilder sb = new StringBuilder(); + sb.AppendFormat(format, text); + return sb.ToString(); + } + + /// + /// Encode a string such that it can be used as a value in a CSV file. + /// This basically means replacing any quote mark with two quote marks, + /// and enclosing the whole string in quotes. + /// + /// + /// + private static string CsvEncode(string text, string format) { + if (text == null) + return null; + + const string DOUBLEQUOTE = @""""; // one double quote + const string TWODOUBEQUOTES = @""""""; // two double quotes + + StringBuilder sb = new StringBuilder(DOUBLEQUOTE); + sb.Append(text.Replace(DOUBLEQUOTE, TWODOUBEQUOTES)); + sb.Append(DOUBLEQUOTE); + + return sb.ToString(); + } + + /// + /// HTML-encodes a string and returns the encoded string. + /// + /// The text string to encode. + /// The HTML-encoded text. + /// Taken from http://www.west-wind.com/weblog/posts/2009/Feb/05/Html-and-Uri-String-Encoding-without-SystemWeb + private static string HtmlEncode(string text, string format) { + if (text == null) + return null; + + StringBuilder sb = new StringBuilder(text.Length); + + int len = text.Length; + for (int i = 0; i < len; i++) { + switch (text[i]) { + case '<': + sb.Append("<"); + break; + case '>': + sb.Append(">"); + break; + case '"': + sb.Append("""); + break; + case '&': + sb.Append("&"); + break; + default: + if (text[i] > 159) { + // decimal numeric entity + sb.Append("&#"); + sb.Append(((int)text[i]).ToString(CultureInfo.InvariantCulture)); + sb.Append(";"); + } else + sb.Append(text[i]); + break; + } + } + return sb.ToString(); + } + #endregion + } +} \ No newline at end of file diff --git a/ObjectListView/Utilities/TypedObjectListView.cs b/ObjectListView/Utilities/TypedObjectListView.cs new file mode 100644 index 0000000..e3a8c1a --- /dev/null +++ b/ObjectListView/Utilities/TypedObjectListView.cs @@ -0,0 +1,561 @@ +/* + * TypedObjectListView - A wrapper around an ObjectListView that provides type-safe delegates. + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * v2.6 + * 2012-10-26 JPP - Handle rare case where a null model object was passed into aspect getters. + * v2.3 + * 2009-03-31 JPP - Added Objects property + * 2008-11-26 JPP - Added tool tip getting methods + * 2008-11-05 JPP - Added CheckState handling methods + * 2008-10-24 JPP - Generate dynamic methods MkII. This one handles value types + * 2008-10-21 JPP - Generate dynamic methods + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Reflection; +using System.Reflection.Emit; + +namespace BrightIdeasSoftware +{ + /// + /// A TypedObjectListView is a type-safe wrapper around an ObjectListView. + /// + /// + /// VCS does not support generics on controls. It can be faked to some degree, but it + /// cannot be completely overcome. In our case in particular, there is no way to create + /// the custom OLVColumn's that we need to truly be generic. So this wrapper is an + /// experiment in providing some type-safe access in a way that is useful and available today. + /// A TypedObjectListView is not more efficient than a normal ObjectListView. + /// Underneath, the same name of casts are performed. But it is easier to use since you + /// do not have to write the casts yourself. + /// + /// + /// The class of model object that the list will manage + /// + /// To use a TypedObjectListView, you write code like this: + /// + /// TypedObjectListView<Person> tlist = new TypedObjectListView<Person>(this.listView1); + /// tlist.CheckStateGetter = delegate(Person x) { return x.IsActive; }; + /// tlist.GetColumn(0).AspectGetter = delegate(Person x) { return x.Name; }; + /// ... + /// + /// To iterate over the selected objects, you can write something elegant like this: + /// + /// foreach (Person x in tlist.SelectedObjects) { + /// x.GrantSalaryIncrease(); + /// } + /// + /// + public class TypedObjectListView where T : class + { + /// + /// Create a typed wrapper around the given list. + /// + /// The listview to be wrapped + public TypedObjectListView(ObjectListView olv) { + this.olv = olv; + } + + //-------------------------------------------------------------------------------------- + // Properties + + /// + /// Return the model object that is checked, if only one row is checked. + /// If zero rows are checked, or more than one row, null is returned. + /// + public virtual T CheckedObject { + get { return (T)this.olv.CheckedObject; } + } + + /// + /// Return the list of all the checked model objects + /// + public virtual IList CheckedObjects { + get { + IList checkedObjects = this.olv.CheckedObjects; + List objects = new List(checkedObjects.Count); + foreach (object x in checkedObjects) + objects.Add((T)x); + + return objects; + } + set { this.olv.CheckedObjects = (IList)value; } + } + + /// + /// The ObjectListView that is being wrapped + /// + public virtual ObjectListView ListView { + get { return olv; } + set { olv = value; } + } + private ObjectListView olv; + + /// + /// Get or set the list of all model objects + /// + public virtual IList Objects { + get { + List objects = new List(this.olv.GetItemCount()); + for (int i = 0; i < this.olv.GetItemCount(); i++) + objects.Add(this.GetModelObject(i)); + + return objects; + } + set { this.olv.SetObjects(value); } + } + + /// + /// Return the model object that is selected, if only one row is selected. + /// If zero rows are selected, or more than one row, null is returned. + /// + public virtual T SelectedObject { + get { return (T)this.olv.SelectedObject; } + set { this.olv.SelectedObject = value; } + } + + /// + /// The list of model objects that are selected. + /// + public virtual IList SelectedObjects { + get { + List objects = new List(this.olv.SelectedIndices.Count); + foreach (int index in this.olv.SelectedIndices) + objects.Add((T)this.olv.GetModelObject(index)); + + return objects; + } + set { this.olv.SelectedObjects = (IList)value; } + } + + //-------------------------------------------------------------------------------------- + // Accessors + + /// + /// Return a typed wrapper around the column at the given index + /// + /// The index of the column + /// A typed column or null + public virtual TypedColumn GetColumn(int i) { + return new TypedColumn(this.olv.GetColumn(i)); + } + + /// + /// Return a typed wrapper around the column with the given name + /// + /// The name of the column + /// A typed column or null + public virtual TypedColumn GetColumn(string name) { + return new TypedColumn(this.olv.GetColumn(name)); + } + + /// + /// Return the model object at the given index + /// + /// The index of the model object + /// The model object or null + public virtual T GetModelObject(int index) { + return (T)this.olv.GetModelObject(index); + } + + //-------------------------------------------------------------------------------------- + // Delegates + + /// + /// CheckStateGetter + /// + /// + /// + public delegate CheckState TypedCheckStateGetterDelegate(T rowObject); + + /// + /// Gets or sets the check state getter + /// + public virtual TypedCheckStateGetterDelegate CheckStateGetter { + get { return checkStateGetter; } + set { + this.checkStateGetter = value; + if (value == null) + this.olv.CheckStateGetter = null; + else + this.olv.CheckStateGetter = delegate(object x) { + return this.checkStateGetter((T)x); + }; + } + } + private TypedCheckStateGetterDelegate checkStateGetter; + + /// + /// BooleanCheckStateGetter + /// + /// + /// + public delegate bool TypedBooleanCheckStateGetterDelegate(T rowObject); + + /// + /// Gets or sets the boolean check state getter + /// + public virtual TypedBooleanCheckStateGetterDelegate BooleanCheckStateGetter { + set { + if (value == null) + this.olv.BooleanCheckStateGetter = null; + else + this.olv.BooleanCheckStateGetter = delegate(object x) { + return value((T)x); + }; + } + } + + /// + /// CheckStatePutter + /// + /// + /// + /// + public delegate CheckState TypedCheckStatePutterDelegate(T rowObject, CheckState newValue); + + /// + /// Gets or sets the check state putter delegate + /// + public virtual TypedCheckStatePutterDelegate CheckStatePutter { + get { return checkStatePutter; } + set { + this.checkStatePutter = value; + if (value == null) + this.olv.CheckStatePutter = null; + else + this.olv.CheckStatePutter = delegate(object x, CheckState newValue) { + return this.checkStatePutter((T)x, newValue); + }; + } + } + private TypedCheckStatePutterDelegate checkStatePutter; + + /// + /// BooleanCheckStatePutter + /// + /// + /// + /// + public delegate bool TypedBooleanCheckStatePutterDelegate(T rowObject, bool newValue); + + /// + /// Gets or sets the boolean check state putter + /// + public virtual TypedBooleanCheckStatePutterDelegate BooleanCheckStatePutter { + set { + if (value == null) + this.olv.BooleanCheckStatePutter = null; + else + this.olv.BooleanCheckStatePutter = delegate(object x, bool newValue) { + return value((T)x, newValue); + }; + } + } + + /// + /// ToolTipGetter + /// + /// + /// + /// + public delegate String TypedCellToolTipGetterDelegate(OLVColumn column, T modelObject); + + /// + /// Gets or sets the cell tooltip getter + /// + public virtual TypedCellToolTipGetterDelegate CellToolTipGetter { + set { + if (value == null) + this.olv.CellToolTipGetter = null; + else + this.olv.CellToolTipGetter = delegate(OLVColumn col, Object x) { + return value(col, (T)x); + }; + } + } + + /// + /// Gets or sets the header tool tip getter + /// + public virtual HeaderToolTipGetterDelegate HeaderToolTipGetter { + get { return this.olv.HeaderToolTipGetter; } + set { this.olv.HeaderToolTipGetter = value; } + } + + //-------------------------------------------------------------------------------------- + // Commands + + /// + /// This method will generate AspectGetters for any column that has an AspectName. + /// + public virtual void GenerateAspectGetters() { + for (int i = 0; i < this.ListView.Columns.Count; i++) + this.GetColumn(i).GenerateAspectGetter(); + } + } + + /// + /// A type-safe wrapper around an OLVColumn + /// + /// + public class TypedColumn where T : class + { + /// + /// Creates a TypedColumn + /// + /// + public TypedColumn(OLVColumn column) { + this.column = column; + } + private OLVColumn column; + + /// + /// + /// + /// + /// + public delegate Object TypedAspectGetterDelegate(T rowObject); + + /// + /// + /// + /// + /// + public delegate void TypedAspectPutterDelegate(T rowObject, Object newValue); + + /// + /// + /// + /// + /// + public delegate Object TypedGroupKeyGetterDelegate(T rowObject); + + /// + /// + /// + /// + /// + public delegate Object TypedImageGetterDelegate(T rowObject); + + /// + /// + /// + public TypedAspectGetterDelegate AspectGetter { + get { return this.aspectGetter; } + set { + this.aspectGetter = value; + if (value == null) + this.column.AspectGetter = null; + else + this.column.AspectGetter = delegate(object x) { + return x == null ? null : this.aspectGetter((T)x); + }; + } + } + private TypedAspectGetterDelegate aspectGetter; + + /// + /// + /// + public TypedAspectPutterDelegate AspectPutter { + get { return aspectPutter; } + set { + this.aspectPutter = value; + if (value == null) + this.column.AspectPutter = null; + else + this.column.AspectPutter = delegate(object x, object newValue) { + this.aspectPutter((T)x, newValue); + }; + } + } + private TypedAspectPutterDelegate aspectPutter; + + /// + /// + /// + public TypedImageGetterDelegate ImageGetter { + get { return imageGetter; } + set { + this.imageGetter = value; + if (value == null) + this.column.ImageGetter = null; + else + this.column.ImageGetter = delegate(object x) { + return this.imageGetter((T)x); + }; + } + } + private TypedImageGetterDelegate imageGetter; + + /// + /// + /// + public TypedGroupKeyGetterDelegate GroupKeyGetter { + get { return groupKeyGetter; } + set { + this.groupKeyGetter = value; + if (value == null) + this.column.GroupKeyGetter = null; + else + this.column.GroupKeyGetter = delegate(object x) { + return this.groupKeyGetter((T)x); + }; + } + } + private TypedGroupKeyGetterDelegate groupKeyGetter; + + #region Dynamic methods + + /// + /// Generate an aspect getter that does the same thing as the AspectName, + /// except without using reflection. + /// + /// + /// + /// If you have an AspectName of "Owner.Address.Postcode", this will generate + /// the equivilent of: this.AspectGetter = delegate (object x) { + /// return x.Owner.Address.Postcode; + /// } + /// + /// + /// + /// If AspectName is empty, this method will do nothing, otherwise + /// this will replace any existing AspectGetter. + /// + /// + public void GenerateAspectGetter() { + if (!String.IsNullOrEmpty(this.column.AspectName)) + this.AspectGetter = this.GenerateAspectGetter(typeof(T), this.column.AspectName); + } + + /// + /// Generates an aspect getter method dynamically. The method will execute + /// the given dotted chain of selectors against a model object given at runtime. + /// + /// The type of model object to be passed to the generated method + /// A dotted chain of selectors. Each selector can be the name of a + /// field, property or parameter-less method. + /// A typed delegate + private TypedAspectGetterDelegate GenerateAspectGetter(Type type, string path) { + DynamicMethod getter = new DynamicMethod(String.Empty, + typeof(Object), new Type[] { type }, type, true); + this.GenerateIL(type, path, getter.GetILGenerator()); + return (TypedAspectGetterDelegate)getter.CreateDelegate(typeof(TypedAspectGetterDelegate)); + } + + /// + /// This method generates the actual IL for the method. + /// + /// + /// + /// + private void GenerateIL(Type type, string path, ILGenerator il) { + // Push our model object onto the stack + il.Emit(OpCodes.Ldarg_0); + + // Generate the IL to access each part of the dotted chain + string[] parts = path.Split('.'); + for (int i = 0; i < parts.Length; i++) { + type = this.GeneratePart(il, type, parts[i], (i == parts.Length - 1)); + if (type == null) + break; + } + + // If the object to be returned is a value type (e.g. int, bool), it + // must be boxed, since the delegate returns an Object + if (type != null && type.IsValueType && !typeof(T).IsValueType) + il.Emit(OpCodes.Box, type); + + il.Emit(OpCodes.Ret); + } + + private Type GeneratePart(ILGenerator il, Type type, string pathPart, bool isLastPart) { + // TODO: Generate check for null + + // Find the first member with the given nam that is a field, property, or parameter-less method + List infos = new List(type.GetMember(pathPart)); + MemberInfo info = infos.Find(delegate(MemberInfo x) { + if (x.MemberType == MemberTypes.Field || x.MemberType == MemberTypes.Property) + return true; + if (x.MemberType == MemberTypes.Method) + return ((MethodInfo)x).GetParameters().Length == 0; + else + return false; + }); + + // If we couldn't find anything with that name, pop the current result and return an error + if (info == null) { + il.Emit(OpCodes.Pop); + if (Munger.IgnoreMissingAspects) + il.Emit(OpCodes.Ldnull); + else + il.Emit(OpCodes.Ldstr, String.Format("'{0}' is not a parameter-less method, property or field of type '{1}'", pathPart, type.FullName)); + return null; + } + + // Generate the correct IL to access the member. We remember the type of object that is going to be returned + // so that we can do a method lookup on it at the next iteration + Type resultType = null; + switch (info.MemberType) { + case MemberTypes.Method: + MethodInfo mi = (MethodInfo)info; + if (mi.IsVirtual) + il.Emit(OpCodes.Callvirt, mi); + else + il.Emit(OpCodes.Call, mi); + resultType = mi.ReturnType; + break; + case MemberTypes.Property: + PropertyInfo pi = (PropertyInfo)info; + il.Emit(OpCodes.Call, pi.GetGetMethod()); + resultType = pi.PropertyType; + break; + case MemberTypes.Field: + FieldInfo fi = (FieldInfo)info; + il.Emit(OpCodes.Ldfld, fi); + resultType = fi.FieldType; + break; + } + + // If the method returned a value type, and something is going to call a method on that value, + // we need to load its address onto the stack, rather than the object itself. + if (resultType.IsValueType && !isLastPart) { + LocalBuilder lb = il.DeclareLocal(resultType); + il.Emit(OpCodes.Stloc, lb); + il.Emit(OpCodes.Ldloca, lb); + } + + return resultType; + } + + #endregion + } +} diff --git a/ObjectListView/VirtualObjectListView.cs b/ObjectListView/VirtualObjectListView.cs new file mode 100644 index 0000000..79e97cd --- /dev/null +++ b/ObjectListView/VirtualObjectListView.cs @@ -0,0 +1,1227 @@ +/* + * VirtualObjectListView - A virtual listview delays fetching model objects until they are actually displayed. + * + * Author: Phillip Piper + * Date: 27/09/2008 9:15 AM + * + * Change log: + * v2.8 + * 2014-09-26 JPP - Correct an incorrect use of checkStateMap when setting CheckedObjects + * and a CheckStateGetter is installed + * v2.6 + * 2012-06-13 JPP - Corrected several bugs related to groups on virtual lists. + * - Added EnsureNthGroupVisible() since EnsureGroupVisible() can't work on virtual lists. + * v2.5.1 + * 2012-05-04 JPP - Avoid bug/feature in ListView.VirtalListSize setter that causes flickering + * when the size of the list changes. + * 2012-04-24 JPP - Fixed bug that occurred when adding/removing item while the view was grouped. + * v2.5 + * 2011-05-31 JPP - Setting CheckedObjects is more efficient on large collections + * 2011-04-05 JPP - CheckedObjects now only returns objects that are currently in the list. + * ClearObjects() now resets all check state info. + * 2011-03-31 JPP - Filtering on grouped virtual lists no longer behaves strangely. + * 2011-03-17 JPP - Virtual lists can (finally) set CheckBoxes back to false if it has been set to true. + * (this is a little hacky and may not work reliably). + * - GetNextItem() and GetPreviousItem() now work on grouped virtual lists. + * 2011-03-08 JPP - BREAKING CHANGE: 'DataSource' was renamed to 'VirtualListDataSource'. This was necessary + * to allow FastDataListView which is both a DataListView AND a VirtualListView -- + * which both used a 'DataSource' property :( + * v2.4 + * 2010-04-01 JPP - Support filtering + * v2.3 + * 2009-08-28 JPP - BIG CHANGE. Virtual lists can now have groups! + * - Objects property now uses "yield return" -- much more efficient for big lists + * 2009-08-07 JPP - Use new scheme for formatting rows/cells + * v2.2.1 + * 2009-07-24 JPP - Added specialised version of RefreshSelectedObjects() which works efficiently with virtual lists + * (thanks to chriss85 for finding this bug) + * 2009-07-03 JPP - Standardized code format + * v2.2 + * 2009-04-06 JPP - ClearObjects() now works again + * v2.1 + * 2009-02-24 JPP - Removed redundant OnMouseDown() since checkbox + * handling is now handled in the base class + * 2009-01-07 JPP - Made all public and protected methods virtual + * 2008-12-07 JPP - Trigger Before/AfterSearching events + * 2008-11-15 JPP - Fixed some caching issues + * 2008-11-05 JPP - Rewrote handling of check boxes + * 2008-10-28 JPP - Handle SetSelectedObjects(null) + * 2008-10-02 JPP - MAJOR CHANGE: Use IVirtualListDataSource + * 2008-09-27 JPP - Separated from ObjectListView.cs + * + * Copyright (C) 2006-2014 Phillip Piper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * If you wish to use this code in a closed source application, please contact phillip_piper@bigfoot.com. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Reflection; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +namespace BrightIdeasSoftware +{ + /// + /// A virtual object list view operates in virtual mode, that is, it only gets model objects for + /// a row when it is needed. This gives it the ability to handle very large numbers of rows with + /// minimal resources. + /// + /// A listview is not a great user interface for a large number of items. But if you've + /// ever wanted to have a list with 10 million items, go ahead, knock yourself out. + /// Virtual lists can never iterate their contents. That would defeat the whole purpose. + /// Animated GIFs should not be used in virtual lists. Animated GIFs require some state + /// information to be stored for each animation, but virtual lists specifically do not keep any state information. + /// In any case, you really do not want to keep state information for 10 million animations! + /// + /// Although it isn't documented, .NET virtual lists cannot have checkboxes. This class codes around this limitation, + /// but you must use the functions provided by ObjectListView: CheckedObjects, CheckObject(), UncheckObject() and their friends. + /// If you use the normal check box properties (CheckedItems or CheckedIndicies), they will throw an exception, since the + /// list is in virtual mode, and .NET "knows" it can't handle checkboxes in virtual mode. + /// + /// Due to the limits of the underlying Windows control, virtual lists do not trigger ItemCheck/ItemChecked events. + /// Use a CheckStatePutter instead. + /// To enable grouping, you must provide an implmentation of IVirtualGroups interface, via the GroupingStrategy property. + /// Similarly, to enable filtering on the list, your VirtualListDataSource must also implement the IFilterableDataSource interface. + /// + public class VirtualObjectListView : ObjectListView + { + /// + /// Create a VirtualObjectListView + /// + public VirtualObjectListView() + : base() { + this.VirtualMode = true; // Virtual lists have to be virtual -- no prizes for guessing that :) + + this.CacheVirtualItems += new CacheVirtualItemsEventHandler(this.HandleCacheVirtualItems); + this.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(this.HandleRetrieveVirtualItem); + this.SearchForVirtualItem += new SearchForVirtualItemEventHandler(this.HandleSearchForVirtualItem); + + // At the moment, we don't need to handle this event. But we'll keep this comment to remind us about it. + //this.VirtualItemsSelectionRangeChanged += new ListViewVirtualItemsSelectionRangeChangedEventHandler(VirtualObjectListView_VirtualItemsSelectionRangeChanged); + + this.VirtualListDataSource = new VirtualListVersion1DataSource(this); + + // Virtual lists have to manage their own check state, since the normal ListView control + // doesn't even allow checkboxes on virtual lists + this.PersistentCheckBoxes = true; + } + + #region Public Properties + + /// + /// Gets whether or not this listview is capabale of showing groups + /// + [Browsable(false)] + public override bool CanShowGroups { + get { + // Virtual lists need Vista and a grouping strategy to show groups + return (ObjectListView.IsVistaOrLater && this.GroupingStrategy != null); + } + } + + /// + /// Gets or sets whether this ObjectListView will show checkboxes in the primary column + /// + /// Due to code in the base ListView class, turning off CheckBoxes on a virtual + /// list always throws an InvalidOperationException. This implementation codes around + /// that limitation. + [Category("Appearance"), + Description("Should the list view show checkboxes?"), + DefaultValue(false)] + new public bool CheckBoxes { + get { return base.CheckBoxes; } + set { + try { + base.CheckBoxes = value; + } + catch (InvalidOperationException) { + this.StateImageList = null; + this.VirtualMode = false; + base.CheckBoxes = value; + this.VirtualMode = true; + this.ShowGroups = this.ShowGroups; + this.BuildList(true); + } + } + } + + /// + /// Get or set the collection of model objects that are checked. + /// When setting this property, any row whose model object isn't + /// in the given collection will be unchecked. Setting to null is + /// equivilent to unchecking all. + /// + /// + /// + /// This property returns a simple collection. Changes made to the returned + /// collection do NOT affect the list. This is different to the behaviour of + /// CheckedIndicies collection. + /// + /// + /// When getting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects. + /// When setting CheckedObjects, the performance of this method is O(n) where n is the number of checked objects plus + /// the number of objects to be checked. + /// + /// + /// If the ListView is not currently showing CheckBoxes, this property does nothing. It does + /// not remember any check box settings made. + /// + /// + /// This class optimizes the mangement of CheckStates so that it will work efficiently even on + /// large lists of item. However, those optimizations are impossible if you install a CheckStateGetter. + /// Witha CheckStateGetter installed, the performance of this method is O(n) where n is the size + /// of the list. This could be painfully slow. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IList CheckedObjects { + get { + // If we aren't should checkboxes, then no objects can be checked + if (!this.CheckBoxes) + return new ArrayList(); + + // If the data source has somehow vanished, we can't do anything + if (this.VirtualListDataSource == null) + return new ArrayList(); + + // If a custom check state getter is install, we can't use our check state management + // We have to use the (slower) base version. + if (this.CheckStateGetter != null) + return base.CheckedObjects; + + // Collect items that are checked AND that still exist in the list. + ArrayList objects = new ArrayList(); + foreach (KeyValuePair kvp in this.CheckStateMap) + { + if (kvp.Value == CheckState.Checked && + (!this.CheckedObjectsMustStillExistInList || + this.VirtualListDataSource.GetObjectIndex(kvp.Key) >= 0)) + objects.Add(kvp.Key); + } + return objects; + } + set { + if (!this.CheckBoxes) + return; + + // If a custom check state getter is install, we can't use our check state management + // We have to use the (slower) base version. + if (this.CheckStateGetter != null) { + base.CheckedObjects = value; + return; + } + + Stopwatch sw = Stopwatch.StartNew(); + + // Set up an efficient way of testing for the presence of a particular model + Hashtable table = new Hashtable(this.GetItemCount()); + if (value != null) { + foreach (object x in value) + table[x] = true; + } + + this.BeginUpdate(); + + // Uncheck anything that is no longer checked + Object[] keys = new Object[this.CheckStateMap.Count]; + this.CheckStateMap.Keys.CopyTo(keys, 0); + foreach (Object key in keys) { + if (!table.Contains(key)) + this.SetObjectCheckedness(key, CheckState.Unchecked); + } + + // Check all the new checked objects + foreach (Object x in table.Keys) + this.SetObjectCheckedness(x, CheckState.Checked); + + this.EndUpdate(); + + Debug.WriteLine(String.Format("PERF - Setting virtual CheckedObjects on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); + } + } + + /// + /// Gets or sets whether or not an object will be included in the CheckedObjects + /// collection, even if it is not present in the control at the moment + /// + /// + /// This property is an implementation detail and should not be altered. + /// + protected internal bool CheckedObjectsMustStillExistInList { + get { return checkedObjectsMustStillExistInList; } + set { checkedObjectsMustStillExistInList = value; } + } + private bool checkedObjectsMustStillExistInList = true; + + /// + /// Gets the collection of objects that survive any filtering that may be in place. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable FilteredObjects { + get { + for (int i = 0; i < this.GetItemCount(); i++) + yield return this.GetModelObject(i); + } + } + + /// + /// Gets or sets the strategy that will be used to create groups + /// + /// + /// This must be provided for a virtual list to show groups. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public IVirtualGroups GroupingStrategy { + get { return this.groupingStrategy; } + set { this.groupingStrategy = value; } + } + private IVirtualGroups groupingStrategy; + + /// + /// Gets whether or not the current list is filtering its contents + /// + /// + /// This is only possible if our underlying data source supports filtering. + /// + public override bool IsFiltering { + get { + return base.IsFiltering && (this.VirtualListDataSource is IFilterableDataSource); + } + } + + /// + /// Get/set the collection of objects that this list will show + /// + /// + /// + /// The contents of the control will be updated immediately after setting this property. + /// + /// Setting this property preserves selection, if possible. Use SetObjects() if + /// you do not want to preserve the selection. Preserving selection is the slowest part of this + /// code -- performance is O(n) where n is the number of selected rows. + /// This method is not thread safe. + /// The property DOES work on virtual lists, but if you try to iterate through a list + /// of 10 million objects, it may take some time :) + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override IEnumerable Objects { + get { + try { + // If we are filtering, we have to temporarily disable filtering so we get + // the whole collection + if (this.IsFiltering) + ((IFilterableDataSource)this.VirtualListDataSource).ApplyFilters(null, null); + return this.FilteredObjects; + } finally { + if (this.IsFiltering) + ((IFilterableDataSource)this.VirtualListDataSource).ApplyFilters(this.ModelFilter, this.ListFilter); + } + } + set { base.Objects = value; } + } + + /// + /// This delegate is used to fetch a rowObject, given it's index within the list + /// + /// Only use this property if you are not using a VirtualListDataSource. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual RowGetterDelegate RowGetter { + get { return ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter; } + set { ((VirtualListVersion1DataSource)this.virtualListDataSource).RowGetter = value; } + } + + /// + /// Should this list show its items in groups? + /// + [Category("Appearance"), + Description("Should the list view show items in groups?"), + DefaultValue(true)] + override public bool ShowGroups { + get { + // Pre-Vista, virtual lists cannot show groups + return ObjectListView.IsVistaOrLater && this.showGroups; + } + set { + this.showGroups = value; + if (this.Created && !value) + this.DisableVirtualGroups(); + } + } + private bool showGroups; + + + /// + /// Get/set the data source that is behind this virtual list + /// + /// Setting this will cause the list to redraw. + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public virtual IVirtualListDataSource VirtualListDataSource { + get { + return this.virtualListDataSource; + } + set { + this.virtualListDataSource = value; + this.CustomSorter = delegate(OLVColumn column, SortOrder sortOrder) { + this.ClearCachedInfo(); + this.virtualListDataSource.Sort(column, sortOrder); + }; + this.BuildList(false); + } + } + private IVirtualListDataSource virtualListDataSource; + + /// + /// Gets or sets the number of rows in this virtual list. + /// + /// + /// There is an annoying feature/bug in the .NET ListView class. + /// When you change the VirtualListSize property, it always scrolls so + /// that the focused item is the top item. This is annoying since it makes + /// the virtual list seem to flicker as the control scrolls to show the focused + /// item and then scrolls back to where ObjectListView wants it to be. + /// + [Browsable(false), + DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + protected new virtual int VirtualListSize { + get { return base.VirtualListSize; } + set { + if (value == this.VirtualListSize || value < 0) + return; + + // Get around the 'private' marker on 'virtualListSize' field using reflection + if (virtualListSizeFieldInfo == null) { + virtualListSizeFieldInfo = typeof(ListView).GetField("virtualListSize", BindingFlags.NonPublic | BindingFlags.Instance); + System.Diagnostics.Debug.Assert(virtualListSizeFieldInfo != null); + } + + // Set the base class private field so that it keeps on working + virtualListSizeFieldInfo.SetValue(this, value); + + // Send a raw message to change the virtual list size *without* changing the scroll position + if (this.IsHandleCreated && !this.DesignMode) + NativeMethods.SetItemCount(this, value); + } + } + static private FieldInfo virtualListSizeFieldInfo; + + #endregion + + #region OLV accessing + + /// + /// Return the number of items in the list + /// + /// the number of items in the list + public override int GetItemCount() { + return this.VirtualListSize; + } + + /// + /// Return the model object at the given index + /// + /// Index of the model object to be returned + /// A model object + public override object GetModelObject(int index) { + if (this.VirtualListDataSource != null && index >= 0 && index < this.GetItemCount()) + return this.VirtualListDataSource.GetNthObject(index); + else + return null; + } + + /// + /// Find the given model object within the listview and return its index + /// + /// The model object to be found + /// The index of the object. -1 means the object was not present + public override int IndexOf(Object modelObject) { + if (this.VirtualListDataSource == null || modelObject == null) + return -1; + + return this.VirtualListDataSource.GetObjectIndex(modelObject); + } + + /// + /// Return the OLVListItem that displays the given model object + /// + /// The modelObject whose item is to be found + /// The OLVListItem that displays the model, or null + /// This method has O(n) performance. + public override OLVListItem ModelToItem(object modelObject) { + if (this.VirtualListDataSource == null || modelObject == null) + return null; + + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + return index >= 0 ? this.GetItem(index) : null; + } + + #endregion + + #region Object manipulation + + /// + /// Add the given collection of model objects to this control. + /// + /// A collection of model objects + /// + /// The added objects will appear in their correct sort position, if sorting + /// is active. Otherwise, they will appear at the end of the list. + /// No check is performed to see if any of the objects are already in the ListView. + /// Null objects are silently ignored. + /// + public override void AddObjects(ICollection modelObjects) { + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the added objects + ItemsAddingEventArgs args = new ItemsAddingEventArgs(modelObjects); + this.OnItemsAdding(args); + if (args.Canceled) + return; + + try { + this.BeginUpdate(); + this.VirtualListDataSource.AddObjects(args.ObjectsToAdd); + this.BuildList(); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Remove all items from this list + /// + /// This method can safely be called from background threads. + public override void ClearObjects() { + if (this.InvokeRequired) + this.Invoke(new MethodInvoker(this.ClearObjects)); + else { + this.CheckStateMap.Clear(); + this.SetObjects(new ArrayList()); + } + } + + /// + /// Scroll the listview so that the given group is at the top. + /// + /// The index of the group to be revealed + /// + /// If the group is already visible, the list will still be scrolled to move + /// the group to the top, if that is possible. + /// + /// This only works when the list is showing groups (obviously). + /// + public virtual void EnsureNthGroupVisible(int groupIndex) { + if (!this.ShowGroups) + return; + + if (groupIndex <= 0 || groupIndex >= this.OLVGroups.Count) { + // There is no easy way to scroll back to the beginning of the list + int delta = 0 - NativeMethods.GetScrollPosition(this, false); + NativeMethods.Scroll(this, 0, delta); + } else { + // Find the display rectangle of the last item in the previous group + OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; + int lastItemInGroup = this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1); + Rectangle r = this.GetItemRect(lastItemInGroup); + + // Scroll so that the last item of the previous group is just out of sight, + // which will make the desired group header visible. + int delta = r.Y + r.Height / 2; + NativeMethods.Scroll(this, 0, delta); + } + } + + /// + /// Update the rows that are showing the given objects + /// + /// This method does not resort the items. + public override void RefreshObjects(IList modelObjects) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.RefreshObjects(modelObjects); }); + return; + } + + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + try { + this.BeginUpdate(); + this.ClearCachedInfo(); + foreach (object modelObject in modelObjects) { + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index >= 0) { + this.VirtualListDataSource.UpdateObject(index, modelObject); + this.RedrawItems(index, index, true); + } + } + } + finally { + this.EndUpdate(); + } + } + + /// + /// Update the rows that are selected + /// + /// This method does not resort or regroup the view. + public override void RefreshSelectedObjects() { + foreach (int index in this.SelectedIndices) + this.RedrawItems(index, index, true); + } + + /// + /// Remove all of the given objects from the control + /// + /// Collection of objects to be removed + /// + /// Nulls and model objects that are not in the ListView are silently ignored. + /// Due to problems in the underlying ListView, if you remove all the objects from + /// the control using this method and the list scroll vertically when you do so, + /// then when you subsequenially add more objects to the control, + /// the vertical scroll bar will become confused and the control will draw one or more + /// blank lines at the top of the list. + /// + public override void RemoveObjects(ICollection modelObjects) { + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the removed objects + ItemsRemovingEventArgs args = new ItemsRemovingEventArgs(modelObjects); + this.OnItemsRemoving(args); + if (args.Canceled) + return; + + try { + this.BeginUpdate(); + this.VirtualListDataSource.RemoveObjects(args.ObjectsToRemove); + this.BuildList(); + this.UnsubscribeNotifications(args.ObjectsToRemove); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Select the row that is displaying the given model object. All other rows are deselected. + /// + /// Model object to select + /// Should the object be focused as well? + public override void SelectObject(object modelObject, bool setFocus) { + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + // Check that the object is in the list (plus not all data sources can locate objects) + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index < 0 || index >= this.VirtualListSize) + return; + + // If the given model is already selected, don't do anything else (prevents an flicker) + if (this.SelectedIndices.Count == 1 && this.SelectedIndices[0] == index) + return; + + // Finally, select the row + this.SelectedIndices.Clear(); + this.SelectedIndices.Add(index); + if (setFocus) + this.SelectedItem.Focused = true; + } + + /// + /// Select the rows that is displaying any of the given model object. All other rows are deselected. + /// + /// A collection of model objects + /// This method has O(n) performance where n is the number of model objects passed. + /// Do not use this to select all the rows in the list -- use SelectAll() for that. + public override void SelectObjects(IList modelObjects) { + // Without a data source, we can't do this. + if (this.VirtualListDataSource == null) + return; + + this.SelectedIndices.Clear(); + + if (modelObjects == null) + return; + + foreach (object modelObject in modelObjects) { + int index = this.VirtualListDataSource.GetObjectIndex(modelObject); + if (index >= 0 && index < this.VirtualListSize) + this.SelectedIndices.Add(index); + } + } + + /// + /// Set the collection of objects that this control will show. + /// + /// + /// Should the state of the list be preserved as far as is possible. + public override void SetObjects(IEnumerable collection, bool preserveState) { + if (this.InvokeRequired) { + this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); }); + return; + } + + if (this.VirtualListDataSource == null) + return; + + // Give the world a chance to cancel or change the assigned collection + ItemsChangingEventArgs args = new ItemsChangingEventArgs(null, collection); + this.OnItemsChanging(args); + if (args.Canceled) + return; + + this.BeginUpdate(); + try { + this.VirtualListDataSource.SetObjects(args.NewObjects); + this.BuildList(); + this.UpdateNotificationSubscriptions(args.NewObjects); + } + finally { + this.EndUpdate(); + } + } + + #endregion + + #region Check boxes +// +// /// +// /// Check all rows +// /// +// /// The performance of this method is O(n) where n is the number of rows in the control. +// public override void CheckAll() +// { +// if (!this.CheckBoxes) +// return; +// +// Stopwatch sw = Stopwatch.StartNew(); +// +// this.BeginUpdate(); +// +// foreach (Object x in this.Objects) +// this.SetObjectCheckedness(x, CheckState.Checked); +// +// this.EndUpdate(); +// +// Debug.WriteLine(String.Format("PERF - CheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); +// +// } +// +// /// +// /// Uncheck all rows +// /// +// /// The performance of this method is O(n) where n is the number of rows in the control. +// public override void UncheckAll() +// { +// if (!this.CheckBoxes) +// return; +// +// Stopwatch sw = Stopwatch.StartNew(); +// +// this.BeginUpdate(); +// +// foreach (Object x in this.Objects) +// this.SetObjectCheckedness(x, CheckState.Unchecked); +// +// this.EndUpdate(); +// +// Debug.WriteLine(String.Format("PERF - UncheckAll() on {2} objects took {0}ms / {1} ticks", sw.ElapsedMilliseconds, sw.ElapsedTicks, this.GetItemCount())); +// } + + /// + /// Get the checkedness of an object from the model. Returning null means the + /// model does know and the value from the control will be used. + /// + /// + /// + protected override CheckState? GetCheckState(object modelObject) + { + if (this.CheckStateGetter != null) + return base.GetCheckState(modelObject); + + CheckState state; + if (modelObject != null && this.CheckStateMap.TryGetValue(modelObject, out state)) + return state; + return CheckState.Unchecked; + } + + #endregion + + #region Implementation + + /// + /// Rebuild the list with its current contents. + /// + /// + /// Invalidate any cached information when we rebuild the list. + /// + public override void BuildList(bool shouldPreserveSelection) { + this.UpdateVirtualListSize(); + this.ClearCachedInfo(); + if (this.ShowGroups) + this.BuildGroups(); + else + this.Sort(); + this.Invalidate(); + } + + /// + /// Clear any cached info this list may have been using + /// + public override void ClearCachedInfo() { + this.lastRetrieveVirtualItemIndex = -1; + } + + /// + /// Do the work of creating groups for this control + /// + /// + protected override void CreateGroups(IEnumerable groups) { + + // In a virtual list, we cannot touch the Groups property. + // It was obviously not written for virtual list and often throws exceptions. + + NativeMethods.ClearGroups(this); + + this.EnableVirtualGroups(); + + foreach (OLVGroup group in groups) { + System.Diagnostics.Debug.Assert(group.Items.Count == 0, "Groups in virtual lists cannot set Items. Use VirtualItemCount instead."); + System.Diagnostics.Debug.Assert(group.VirtualItemCount > 0, "VirtualItemCount must be greater than 0."); + + group.InsertGroupNewStyle(this); + } + } + + /// + /// Do the plumbing to disable groups on a virtual list + /// + protected void DisableVirtualGroups() { + NativeMethods.ClearGroups(this); + //System.Diagnostics.Debug.WriteLine(err); + + const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; + IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 0, 0); + //System.Diagnostics.Debug.WriteLine(x); + + const int LVM_SETOWNERDATACALLBACK = 0x10BB; + IntPtr x2 = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, 0, 0); + //System.Diagnostics.Debug.WriteLine(x2); + } + + /// + /// Do the plumbing to enable groups on a virtual list + /// + protected void EnableVirtualGroups() { + + // We need to implement the IOwnerDataCallback interface + if (this.ownerDataCallbackImpl == null) + this.ownerDataCallbackImpl = new OwnerDataCallbackImpl(this); + + const int LVM_SETOWNERDATACALLBACK = 0x10BB; + IntPtr ptr = Marshal.GetComInterfaceForObject(ownerDataCallbackImpl, typeof(IOwnerDataCallback)); + IntPtr x = NativeMethods.SendMessage(this.Handle, LVM_SETOWNERDATACALLBACK, ptr, 0); + //System.Diagnostics.Debug.WriteLine(x); + Marshal.Release(ptr); + + const int LVM_ENABLEGROUPVIEW = 0x1000 + 157; + x = NativeMethods.SendMessage(this.Handle, LVM_ENABLEGROUPVIEW, 1, 0); + //System.Diagnostics.Debug.WriteLine(x); + } + private OwnerDataCallbackImpl ownerDataCallbackImpl; + + /// + /// Return the position of the given itemIndex in the list as it currently shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public override int GetDisplayOrderOfItemIndex(int itemIndex) { + if (!this.ShowGroups) + return itemIndex; + + int groupIndex = this.GroupingStrategy.GetGroup(itemIndex); + int displayIndex = 0; + for (int i = 0; i < groupIndex - 1; i++) + displayIndex += this.OLVGroups[i].VirtualItemCount; + displayIndex += this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemIndex); + + return displayIndex; + } + + /// + /// Return the last item in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + public override OLVListItem GetLastItemInDisplayOrder() { + if (!this.ShowGroups) + return base.GetLastItemInDisplayOrder(); + + if (this.OLVGroups.Count > 0) { + OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; + if (lastGroup.VirtualItemCount > 0) + return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); + } + + return null; + } + + /// + /// Return the n'th item (0-based) in the order they are shown to the user. + /// If the control is not grouped, the display order is the same as the + /// sorted list order. But if the list is grouped, the display order is different. + /// + /// + /// + public override OLVListItem GetNthItemInDisplayOrder(int n) { + if (!this.ShowGroups || this.OLVGroups == null || this.OLVGroups.Count == 0) + return this.GetItem(n); + + foreach (OLVGroup group in this.OLVGroups) { + if (n < group.VirtualItemCount) + return this.GetItem(this.GroupingStrategy.GetGroupMember(group, n)); + + n -= group.VirtualItemCount; + } + + return null; + } + + /// + /// Return the ListViewItem that appears immediately after the given item. + /// If the given item is null, the first item in the list will be returned. + /// Return null if the given item is the last item. + /// + /// The item that is before the item that is returned, or null + /// A OLVListItem + public override OLVListItem GetNextItem(OLVListItem itemToFind) { + if (!this.ShowGroups) + return base.GetNextItem(itemToFind); + + // Sanity + if (this.OLVGroups == null || this.OLVGroups.Count == 0) + return null; + + // If the given item is null, return the first member of the first group + if (itemToFind == null) { + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[0], 0)); + } + + // Find where this item occurs (which group and where in that group) + int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); + int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); + + // If it's not the last member, just return the next member + if (indexWithinGroup < this.OLVGroups[groupIndex].VirtualItemCount - 1) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup + 1)); + + // The item is the last member of its group. Return the first member of the next group + // (unless there isn't a next group) + if (groupIndex < this.OLVGroups.Count - 1) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex + 1], 0)); + + return null; + } + + /// + /// Return the ListViewItem that appears immediately before the given item. + /// If the given item is null, the last item in the list will be returned. + /// Return null if the given item is the first item. + /// + /// The item that is before the item that is returned + /// A ListViewItem + public override OLVListItem GetPreviousItem(OLVListItem itemToFind) { + if (!this.ShowGroups) + return base.GetPreviousItem(itemToFind); + + // Sanity + if (this.OLVGroups == null || this.OLVGroups.Count == 0) + return null; + + // If the given items is null, return the last member of the last group + if (itemToFind == null) { + OLVGroup lastGroup = this.OLVGroups[this.OLVGroups.Count - 1]; + return this.GetItem(this.GroupingStrategy.GetGroupMember(lastGroup, lastGroup.VirtualItemCount - 1)); + } + + // Find where this item occurs (which group and where in that group) + int groupIndex = this.GroupingStrategy.GetGroup(itemToFind.Index); + int indexWithinGroup = this.GroupingStrategy.GetIndexWithinGroup(this.OLVGroups[groupIndex], itemToFind.Index); + + // If it's not the first member of the group, just return the previous member + if (indexWithinGroup > 0) + return this.GetItem(this.GroupingStrategy.GetGroupMember(this.OLVGroups[groupIndex], indexWithinGroup - 1)); + + // The item is the first member of its group. Return the last member of the previous group + // (if there is one) + if (groupIndex > 0) { + OLVGroup previousGroup = this.OLVGroups[groupIndex - 1]; + return this.GetItem(this.GroupingStrategy.GetGroupMember(previousGroup, previousGroup.VirtualItemCount - 1)); + } + + return null; + } + + /// + /// Make a list of groups that should be shown according to the given parameters + /// + /// + /// + protected override IList MakeGroups(GroupingParameters parms) { + if (this.GroupingStrategy == null) + return new List(); + else + return this.GroupingStrategy.GetGroups(parms); + } + + /// + /// Create a OLVListItem for given row index + /// + /// The index of the row that is needed + /// An OLVListItem + public virtual OLVListItem MakeListViewItem(int itemIndex) { + OLVListItem olvi = new OLVListItem(this.GetModelObject(itemIndex)); + this.FillInValues(olvi, olvi.RowObject); + + this.PostProcessOneRow(itemIndex, this.GetDisplayOrderOfItemIndex(itemIndex), olvi); + + if (this.HotRowIndex == itemIndex) + this.UpdateHotRow(olvi); + + return olvi; + } + + /// + /// On virtual lists, this cannot work. + /// + protected override void PostProcessRows() { + } + + /// + /// Record the change of checkstate for the given object in the model. + /// This does not update the UI -- only the model + /// + /// + /// + /// The check state that was recorded and that should be used to update + /// the control. + protected override CheckState PutCheckState(object modelObject, CheckState state) { + state = base.PutCheckState(modelObject, state); + this.CheckStateMap[modelObject] = state; + return state; + } + + /// + /// Refresh the given item in the list + /// + /// The item to refresh + public override void RefreshItem(OLVListItem olvi) { + this.ClearCachedInfo(); + this.RedrawItems(olvi.Index, olvi.Index, true); + } + + /// + /// Change the size of the list + /// + /// + protected virtual void SetVirtualListSize(int newSize) { + if (newSize < 0 || this.VirtualListSize == newSize) + return; + + int oldSize = this.VirtualListSize; + + this.ClearCachedInfo(); + + // There is a bug in .NET when a virtual ListView is cleared + // (i.e. VirtuaListSize set to 0) AND it is scrolled vertically: the scroll position + // is wrong when the list is next populated. To avoid this, before + // clearing a virtual list, we make sure the list is scrolled to the top. + // [6 weeks later] Damn this is a pain! There are cases where this can also throw exceptions! + try { + if (newSize == 0 && this.TopItemIndex > 0) + this.TopItemIndex = 0; + } + catch (Exception) { + // Ignore any failures + } + + // In strange cases, this can throw the exceptions too. The best we can do is ignore them :( + try { + this.VirtualListSize = newSize; + } + catch (ArgumentOutOfRangeException) { + // pass + } + catch (NullReferenceException) { + // pass + } + + // Tell the world that the size of the list has changed + this.OnItemsChanged(new ItemsChangedEventArgs(oldSize, this.VirtualListSize)); + } + + /// + /// Take ownership of the 'objects' collection. This separates our collection from the source. + /// + /// + /// + /// This method + /// separates the 'objects' instance variable from its source, so that any AddObject/RemoveObject + /// calls will modify our collection and not the original colleciton. + /// + /// + /// VirtualObjectListViews always own their collections, so this is a no-op. + /// + /// + protected override void TakeOwnershipOfObjects() { + } + + /// + /// Change the state of the control to reflect changes in filtering + /// + protected override void UpdateFiltering() { + IFilterableDataSource filterable = this.VirtualListDataSource as IFilterableDataSource; + if (filterable == null) + return; + + this.BeginUpdate(); + try { + int originalSize = this.VirtualListSize; + filterable.ApplyFilters(this.ModelFilter, this.ListFilter); + this.BuildList(); + + //// If the filtering actually did something, rebuild the groups if they are being shown + //if (originalSize != this.VirtualListSize && this.ShowGroups) + // this.BuildGroups(); + } + finally { + this.EndUpdate(); + } + } + + /// + /// Change the size of the virtual list so that it matches its data source + /// + public virtual void UpdateVirtualListSize() { + if (this.VirtualListDataSource != null) + this.SetVirtualListSize(this.VirtualListDataSource.GetObjectCount()); + } + + #endregion + + #region Event handlers + + /// + /// Handle the CacheVirtualItems event + /// + /// + /// + protected virtual void HandleCacheVirtualItems(object sender, CacheVirtualItemsEventArgs e) { + if (this.VirtualListDataSource != null) + this.VirtualListDataSource.PrepareCache(e.StartIndex, e.EndIndex); + } + + /// + /// Handle a RetrieveVirtualItem + /// + /// + /// + protected virtual void HandleRetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e) { + // .NET 2.0 seems to generate a lot of these events. Before drawing *each* sub-item, + // this event is triggered 4-8 times for the same index. So we save lots of CPU time + // by caching the last result. + //System.Diagnostics.Debug.WriteLine(String.Format("HandleRetrieveVirtualItem({0})", e.ItemIndex)); + + if (this.lastRetrieveVirtualItemIndex != e.ItemIndex) { + this.lastRetrieveVirtualItemIndex = e.ItemIndex; + this.lastRetrieveVirtualItem = this.MakeListViewItem(e.ItemIndex); + } + e.Item = this.lastRetrieveVirtualItem; + } + + /// + /// Handle the SearchForVirtualList event, which is called when the user types into a virtual list + /// + /// + /// + protected virtual void HandleSearchForVirtualItem(object sender, SearchForVirtualItemEventArgs e) { + // The event has e.IsPrefixSearch, but as far as I can tell, this is always false (maybe that's different under Vista) + // So we ignore IsPrefixSearch and IsTextSearch and always to a case insensitve prefix match. + + // We can't do anything if we don't have a data source + if (this.VirtualListDataSource == null) + return; + + // Where should we start searching? If the last row is focused, the SearchForVirtualItemEvent starts searching + // from the next row, which is actually an invalidate index -- so we make sure we never go past the last object. + int start = Math.Min(e.StartIndex, this.VirtualListDataSource.GetObjectCount() - 1); + + // Give the world a chance to fiddle with or completely avoid the searching process + BeforeSearchingEventArgs args = new BeforeSearchingEventArgs(e.Text, start); + this.OnBeforeSearching(args); + if (args.Canceled) + return; + + // Do the search + int i = this.FindMatchingRow(args.StringToFind, args.StartSearchFrom, e.Direction); + + // Tell the world that a search has occurred + AfterSearchingEventArgs args2 = new AfterSearchingEventArgs(args.StringToFind, i); + this.OnAfterSearching(args2); + + // If we found a match, tell the event + if (i != -1) + e.Index = i; + } + + /// + /// Find the first row in the given range of rows that prefix matches the string value of the given column. + /// + /// + /// + /// + /// + /// The index of the matched row, or -1 + protected override int FindMatchInRange(string text, int first, int last, OLVColumn column) { + return this.VirtualListDataSource.SearchText(text, first, last, column); + } + + #endregion + + #region Variable declaractions + + private OLVListItem lastRetrieveVirtualItem; + private int lastRetrieveVirtualItemIndex = -1; + + #endregion + } +} diff --git a/ObjectListView/olv-keyfile.snk b/ObjectListView/olv-keyfile.snk new file mode 100644 index 0000000..2658a0a Binary files /dev/null and b/ObjectListView/olv-keyfile.snk differ diff --git a/WindowsFormsApplication1/Program.cs b/WindowsFormsApplication1/Program.cs new file mode 100644 index 0000000..f369989 --- /dev/null +++ b/WindowsFormsApplication1/Program.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace WindowsFormsApplication1 +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Form1()); + } + } +} diff --git a/WindowsFormsApplication1/Properties/AssemblyInfo.cs b/WindowsFormsApplication1/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d19a6cd --- /dev/null +++ b/WindowsFormsApplication1/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WindowsFormsApplication1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsFormsApplication1")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a18429c0-d528-47b3-9857-9c06df054a03")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/WindowsFormsApplication1/Properties/Resources.Designer.cs b/WindowsFormsApplication1/Properties/Resources.Designer.cs new file mode 100644 index 0000000..f687b8d --- /dev/null +++ b/WindowsFormsApplication1/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WindowsFormsApplication1.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WindowsFormsApplication1.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/WindowsFormsApplication1/Properties/Resources.resx b/WindowsFormsApplication1/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/WindowsFormsApplication1/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/WindowsFormsApplication1/Properties/Settings.Designer.cs b/WindowsFormsApplication1/Properties/Settings.Designer.cs new file mode 100644 index 0000000..e85fc55 --- /dev/null +++ b/WindowsFormsApplication1/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WindowsFormsApplication1.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/WindowsFormsApplication1/Properties/Settings.settings b/WindowsFormsApplication1/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/WindowsFormsApplication1/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/WindowsFormsApplication1/WindowsFormsApplication1.csproj b/WindowsFormsApplication1/WindowsFormsApplication1.csproj new file mode 100644 index 0000000..cc9a77d --- /dev/null +++ b/WindowsFormsApplication1/WindowsFormsApplication1.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {A18429C0-D528-47B3-9857-9C06DF054A03} + WinExe + Properties + WindowsFormsApplication1 + WindowsFormsApplication1 + v4.0 + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + \ No newline at end of file