diff --git a/.gitignore b/.gitignore
index 1b71e0c..3a788d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,30 @@
/PropertyHandler/x64/Release
/x64/Release
/Setup/obj/x64/Release
+/TestDriver/TestDriver.suo
+/AssociationManager/bin/Debug
+/CommandLine/Debug
+/CommandLineAssociationManager/bin/Debug
+/ContextMenuHandler/Debug
+/Debug
+/PropertyHandler/Debug
+/Setup/bin
+/Setup/obj/x86/Debug
+/Setup/msi/Debug
+/AssociationManager/bin/Release
+/CommandLine/Release
+/CommandLineAssociationManager/bin/Release
+/ContextMenuHandler/Release
+/PropertyHandler/Release
+/Release
+/Setup/msi/Release
+/Setup/obj/x86/Release
+/CommandLine/FileMeta.aps
+/ContextMenuHandler/ContextMenuHandler.aps
+/PropertyHandler/FileMetaPropertyHandler.aps
+/TestDriverAssoc/.vs/TestDriverAssoc/v15
+/TestSandbox
+/TestDriver/bin/Debug
+/TestDriverAssoc/bin/Debug
+/TestSandbox32
+/CommandLineAssociationManager/CommandLineAssociationManager.csproj.user
diff --git a/AssociationManager/Extension.cs b/AssociationManager/Extension.cs
index b458d6f..26dcd1d 100644
--- a/AssociationManager/Extension.cs
+++ b/AssociationManager/Extension.cs
@@ -19,16 +19,20 @@ public enum HandlerState
Ours,
Foreign,
Chained,
+ ProfileOnly,
}
public class Extension : INotifyPropertyChanged
{
const string OurPropertyHandlerTitle = "File Meta Property Handler";
const string OurPropertyHandlerPrefix = "File Meta + ";
+ const string OurProfilePrefix = "Profile + ";
const string OurPropertyHandlerGuid64 = "{D06391EE-2FEB-419B-9667-AD160D0849F3}";
const string OurPropertyHandlerGuid32 = "{60211757-EF87-465e-B6C1-B37CF98295F9}";
const string OurContextHandlerGuid64 = "{28D14D00-2D80-4956-9657-9D50C8BB47A5}";
const string OurContextHandlerGuid32 = "{DA38301B-BE91-4397-B2C8-E27A0BD80CC5}";
+ const string OurExportContextHandlerGuid64 = "{DE4C4CAF-C564-4EEA-9FF7-C46FB8023818}";
+ const string OurExportContextHandlerGuid32 = "{5A677F18-527D-42B3-BAA0-9785D3A8256F}";
const string FullDetailsValueName = "FullDetails";
const string PreviewDetailsValueName = "PreviewDetails";
@@ -67,6 +71,9 @@ public FontWeight Weight
case HandlerState.Chained:
return FontWeights.ExtraBold;
+ case HandlerState.ProfileOnly:
+ return FontWeights.Bold;
+
case HandlerState.Foreign:
case HandlerState.None:
default:
@@ -91,6 +98,9 @@ public string PropertyHandlerDisplay
case HandlerState.Chained:
return OurPropertyHandlerPrefix + (PropertyHandlerTitle != null ? PropertyHandlerTitle : PropertyHandlerGuid);
+ case HandlerState.ProfileOnly:
+ return OurProfilePrefix + (PropertyHandlerTitle != null ? PropertyHandlerTitle : PropertyHandlerGuid);
+
case HandlerState.None:
default:
return LocalizedMessages.PropertyHandlerNone;
@@ -111,9 +121,11 @@ public static bool IsElevated
#if x64
private static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid64; } }
private static string OurContextHandlerGuid { get { return OurContextHandlerGuid64; } }
+ private static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid64; } }
#elif x86
private static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid32; } }
private static string OurContextHandlerGuid { get { return OurContextHandlerGuid32; } }
+ private static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid32; } }
#endif
public static bool IsOurPropertyHandlerRegistered
@@ -151,21 +163,26 @@ public void RecordPropertyHandler(string handlerGuid, string handlerChainedGuid)
if (handlerGuid == OurPropertyHandlerGuid)
{
if (handlerChainedGuid != null)
+ {
RecordPropertyHandler(HandlerState.Chained, handlerChainedGuid, GetHandlerTitle(handlerChainedGuid));
+ Profile = GetCurrentProfileIfKnown();
+ }
else
+ {
RecordPropertyHandler(HandlerState.Ours, null, null);
+ Profile = GetCurrentProfileIfKnown();
+ }
}
else if (handlerGuid != null)
- RecordPropertyHandler(HandlerState.Foreign, handlerGuid, GetHandlerTitle(handlerGuid));
+ {
+ Profile = GetCurrentProfileIfKnown(true);
+ RecordPropertyHandler(Profile == null ? HandlerState.Foreign : HandlerState.ProfileOnly,
+ handlerGuid, GetHandlerTitle(handlerGuid));
+ }
else
RecordPropertyHandler(HandlerState.None, null, null);
}
-
- public void IdentifyCurrentProfile()
- {
- Profile = GetCurrentProfileIfKnown();
- }
-
+
public Profile GetDefaultCustomProfile()
{
var p = new Profile();
@@ -226,14 +243,20 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP
}
// Find the key for the extension in HKEY_CLASSES_ROOT
- using (RegistryKey target = GetHKCRProfileKey(true))
+ if (PropertyHandlerState == HandlerState.Foreign)
{
// We used to place entries on this key, but no longer do, because such keys can be shared,
// and we only want to affect a specific extension
// We still have to hide any existing entries, because otherwise they would take priority, but fortunately they do not often occur
// If there are entries, and we are merging, we read them into the new profile, in case this is the only place they occur
- if (PropertyHandlerState == HandlerState.Foreign)
- GetAndHidePreExistingProgidRegistryEntries(target, createMergedProfile ? profile : null);
+ bool exist;
+ // Use read-only access to test for existence
+ using (RegistryKey target = GetHKCRProfileKey(false))
+ { exist = PreExistingProgidRegistryEntries(target); }
+
+ if (exist)
+ using (RegistryKey target = GetHKCRProfileKey(true))
+ { GetAndHidePreExistingProgidRegistryEntries(target, createMergedProfile ? profile : null); }
}
// Now we only update the extension specific area
@@ -252,15 +275,53 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP
GetAndHidePreExistingProgidRegistryEntries(target, null);
}
- SetupProgidRegistryEntries(target, profile);
+ SetupProfileDetailEntries(target, profile);
+ SetupContextMenuRegistryEntries(target);
}
if (PropertyHandlerState == HandlerState.Foreign)
{
// Write the updated custom profile information back to the store
State.StoreUpdatedProfile(profile);
- }
+ }
+
+ AddPropertyHandler();
+
+ // Test to see if our handler is actually being used, or if this is one of those cases where
+ // Windows ignores the registry setting when providing the property handler
+ if (PropertyHandlerState == HandlerState.Chained &&
+ PropertyHandlerTest.WindowsIgnoresOurPropertyHandler(Name))
+ {
+ // Rollback our property handler
+ RemovePropertyHandler();
+
+ // Switch to the export only context menu
+ SetupExportContextMenu();
+ // Update the property handler state
+ this.RecordPropertyHandler(HandlerState.ProfileOnly, PropertyHandlerGuid, PropertyHandlerTitle);
+ }
+
+ this.Profile = profile;
+
+ State.HasChanged = true;
+ }
+
+ // Temporary code to support MainWindow.CheckForBlockedExtensions()
+ //public bool IsPropertyHandlerBlocked()
+ //{
+ // bool result = false;
+ // if (PropertyHandlerState == HandlerState.Foreign)
+ // {
+ // AddPropertyHandler();
+ // result = PropertyHandlerTest.WindowsIgnoresOurPropertyHandler(Name);
+ // RemovePropertyHandler();
+ // }
+ // return result;
+ //}
+
+ private void AddPropertyHandler()
+ {
#if x64
// On 64-bit machines, set up the 32-bit property handler, so that 32-bit applications can also access our properties
using (RegistryKey handlers = RegistryExtensions.OpenBaseKey(RegistryHive.LocalMachine, RegistryExtensions.RegistryHiveType.X86).
@@ -270,7 +331,7 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP
{
if (PropertyHandlerState == HandlerState.None)
{
- var temp = handler.GetValue(null);
+ var temp = handler.GetValue(null);
// In the case where there is no 64-bit handler, but there is a 32-bit handler, leave it alone
if (temp == null)
handler.SetValue(null, OurPropertyHandlerGuid32);
@@ -284,7 +345,7 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP
}
}
}
-#endif
+#endif
// Now, add the main handler extension key, which is 32- or 64-bit, depending on how we were built
// The 32-bit and 64-bit values of these are separate and isolated on 64-bit Windows,
// the 32-bit value being under SOFTWARE\Wow6432Node. Thus a 64-bit manager is needed to set up a 64-bit handler
@@ -305,10 +366,6 @@ public void SetupHandlerForExtension(Profile selectedProfile, bool createMergedP
}
}
}
-
- this.Profile = profile;
-
- State.HasChanged = true;
}
// Update the registry settings for an extension when the Full details or Preview details in a profile are changed
@@ -322,7 +379,9 @@ public void UpdateProfileSettingsForExtension(Profile profile)
ErrorCode = WindowsErrorCodes.ERROR_ACCESS_DENIED
};
- if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained)
+ if (PropertyHandlerState != HandlerState.Ours &&
+ PropertyHandlerState != HandlerState.Chained &&
+ PropertyHandlerState != HandlerState.ProfileOnly)
return;
using (RegistryKey target = GetSystemFileAssociationsProfileKey(true))
@@ -352,6 +411,16 @@ private bool GetExistingProgidRegistryEntries(RegistryKey target, Profile profil
return val != null;
}
+ private bool PreExistingProgidRegistryEntries(RegistryKey target)
+ {
+ if (target == null)
+ return false;
+
+ return (target.GetValue(FullDetailsValueName) != null ||
+ target.GetValue(InfoTipValueName) != null ||
+ target.GetValue(PreviewDetailsValueName) != null);
+ }
+
// If the caller does not need the existing values, it should pass null for the profile
private void GetAndHidePreExistingProgidRegistryEntries(RegistryKey target, Profile profile)
{
@@ -387,10 +456,8 @@ private void GetAndHidePreExistingProgidRegistryEntries(RegistryKey target, Prof
}
}
- private void SetupProgidRegistryEntries(RegistryKey target, Profile profile)
+ private void SetupContextMenuRegistryEntries(RegistryKey target, bool export = false)
{
- SetupProfileDetailEntries(target, profile);
-
// Set up the eontext handler, if registered
if (IsOurContextHandlerRegistered)
{
@@ -400,7 +467,7 @@ private void SetupProgidRegistryEntries(RegistryKey target, Profile profile)
{
using (RegistryKey keyHandler = keyCMH.CreateSubKey(ContextHandlerKeyName))
{
- keyHandler.SetValue(null, OurContextHandlerGuid);
+ keyHandler.SetValue(null, export ? OurExportContextHandlerGuid : OurContextHandlerGuid);
}
}
}
@@ -419,14 +486,13 @@ private void SetupProfileDetailEntries(RegistryKey target, Profile profile)
target.SetValue(FileMetaCustomProfileValueName, profile.Name);
}
- private Profile GetCurrentProfileIfKnown()
+ private Profile GetCurrentProfileIfKnown(bool keyOnly = false)
{
- if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained)
- return null;
-
// Find the key for the extension
string pd;
+ string opd;
string cp;
+
using (RegistryKey target = GetSystemFileAssociationsProfileKey(false))
{
if (target == null)
@@ -434,6 +500,7 @@ private Profile GetCurrentProfileIfKnown()
// Try to identify the profile
pd = (string)target.GetValue(PreviewDetailsValueName);
+ opd = (string)target.GetValue(OldPreviewDetailsValueName);
cp = (string)target.GetValue(FileMetaCustomProfileValueName);
}
@@ -441,8 +508,8 @@ private Profile GetCurrentProfileIfKnown()
if (cp != null)
return State.CustomProfiles.Where(p => p.Name == cp).FirstOrDefault();
- // Otherwise, tried to match the preview details against one of the built-in values
- else if (pd != null)
+ // Otherwise, try to match the preview details against one of the built-in values
+ else if ((!keyOnly || opd != null) && pd != null)
return State.BuiltInProfiles.Where(p => p.PreviewDetailsString == pd).FirstOrDefault();
return null;
@@ -477,7 +544,7 @@ public bool IsRefreshRequired()
return true;
}
}
-#endif
+#endif
return false;
}
@@ -491,17 +558,42 @@ public void RemoveHandlerFromExtension()
ErrorCode = WindowsErrorCodes.ERROR_ACCESS_DENIED
};
- if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained)
+ if (PropertyHandlerState != HandlerState.Ours && PropertyHandlerState != HandlerState.Chained &&
+ PropertyHandlerState != HandlerState.ProfileOnly)
return;
- // Now find the key for the extension in HKEY_CLASSES_ROOT
- using (RegistryKey target = GetHKCRProfileKey(true))
+ // Now find the key for the extension in HKEY_CLASSES_ROOT
+ bool exist;
+ // Use read-only access to test for existence
+ using (RegistryKey target = GetHKCRProfileKey(false))
{
- // Tolerate the case where the extension has been removed since we set up a handler for it
- // We still do this even though no longer write entries here so as to be sure to clean up after earlier versions,
- // and to restore pre-existing entries if we had to hide them
- if (target != null)
- RemoveProgidRegistryEntries(target);
+ exist = HiddenProgidRegistryEntries(target);
+ }
+
+ // Cleanup required for hidden cases, and for some old configurations
+ if (exist || PropertyHandlerState == HandlerState.Ours)
+ {
+ try
+ {
+ using (RegistryKey target = GetHKCRProfileKey(true))
+ {
+ // Tolerate the case where the extension has been removed since we set up a handler for it
+ // We still do this even though no longer write entries here so as to be sure to clean up after earlier versions,
+ // and to restore pre-existing entries if we had to hide them
+ if (target != null)
+ {
+ RemoveProfileDetailRegistryEntries(target);
+ RemoveContextMenuRegistryEntries(target);
+ }
+ }
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ // Tolerate actors problems if it is our handler. We need to try and get write access
+ // to clean up settings from older versions, but this fails some extensions like .chm
+ if (PropertyHandlerState != HandlerState.Ours)
+ throw ex;
+ }
}
// Now go after the settings in SystemFileAssociations
@@ -509,9 +601,34 @@ public void RemoveHandlerFromExtension()
using (RegistryKey target = GetSystemFileAssociationsProfileKey(true))
{
if (target != null)
- RemoveProgidRegistryEntries(target);
+ {
+ RemoveProfileDetailRegistryEntries(target);
+ RemoveContextMenuRegistryEntries(target);
+ }
}
+ if (PropertyHandlerState != HandlerState.ProfileOnly)
+ RemovePropertyHandler();
+ else
+ this.RecordPropertyHandler(HandlerState.Foreign, PropertyHandlerGuid, PropertyHandlerTitle);
+
+ this.Profile = null;
+ State.HasChanged = true;
+ }
+
+ private void SetupExportContextMenu()
+ {
+ using (RegistryKey target = GetSystemFileAssociationsProfileKey(true))
+ {
+ if (target != null)
+ {
+ SetupContextMenuRegistryEntries(target, true);
+ }
+ }
+ }
+
+ private void RemovePropertyHandler()
+ {
#if x64
// On 64-bit machines, remove the 32-bit property handler, as we set up both
using (RegistryKey handlers = RegistryExtensions.OpenBaseKey(RegistryHive.LocalMachine, RegistryExtensions.RegistryHiveType.X86).
@@ -540,7 +657,7 @@ public void RemoveHandlerFromExtension()
if (delete)
handlers.DeleteSubKey(Name);
}
-#endif
+#endif
// Now, remove the main handler extension key, which is 32- or 64-bit, depending on how we were built
// The 32-bit and 64-bit values of these are separate and isolated on 64-bit Windows,
// the 32-bit value being under SOFTWARE\Wow6432Node. Thus a 64-bit manager is needed to set up a 64-bit handler
@@ -550,23 +667,20 @@ public void RemoveHandlerFromExtension()
{
handlers.DeleteSubKey(Name);
this.RecordPropertyHandler(HandlerState.None, null, null);
- this.Profile = null;
}
- else // Chained
+ else if (PropertyHandlerState == HandlerState.Chained)
{
using (RegistryKey handler = handlers.OpenSubKey(Name, true))
{
handler.SetValue(null, PropertyHandlerGuid);
handler.DeleteValue(ChainedValueName);
this.RecordPropertyHandler(HandlerState.Foreign, PropertyHandlerGuid, PropertyHandlerTitle);
- this.Profile = null;
}
}
}
- State.HasChanged = true;
}
- private void RemoveProgidRegistryEntries(RegistryKey target)
+ private void RemoveProfileDetailRegistryEntries(RegistryKey target)
{
target.DeleteValue(FullDetailsValueName, false);
target.DeleteValue(InfoTipValueName, false);
@@ -574,7 +688,10 @@ private void RemoveProgidRegistryEntries(RegistryKey target)
target.DeleteValue(FileMetaCustomProfileValueName, false);
UnhidePreExistingProgidRegistryEntries(target);
+ }
+ private void RemoveContextMenuRegistryEntries(RegistryKey target)
+ {
// Always have a go at removing the context handler setup, even if the handler is not registered
// There might be entries lying around from when it was
using (RegistryKey keyShellEx = target.OpenSubKey(ShellExKeyName, true))
@@ -609,6 +726,16 @@ private void RemoveProgidRegistryEntries(RegistryKey target)
target.Close();
}
+ private bool HiddenProgidRegistryEntries(RegistryKey target)
+ {
+ if (target == null)
+ return false;
+
+ return (target.GetValue(OldFullDetailsValueName) != null ||
+ target.GetValue(OldInfoTipValueName) != null ||
+ target.GetValue(OldPreviewDetailsValueName) != null);
+ }
+
private void UnhidePreExistingProgidRegistryEntries(RegistryKey target)
{
if (target == null)
@@ -729,7 +856,7 @@ private string GetHandlerTitle(string handlerGuid)
return handlerTitle;
}
-
+
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(String info)
@@ -740,4 +867,5 @@ private void OnPropertyChanged(String info)
}
}
}
+
}
diff --git a/AssociationManager/FileMetaAssociationManager.csproj b/AssociationManager/FileMetaAssociationManager.csproj
index 295f048..335292a 100644
--- a/AssociationManager/FileMetaAssociationManager.csproj
+++ b/AssociationManager/FileMetaAssociationManager.csproj
@@ -111,6 +111,7 @@
+
diff --git a/AssociationManager/MainView.cs b/AssociationManager/MainView.cs
index 076a811..68a60ea 100644
--- a/AssociationManager/MainView.cs
+++ b/AssociationManager/MainView.cs
@@ -164,6 +164,10 @@ public void AddHandlers()
foreach (Extension ext in SelectedExtensions)
{
ext.SetupHandlerForExtension(SelectedProfile, true);
+
+ if (ext.PropertyHandlerState == HandlerState.ProfileOnly)
+ MessageBox.Show(string.Format(LocalizedMessages.WindowsWontExtendHandler, ext.Name),
+ LocalizedMessages.SetupHandler);
}
}
@@ -242,7 +246,9 @@ private void DeterminePossibleActions()
break;
}
}
- else if (e.PropertyHandlerState == HandlerState.Ours || e.PropertyHandlerState == HandlerState.Chained)
+ else if (e.PropertyHandlerState == HandlerState.Ours ||
+ e.PropertyHandlerState == HandlerState.Chained ||
+ e.PropertyHandlerState == HandlerState.ProfileOnly)
{
if (handlersSelected == null)
handlersSelected = HandlerSet.Ours;
diff --git a/AssociationManager/MainWindow.xaml.cs b/AssociationManager/MainWindow.xaml.cs
index 41f3220..554f046 100644
--- a/AssociationManager/MainWindow.xaml.cs
+++ b/AssociationManager/MainWindow.xaml.cs
@@ -147,8 +147,32 @@ private void profiles_Click(object sender, RoutedEventArgs e)
view.SelectedProfile = state.SelectedProfile;
}
+ // Temporary code to enumerate extensions for which Add Handler fails
+ //private void CheckForBlockedExtensions()
+ //{
+ // using (var w = new System.IO.StreamWriter(@"D:\TestProperties\Results\Extensions.csv"))
+ // {
+ // foreach (var ext in state.Extensions.Where(x => x.PropertyHandlerState == HandlerState.Foreign))
+ // {
+ // string result = "Exception";
+ // try
+ // {
+ // result = ext.IsPropertyHandlerBlocked() ? "Blocked" : "Ok";
+ // }
+ // catch (System.Exception ex)
+ // {
+ // }
+ // w.WriteLine(String.Format("{0}, {1}, {2}", ext.Name, result, ext.PropertyHandlerDisplay));
+ // }
+ // }
+ //}
+
private void restartExplorer_Click(object sender, RoutedEventArgs e)
{
+ // Temporary code to enumerate extensions for which Add Handler fails
+ //CheckForBlockedExtensions();
+ //return;
+
bool failed = false;
try
{
diff --git a/AssociationManager/PropertyHandlerTest.cs b/AssociationManager/PropertyHandlerTest.cs
new file mode 100644
index 0000000..b63ed78
--- /dev/null
+++ b/AssociationManager/PropertyHandlerTest.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace FileMetadataAssociationManager
+{
+ static class PropertyHandlerTest
+ {
+ private static Guid IPropertyStoreGuid = new Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
+
+ public static bool WindowsIgnoresOurPropertyHandler(string extension)
+ {
+ // Create a temporary file with the right extension
+ string fileFullName = Path.ChangeExtension(
+ Path.GetTempPath() + Guid.NewGuid().ToString(), extension);
+ using (var fs = File.Create(fileFullName)) { }
+
+ // Get the property store that Explorer would use
+ IPropertyStore ps;
+ bool result = true;
+ HResult hr = (HResult)SHGetPropertyStoreFromParsingName(fileFullName, IntPtr.Zero,
+ GETPROPERTYSTOREFLAGS.GPS_NO_OPLOCK | GETPROPERTYSTOREFLAGS.GPS_HANDLERPROPERTIESONLY, ref IPropertyStoreGuid, out ps);
+ if (hr == HResult.Ok)
+ {
+ // Look for the signature property value that marks the handler as ours
+ PropertyKey key;
+ PropVariant pv = new PropVariant();
+ PropertySystemNativeMethods.PSGetPropertyKeyFromName("System.Software.ProductName", out key);
+ hr = ps.GetValue(key, pv);
+ if (hr == HResult.Ok && !pv.IsNullOrEmpty)
+ {
+ if ((pv.Value as string) == "FileMetadata")
+ result = false;
+ }
+ Marshal.ReleaseComObject(ps); // This release frees up the file for deletion
+ }
+
+ File.Delete(fileFullName);
+
+ return result;
+ }
+
+ [DllImport("shell32.dll", SetLastError = true)]
+ public static extern int SHGetPropertyStoreFromParsingName(
+ [In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
+ IntPtr zeroWorks,
+ GETPROPERTYSTOREFLAGS flags,
+ ref Guid iIdPropStore,
+ [Out] out IPropertyStore propertyStore);
+ }
+
+ public enum GETPROPERTYSTOREFLAGS
+ {
+ // If no flags are specified (GPS_DEFAULT), a read-only property store is returned that includes properties for the file or item.
+ // In the case that the shell item is a file, the property store contains:
+ // 1. properties about the file from the file system
+ // 2. properties from the file itself provided by the file's property handler, unless that file is offline,
+ // see GPS_OPENSLOWITEM
+ // 3. if requested by the file's property handler and supported by the file system, properties stored in the
+ // alternate property store.
+ //
+ // Non-file shell items should return a similar read-only store
+ //
+ // Specifying other GPS_ flags modifies the store that is returned
+ GPS_DEFAULT = 0x00000000,
+ GPS_HANDLERPROPERTIESONLY = 0x00000001, // only include properties directly from the file's property handler
+ GPS_READWRITE = 0x00000002, // Writable stores will only include handler properties
+ GPS_TEMPORARY = 0x00000004, // A read/write store that only holds properties for the lifetime of the IShellItem object
+ GPS_FASTPROPERTIESONLY = 0x00000008, // do not include any properties from the file's property handler (because the file's property handler will hit the disk)
+ GPS_OPENSLOWITEM = 0x00000010, // include properties from a file's property handler, even if it means retrieving the file from offline storage.
+ GPS_DELAYCREATION = 0x00000020, // delay the creation of the file's property handler until those properties are read, written, or enumerated
+ GPS_BESTEFFORT = 0x00000040, // For readonly stores, succeed and return all available properties, even if one or more sources of properties fails. Not valid with GPS_READWRITE.
+ GPS_NO_OPLOCK = 0x00000080, // some data sources protect the read property store with an oplock, this disables that
+ GPS_MASK_VALID = 0x000000FF,
+ }
+
+ ///
+ /// A property store
+ ///
+ [ComImport]
+ [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ interface IPropertyStore
+ {
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+ HResult GetCount([Out] out uint propertyCount);
+
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+ HResult GetAt([In] uint propertyIndex, out PropertyKey key);
+
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+ HResult GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
+
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), PreserveSig]
+ HResult SetValue([In] ref PropertyKey key, [In] PropVariant pv);
+
+ [PreserveSig]
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+ HResult Commit();
+ }
+
+ [ComImport]
+ [Guid("C8E2D566-186E-4D49-BF41-6909EAD56ACC")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ interface IPropertyStoreCapabilities
+ {
+ [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+ HResult IsPropertyWritable([In]ref PropertyKey propertyKey);
+ }
+}
diff --git a/AssociationManager/State.cs b/AssociationManager/State.cs
index d58c0ec..2974a31 100644
--- a/AssociationManager/State.cs
+++ b/AssociationManager/State.cs
@@ -68,7 +68,8 @@ public void Populate(string savedStateFile = null)
public void SortExtensions()
{
- // Sort by file extension, but group by our handler, chained handlers, other handlers, and finally no handler
+ // Sort by file extension, but group by our handler, chained handlers, profile only,
+ // other handlers, and finally no handler
// This uses a Sort extension to ObservableCollection
extensions.Sort((e, f) =>
{
@@ -82,6 +83,10 @@ public void SortExtensions()
return -1;
else if (f.PropertyHandlerState == HandlerState.Chained)
return 1;
+ else if (e.PropertyHandlerState == HandlerState.ProfileOnly)
+ return -1;
+ else if (f.PropertyHandlerState == HandlerState.ProfileOnly)
+ return 1;
else if (e.PropertyHandlerState == HandlerState.Foreign)
return -1;
else if (f.PropertyHandlerState == HandlerState.Foreign)
@@ -409,8 +414,6 @@ private void PopulateExtensions()
if (dictExtensions.TryGetValue(name.ToLower(), out e))
{
e.RecordPropertyHandler(handlerGuid, handlerChainedGuid);
-
- e.IdentifyCurrentProfile();
}
}
}
diff --git a/AssociationMessages/LocalizedMessages.Designer.cs b/AssociationMessages/LocalizedMessages.Designer.cs
index e0ed0f6..b8a7b67 100644
--- a/AssociationMessages/LocalizedMessages.Designer.cs
+++ b/AssociationMessages/LocalizedMessages.Designer.cs
@@ -528,6 +528,15 @@ public static string UseDragDrop {
}
}
+ ///
+ /// Looks up a localized string similar to Windows will not allow the handler for {0} to be extended. Using profile with Windows handler..
+ ///
+ public static string WindowsWontExtendHandler {
+ get {
+ return ResourceManager.GetString("WindowsWontExtendHandler", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Error parsing XML store of saved custom profiles.
///
diff --git a/AssociationMessages/LocalizedMessages.resx b/AssociationMessages/LocalizedMessages.resx
index cd7366e..d36d41c 100644
--- a/AssociationMessages/LocalizedMessages.resx
+++ b/AssociationMessages/LocalizedMessages.resx
@@ -279,4 +279,7 @@
-add and -remove commands require at least one extension to be specified
+
+ Windows will not allow the handler for {0} to be extended. Using profile with Windows handler.
+
\ No newline at end of file
diff --git a/CommandLine/FileMeta.cpp b/CommandLine/FileMeta.cpp
index 8b1328f..f0eaf68 100644
--- a/CommandLine/FileMeta.cpp
+++ b/CommandLine/FileMeta.cpp
@@ -40,6 +40,10 @@ int wmain(int argc, WCHAR* argv[])
SwitchArg promptSwitch(L"p",L"prompt",L"After execution, prompt to continue", false);
cmd.add(promptSwitch);
+ // Define Explorer view of meta data switch
+ SwitchArg explorerSwitch(L"v", L"explorer", L"Export metadata thisExplorer sees", false);
+ cmd.add(explorerSwitch);
+
// Define XML file name override
ValueArg xmlFileArg(L"x",L"xml",L"Name of XML file (only valid if one target file)",false,L"",L"file name");
cmd.add( xmlFileArg );
@@ -83,6 +87,11 @@ int wmain(int argc, WCHAR* argv[])
if (!exportSwitch.isSet())
throw ArgException(L"-c can only be used with -e", L"console");
}
+ else if (explorerSwitch.isSet())
+ {
+ if (!exportSwitch.isSet())
+ throw ArgException(L"-v can only be used with -e", L"explorer");
+ }
for (auto pos = targetFiles.begin(); pos != targetFiles.end(); ++pos)
{
@@ -94,9 +103,11 @@ int wmain(int argc, WCHAR* argv[])
result = ERROR_FILE_NOT_FOUND;
break;
}
- else if (!checker.HasOurPropertyHandler(targetFile))
+ else if (0 == checker.HasPropertyHandler(targetFile) ||
+ (-1 == checker.HasPropertyHandler(targetFile) && !explorerSwitch.isSet()))
{
- // Skip files that do not have our property handler
+ // Skip files that do not have our property handler,
+ // unless we were asked for the Explorer view
continue;
}
else if (deleteSwitch.isSet())
@@ -139,7 +150,7 @@ int wmain(int argc, WCHAR* argv[])
if (exportSwitch.isSet())
{
xml_document doc;
- ExportMetadata(&doc, targetFile);
+ ExportMetadata(&doc, targetFile, explorerSwitch.isSet());
// writing to a string rather than directly to the stream is odd, but writing directly does not compile
// (trying to access a private constructor on traits - a typically arcane template issue)
diff --git a/CommandLine/FileMeta.rc b/CommandLine/FileMeta.rc
index bc5d780..3a44500 100644
Binary files a/CommandLine/FileMeta.rc and b/CommandLine/FileMeta.rc differ
diff --git a/CommandLine/XmlHelpers.cpp b/CommandLine/XmlHelpers.cpp
index 5f74b34..cb5bd2d 100644
--- a/CommandLine/XmlHelpers.cpp
+++ b/CommandLine/XmlHelpers.cpp
@@ -7,6 +7,7 @@
#include "tclap/CmdLine.h"
#include
#include
+#include
using namespace TCLAP;
@@ -58,11 +59,12 @@ WCHAR* CPHException::GetMessage()
return _pszError;
}
-// See if file is handled by our property handler
-BOOL CExtensionChecker::HasOurPropertyHandler(wstring fileName)
+// See if file is handled by our property handler, or another one, or none
+// Returns 1 if our handler is used, 0 if no handler is set up, or -1 if a foreign handler is used
+int CExtensionChecker::HasPropertyHandler(wstring fileName)
{
WCHAR pszExt[_MAX_EXT];
- BOOL val = FALSE;
+ int val = 0;
// Try and get the extension
if (0 == _wsplitpath_s(fileName.c_str(), NULL, 0, NULL, 0, NULL, 0, pszExt, _MAX_EXT))
@@ -78,16 +80,18 @@ BOOL CExtensionChecker::HasOurPropertyHandler(wstring fileName)
WCHAR buffer[_MAX_EXT];
DWORD size = _MAX_EXT;
- // Not finding the key results in FALSE
+ // Not finding the key results in 0
if (ERROR_SUCCESS == RegGetValue(HKEY_LOCAL_MACHINE, subKey.c_str(), NULL, RRF_RT_REG_SZ, NULL, buffer, &size))
{
- // If the key is found, TRUE iff our property handler
+ // If the key is found, 1 iff our property handler, else -1
#ifdef _WIN64
- val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid64));
+ val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid64)) ? 1 : -1;
#else
- val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid32));
+ val = (0 == StrCmpI(buffer, OurPropertyHandlerGuid32)) ? 1 : -1;
#endif
}
+ else
+ val = 0;
m_extensions[pszExt] = val;
}
@@ -363,66 +367,75 @@ inline bool operator<(const PROPERTYKEY& a, const PROPERTYKEY& b)
return a.pid < b.pid;
}
-void ExportMetadata (xml_document *doc, wstring targetFile)
+void ExportMetadata (xml_document *doc, wstring targetFile, bool explorerView)
{
HRESULT hr = E_UNEXPECTED;
+ CComPtr pStore;
+ PROPERTYKEY * keys = NULL;
xml_node *root = doc->allocate_node(node_element, MetadataNodeName);
doc->append_node(root);
- if (GetStgOpenStorageEx())
+ try
{
- CComPtr pPropSetStg;
- CComPtr pStore;
- PROPERTYKEY * keys = NULL;
-
- try
+ if (explorerView)
{
- hr = (v_pfnStgOpenStorageEx)(targetFile.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_FILE, 0, NULL, 0,
- IID_IPropertySetStorage, (void**)&pPropSetStg);
- if( FAILED(hr) )
- throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPSS_1, hr);
-
- hr = PSCreatePropertyStoreFromPropertySetStorage(pPropSetStg, STGM_READ, IID_IPropertyStore, (void **)&pStore);
- pPropSetStg.Release();
- if( FAILED(hr) )
- throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_PSCREATE_1, hr);
-
- DWORD cProps;
- hr = pStore->GetCount(&cProps);
- if( FAILED(hr) )
- throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPS_GETCOUNT_1, hr);
-
- keys = new PROPERTYKEY[cProps];
-
- for (DWORD i = 0; i < cProps; i++)
+ // Access the property store that Explorer would see - will not always be our handler
+ hr = SHGetPropertyStoreFromParsingName(targetFile.c_str(), NULL, GPS_READWRITE, IID_IPropertyStore, (void **)&pStore);
+ }
+ else
+ {
+ // Always use our own handler, which will access the alternate stream
+ CComPtr pPropSetStg;
+ if (GetStgOpenStorageEx())
{
- hr = pStore->GetAt(i, &keys[i]);
+ hr = (v_pfnStgOpenStorageEx)(targetFile.c_str(), STGM_READ | STGM_SHARE_EXCLUSIVE, STGFMT_FILE, 0, NULL, 0,
+ IID_IPropertySetStorage, (void**)&pPropSetStg);
if( FAILED(hr) )
- throw CPHException(ERROR_UNKNOWN_PROPERTY, hr, IDS_E_IPS_GETAT_1, hr);
+ throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPSS_1, hr);
+
+ hr = PSCreatePropertyStoreFromPropertySetStorage(pPropSetStg, STGM_READ, IID_IPropertyStore, (void **)&pStore);
+ pPropSetStg.Release();
}
+ }
- // Sort keys into their property sets
- // We used to use IPropertyStorage to get the grouping, but this worked badly with Unicode property value
- sort(keys, &keys[cProps]);
+ if( FAILED(hr) )
+ throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_PSCREATE_1, hr);
- // Loop through all the properties
- DWORD index = 0;
+ DWORD cProps;
+ hr = pStore->GetCount(&cProps);
+ if( FAILED(hr) )
+ throw CPHException(ERROR_OPEN_FAILED, hr, IDS_E_IPS_GETCOUNT_1, hr);
- while( index < cProps)
- {
- // Export the properties in the property set - throws exceptions on error
- ExportPropertySetData( doc, root, keys, index, pStore );
- }
- }
- catch (CPHException& e)
+ keys = new PROPERTYKEY[cProps];
+
+ for (DWORD i = 0; i < cProps; i++)
{
- delete [] keys;
- throw e;
+ hr = pStore->GetAt(i, &keys[i]);
+ if( FAILED(hr) )
+ throw CPHException(ERROR_UNKNOWN_PROPERTY, hr, IDS_E_IPS_GETAT_1, hr);
}
+ // Sort keys into their property sets
+ // We used to use IPropertyStorage to get the grouping, but this worked badly with Unicode property value
+ sort(keys, &keys[cProps]);
+
+ // Loop through all the properties
+ DWORD index = 0;
+
+ while( index < cProps)
+ {
+ // Export the properties in the property set - throws exceptions on error
+ ExportPropertySetData( doc, root, keys, index, pStore );
+ }
+ }
+ catch (CPHException& e)
+ {
delete [] keys;
+ throw e;
}
+
+ delete [] keys;
}
// throws CPHException on error
diff --git a/CommandLine/XmlHelpers.h b/CommandLine/XmlHelpers.h
index b972188..d485919 100644
--- a/CommandLine/XmlHelpers.h
+++ b/CommandLine/XmlHelpers.h
@@ -35,11 +35,11 @@ class CPHException
class CExtensionChecker
{
private:
- std::map m_extensions; // map of extensions checked so far
+ std::map m_extensions; // map of extensions checked so far
public:
- // See if file is handled by our property handler
- BOOL HasOurPropertyHandler(wstring fileName);
+ // See if file is handled by our property handler, or another, or none
+ int HasPropertyHandler(wstring fileName);
};
int AccessResourceString(UINT uId, LPWSTR lpBuffer, int nBufferMax);
@@ -61,7 +61,7 @@ void OutputDebugStringFormat( WCHAR* lpszFormat, ... );
typedef HRESULT (CALLBACK* PFN_STGOPENSTGEX)(const WCHAR*, DWORD, DWORD, DWORD, void*, void*, REFIID riid, void **);
HRESULT MetadataPresent(wstring targetFile);
-void ExportMetadata (xml_document *doc, wstring targetFile);
+void ExportMetadata (xml_document *doc, wstring targetFile, bool explorerView = false);
void ExportPropertySetData (xml_document *doc, xml_node *root, PROPERTYKEY* keys, DWORD& index, CComPtr pStore);
void ImportMetadata (xml_document *doc, wstring targetFile);
diff --git a/CommandLineAssociationManager/CommandLineAssociationManager.csproj b/CommandLineAssociationManager/CommandLineAssociationManager.csproj
index fc1099c..e9b752f 100644
--- a/CommandLineAssociationManager/CommandLineAssociationManager.csproj
+++ b/CommandLineAssociationManager/CommandLineAssociationManager.csproj
@@ -87,6 +87,9 @@
PropertyListEntry.cs
+
+ PropertyHandlerTest.cs
+
SavedState.cs
@@ -96,6 +99,15 @@
TreeItem.cs
+
+
+
+
+
+
+
+
+
diff --git a/CommandLineAssociationManager/Program.cs b/CommandLineAssociationManager/Program.cs
index 4147c94..d029171 100644
--- a/CommandLineAssociationManager/Program.cs
+++ b/CommandLineAssociationManager/Program.cs
@@ -18,7 +18,7 @@ class Program
"Usage:",
"",
" FileMetaAssoc.exe {-l|-a|-r|-h} [-p=] ",
- " [-d=] [ ...]",
+ " [-d=] [-m] [ ...]",
"",
"Where:",
"",
@@ -42,6 +42,10 @@ class Program
" --d=, --definitions=",
" Profile definitions file to be used for -add",
"",
+ " -m, --merge",
+ " If set -add merges any existing settings into a new profile. ",
+ " Otherwise, profile settings are used",
+ "",
" ",
" One or more target extensions for -add or -remove, for example, .txt",
"",
@@ -57,6 +61,7 @@ static int Main(string[] args)
bool remove = false;
string profile = null;
string definitions = null;
+ bool merge = false;
try
{
@@ -67,6 +72,7 @@ static int Main(string[] args)
{ "h|?|help", v => {help = v != null; commands++;} },
{ "p|profile=", v => profile = v },
{ "d|definitions=", v => definitions = v },
+ { "m|merge", v => merge = true },
};
List extensions = argParser.Parse(args);
@@ -120,7 +126,9 @@ static int Main(string[] args)
ErrorCode = WindowsErrorCodes.ERROR_INVALID_PARAMETER
};
- if (!(e.PropertyHandlerState == HandlerState.Ours || e.PropertyHandlerState == HandlerState.Chained))
+ if (!(e.PropertyHandlerState == HandlerState.Ours ||
+ e.PropertyHandlerState == HandlerState.Chained ||
+ e.PropertyHandlerState == HandlerState.ProfileOnly))
throw new AssocMgrException
{
Description = String.Format(LocalizedMessages.DoesNotHaveHandler, ext),
@@ -158,7 +166,7 @@ static int Main(string[] args)
e = state.CreateExtension(ext);
}
- e.SetupHandlerForExtension(p, false);
+ e.SetupHandlerForExtension(p, merge);
Console.WriteLine(String.Format(LocalizedMessages.HandlerAddedOK, e.Name));
}
}
diff --git a/ContextMenuHandler/ContextMenuHandler.cpp b/ContextMenuHandler/ContextMenuHandler.cpp
index 73005fe..29182ff 100644
--- a/ContextMenuHandler/ContextMenuHandler.cpp
+++ b/ContextMenuHandler/ContextMenuHandler.cpp
@@ -147,7 +147,6 @@ IFACEMETHODIMP CContextMenuHandler::QueryContextMenu(
if (hDrop != NULL)
{
// Determine how many files are involved in this operation.
- // We show the custom context menu item only when exactly one file is selected
UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
WCHAR buff[MAX_PATH];
@@ -258,9 +257,9 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
for (int i = 0; i < m_files.size(); i++)
{
// In the multi-file case, we know only that at least one of the files has our context menu,
- // so we need to check the extension of each file to see if our propertyhandler is configured for it
+ // so we need to check the extension of each file to see if a propertyhandler is configured for it
if (m_files.size() > 1)
- if (!m_checker.HasOurPropertyHandler(m_files[i]))
+ if (0 == m_checker.HasPropertyHandler(m_files[i]))
continue;
// Build an XML document containing thw metadata
@@ -307,7 +306,7 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
// In the multi-file case, we know only that at least one of the files has our context menu,
// so we need to check the extension of each file to see if our propertyhandler is configured for it
if (m_files.size() > 1)
- if (!m_checker.HasOurPropertyHandler(m_files[i]))
+ if (1 != m_checker.HasPropertyHandler(m_files[i]))
continue;
wstring szXmlTarget = m_files[i];
@@ -409,7 +408,7 @@ IFACEMETHODIMP CContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
// so we need to check the extension of each file to see if our propertyhandler is configured for it
if (m_files.size() > 1)
{
- if (!m_checker.HasOurPropertyHandler(m_files[i]))
+ if (1 != m_checker.HasPropertyHandler(m_files[i]))
continue;
// Also, we need to check if metadata is present, to avoid adding an empty alternate stream by opening
diff --git a/ContextMenuHandler/ContextMenuHandler.rc b/ContextMenuHandler/ContextMenuHandler.rc
index 75f87f2..3c44de3 100644
Binary files a/ContextMenuHandler/ContextMenuHandler.rc and b/ContextMenuHandler/ContextMenuHandler.rc differ
diff --git a/ContextMenuHandler/ContextMenuHandler.vcxproj b/ContextMenuHandler/ContextMenuHandler.vcxproj
index f25c848..7302f10 100644
--- a/ContextMenuHandler/ContextMenuHandler.vcxproj
+++ b/ContextMenuHandler/ContextMenuHandler.vcxproj
@@ -134,6 +134,7 @@
+
diff --git a/ContextMenuHandler/Dll.cpp b/ContextMenuHandler/Dll.cpp
index e6d630d..af83cce 100644
--- a/ContextMenuHandler/Dll.cpp
+++ b/ContextMenuHandler/Dll.cpp
@@ -22,7 +22,8 @@ struct CLASS_OBJECT_INIT
// add classes supported by this module here
const CLASS_OBJECT_INIT c_rgClassObjectInit[] =
{
- { &__uuidof(CContextMenuHandler), CContextMenuHandler_CreateInstance }
+ { &__uuidof(CContextMenuHandler), CContextMenuHandler_CreateInstance },
+ { &__uuidof(CExportContextMenuHandler), CExportContextMenuHandler_CreateInstance }
};
@@ -152,12 +153,14 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
STDAPI DllRegisterServer()
{
HRESULT hr = RegisterContextMenuHandler();
+ if (SUCCEEDED(hr)) hr = RegisterExportContextMenuHandler();
return hr;
}
STDAPI DllUnregisterServer()
{
HRESULT hr = UnregisterContextMenuHandler();
+ if (SUCCEEDED(hr)) hr = UnregisterExportContextMenuHandler();
return hr;
}
diff --git a/ContextMenuHandler/ExportContextMenuHandler.cpp b/ContextMenuHandler/ExportContextMenuHandler.cpp
new file mode 100644
index 0000000..381f79d
--- /dev/null
+++ b/ContextMenuHandler/ExportContextMenuHandler.cpp
@@ -0,0 +1,339 @@
+// Some parts copied from CppShellExtContextMenuHandler Copyright (c) Microsoft Corporation and subject to the Microsoft Public License: http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
+// All other code Copyright (c) 2013, Dijji, and released under Ms-PL. This, with other relevant licenses, can be found in the root of this distribution.
+
+#include
+#include
+#include
+#include
+#include
+#include // For IShellExtInit and IContextMenu
+#include // For CComPtr
+#include
+#include "..\CommandLine\XmlHelpers.h"
+#include "resource.h"
+#include "dll.h"
+#include "RegisterExtension.h"
+#include
+#include
+#include
+#include
+using namespace std;
+using namespace rapidxml;
+
+#define IDM_EXPORT 0 // The commands' identifier offsets
+
+static const WCHAR* PropertyHandlerDescription = L"File Metadata Export Context Menu Handler";
+static const WCHAR* ExportVerb = L"CMHExport";
+
+class CExportContextMenuHandler : public IShellExtInit, public IContextMenu
+{
+public:
+ CExportContextMenuHandler() : _cRef(1), m_pdtobj(NULL)
+ {
+ DllAddRef();
+ }
+
+ // IUnknown
+ IFACEMETHODIMP QueryInterface(REFIID riid, void ** ppv)
+ {
+ static const QITAB qit[] =
+ {
+ QITABENT(CExportContextMenuHandler, IContextMenu),
+ QITABENT(CExportContextMenuHandler, IShellExtInit),
+ {0, 0 },
+ };
+ return QISearch(this, qit, riid, ppv);
+ }
+
+ IFACEMETHODIMP_(ULONG) AddRef()
+ {
+ return InterlockedIncrement(&_cRef);
+ }
+
+ IFACEMETHODIMP_(ULONG) Release()
+ {
+ long cRef = InterlockedDecrement(&_cRef);
+ if (cRef == 0)
+ {
+ delete this;
+ }
+ return cRef;
+ }
+
+ // IShellExtInit
+ IFACEMETHODIMP Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID);
+
+ // IContextMenu
+ IFACEMETHODIMP QueryContextMenu(HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags);
+ IFACEMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO pici);
+ IFACEMETHODIMP GetCommandString(UINT_PTR idCommand, UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax);
+
+
+private:
+ ~CExportContextMenuHandler()
+ {
+ SafeRelease(&m_pdtobj);
+ DllRelease();
+ }
+
+ long _cRef;
+ CExtensionChecker m_checker;
+
+ IDataObject *m_pdtobj;
+ std::vector m_files;
+};
+
+
+#pragma region IShellExtInit
+
+// Initialize the context menu handler.
+// If any value other than S_OK is returned from the method, the context
+// menu item is not displayed.
+IFACEMETHODIMP CExportContextMenuHandler::Initialize(
+ LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID)
+{
+ // This is important because Initialize can be called multiple times,
+ // e.g. when more than 16 files are selected
+ SafeRelease(&m_pdtobj);
+
+ if (NULL == pDataObj)
+ {
+ return E_INVALIDARG;
+ }
+ else
+ {
+ m_pdtobj = pDataObj;
+ m_pdtobj->AddRef();
+ }
+
+ return S_OK;
+}
+
+#pragma endregion
+
+#pragma region IContextMenu
+
+//
+// FUNCTION: CExportContextMenuHandler::QueryContextMenu
+//
+// PURPOSE: The Shell calls IContextMenu::QueryContextMenu to allow the
+// context menu handler to add its menu items to the menu. It
+// passes in the HMENU handle in the hmenu parameter. The
+// indexMenu parameter is set to the index to be used for the
+// first menu item that is to be added.
+//
+IFACEMETHODIMP CExportContextMenuHandler::QueryContextMenu(
+ HMENU hMenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags)
+{
+ // If uFlags include CMF_DEFAULTONLY then we should not do anything.
+ if (CMF_DEFAULTONLY & uFlags)
+ {
+ return MAKE_HRESULT(SEVERITY_SUCCESS, 0, USHORT(0));
+ }
+
+ HRESULT hr = E_FAIL;
+
+ FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
+ STGMEDIUM stm;
+
+ // The pDataObj pointer contains the objects being acted upon
+ if (NULL != m_pdtobj && SUCCEEDED(m_pdtobj->GetData(&fe, &stm)))
+ {
+ // Get an HDROP handle.
+ HDROP hDrop = static_cast(GlobalLock(stm.hGlobal));
+ if (hDrop != NULL)
+ {
+ // Determine how many files are involved in this operation.
+ UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
+ WCHAR buff[MAX_PATH];
+
+ for (int i=0; i < nFiles; i++)
+ {
+ DragQueryFile(hDrop, i, buff, sizeof(buff));
+ m_files.push_back(buff);
+ }
+
+ GlobalUnlock(stm.hGlobal);
+ }
+
+ ReleaseStgMedium(&stm);
+ }
+
+ WCHAR szXmlTarget[MAX_PATH];
+ hr = S_OK;
+
+ if (m_files.size() == 1)
+ {
+ wcscpy_s(szXmlTarget, MAX_PATH, m_files[0].c_str());
+ wcscat_s(szXmlTarget, MAX_PATH, MetadataFileSuffix);
+ }
+ else
+ {
+ AccessResourceString(IDS_XML_FILE, szXmlTarget, MAX_PATH);
+ }
+
+ // First, create and populate a submenu.
+ HMENU hSubmenu = CreatePopupMenu();
+ UINT uID = idCmdFirst;
+ WCHAR buffer[2*MAX_PATH];
+
+ // Export
+ AccessResourceString(IDS_EXPORT, buffer, MAX_PATH);
+ wcscat_s(buffer, 2*MAX_PATH, szXmlTarget);
+ if (!InsertMenu ( hSubmenu, 0, MF_BYPOSITION, uID++, buffer) )
+ return HRESULT_FROM_WIN32(GetLastError());
+
+ // Insert the submenu into the ctx menu provided by Explorer.
+ MENUITEMINFO mii = { sizeof(MENUITEMINFO) };
+
+ mii.fMask = MIIM_SUBMENU | MIIM_STRING | MIIM_ID;
+ mii.wID = uID++;
+ mii.hSubMenu = hSubmenu;
+
+ AccessResourceString(IDS_METADATA, buffer, MAX_PATH);
+ mii.dwTypeData = (LPWSTR)buffer;
+
+ if (!InsertMenuItem ( hMenu, indexMenu, TRUE, &mii ) )
+ return HRESULT_FROM_WIN32(GetLastError());
+
+ return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, uID - idCmdFirst );
+}
+
+//
+// FUNCTION: CExportContextMenuHandler::InvokeCommand
+//
+// PURPOSE: This method is called when a user clicks a menu item to tell
+// the handler to run the associated command. The lpcmi parameter
+// points to a structure that contains the needed information.
+//
+IFACEMETHODIMP CExportContextMenuHandler::InvokeCommand(LPCMINVOKECOMMANDINFO pici)
+{
+ HRESULT hr = E_FAIL;
+
+ // Commands can be indicated by offset id or command verb string
+ // We support only the id form
+ if (IS_INTRESOURCE(pici->lpVerb))
+ {
+ switch(LOWORD(pici->lpVerb))
+ {
+ case IDM_EXPORT:
+ try
+ {
+ for (int i = 0; i < m_files.size(); i++)
+ {
+ // In the multi-file case, we know only that at least one of the files has our context menu,
+ // so we need to check the extension of each file to see if a propertyhandler is configured for it
+ if (m_files.size() > 1)
+ if (0 == m_checker.HasPropertyHandler(m_files[i]))
+ continue;
+
+ // Build an XML document containing thw metadata
+ xml_document doc;
+ ExportMetadata(&doc, m_files[i], true);
+
+ // writing to a string rather than directly to the stream is odd, but writing directly does not compile
+ // (trying to access a private constructor on traits - a typically arcane template issue)
+ wstring s;
+ print(std::back_inserter(s), doc, 0);
+
+ wstring szXmlTarget = m_files[i];
+ szXmlTarget += MetadataFileSuffix;
+
+ // Now write from the XML string to a file stream
+ // This used to be STL, but wofstream by default writes 8-bit encoded files, and changing that is complex
+ FILE *pfile;
+ errno_t err = _wfopen_s(&pfile, szXmlTarget.c_str(), L"w+, ccs=UTF-16LE");
+ if (0 == err)
+ {
+ fwrite(s.c_str(), sizeof(WCHAR), s.length(), pfile);
+ fclose(pfile);
+ }
+ else
+ throw CPHException(err, E_FAIL, IDS_E_FILEOPEN_1, err);
+ }
+
+ hr = S_OK;
+ }
+ catch(CPHException& e)
+ {
+ WCHAR buffer[MAX_PATH];
+ AccessResourceString(IDS_EXPORT_FAILED, buffer, MAX_PATH);
+ MessageBox(NULL, e.GetMessage(), buffer, MB_OK);
+ hr = e.GetHResult();
+ }
+ break;
+ }
+ }
+
+ return hr;
+}
+
+
+//
+// FUNCTION: CCExportContextMenuHandler::GetCommandString
+//
+// PURPOSE: This method can be called
+// to request the verb string that is assigned to a command.
+// Either ANSI or Unicode verb strings can be requested.
+// We only implement support for the Unicode values of
+// uFlags, because only those have been used in Windows Explorer
+// since Windows 2000.
+//
+IFACEMETHODIMP CExportContextMenuHandler::GetCommandString(UINT_PTR idCommand,
+ UINT uFlags, UINT *pwReserved, LPSTR pszName, UINT cchMax)
+{
+ HRESULT hr = E_INVALIDARG;
+
+ switch (uFlags)
+ {
+ // GCS_VERBW is an optional feature that enables a caller to
+ // discover the canonical name for the verb passed in through
+ // idCommand.
+ case GCS_VERBW:
+ switch (idCommand)
+ {
+ case IDM_EXPORT:
+ hr = StringCchCopyW(reinterpret_cast(pszName), cchMax, ExportVerb);
+ break;
+ }
+ }
+
+ // If the command (idCommand) is not supported by this context menu
+ // extension handler, return E_INVALIDARG.
+
+ return hr;
+}
+
+
+#pragma region Registration and other COM mechanics
+
+HRESULT CExportContextMenuHandler_CreateInstance(REFIID riid, void **ppv)
+{
+ HRESULT hr = E_OUTOFMEMORY;
+ CExportContextMenuHandler *pirm = new (std::nothrow) CExportContextMenuHandler();
+ if (pirm)
+ {
+ hr = pirm->QueryInterface(riid, ppv);
+ pirm->Release();
+ }
+ return hr;
+}
+
+HRESULT RegisterExportContextMenuHandler()
+{
+ // register the context menu handler COM object
+ CRegisterExtension re(__uuidof(CExportContextMenuHandler), HKEY_LOCAL_MACHINE);
+ HRESULT hr = re.RegisterInProcServer(PropertyHandlerDescription, L"Both");
+
+ return hr;
+}
+
+HRESULT UnregisterExportContextMenuHandler()
+{
+ // Unregister the context menu handler COM object.
+ CRegisterExtension re(__uuidof(CExportContextMenuHandler), HKEY_LOCAL_MACHINE);
+ HRESULT hr = re.UnRegisterObject();
+
+ return hr;
+}
+#pragma endregion
\ No newline at end of file
diff --git a/ContextMenuHandler/dll.h b/ContextMenuHandler/dll.h
index 42684d7..fcd8ec9 100644
--- a/ContextMenuHandler/dll.h
+++ b/ContextMenuHandler/dll.h
@@ -22,13 +22,18 @@ template void SafeRelease(T **ppT)
#ifdef _WIN64
class DECLSPEC_UUID("28D14D00-2D80-4956-9657-9D50C8BB47A5") CContextMenuHandler;
+class DECLSPEC_UUID("DE4C4CAF-C564-4EEA-9FF7-C46FB8023818") CExportContextMenuHandler;
#else
class DECLSPEC_UUID("DA38301B-BE91-4397-B2C8-E27A0BD80CC5") CContextMenuHandler;
+class DECLSPEC_UUID("5A677F18-527D-42B3-BAA0-9785D3A8256F") CExportContextMenuHandler;
#endif
HRESULT CContextMenuHandler_CreateInstance(REFIID riid, void **ppv);
+HRESULT CExportContextMenuHandler_CreateInstance(REFIID riid, void **ppv);
void DllAddRef();
void DllRelease();
HRESULT RegisterContextMenuHandler();
HRESULT UnregisterContextMenuHandler();
+HRESULT RegisterExportContextMenuHandler();
+HRESULT UnregisterExportContextMenuHandler();
diff --git a/PropertyHandler/FileMetaPropertyHandler.rc b/PropertyHandler/FileMetaPropertyHandler.rc
index a09f733..e22f87d 100644
Binary files a/PropertyHandler/FileMetaPropertyHandler.rc and b/PropertyHandler/FileMetaPropertyHandler.rc differ
diff --git a/PropertyHandler/PropertyHandler.cpp b/PropertyHandler/PropertyHandler.cpp
index c409021..4e7df7d 100644
--- a/PropertyHandler/PropertyHandler.cpp
+++ b/PropertyHandler/PropertyHandler.cpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include "dll.h"
#include "RegisterExtension.h"
@@ -119,9 +120,12 @@ HRESULT CPropertyHandler::GetValue(REFPROPERTYKEY key, PROPVARIANT *pPropVar)
PropVariantInit(pPropVar);
HRESULT hr = OpenStore(FALSE);
// Take the File Meta property value first, and if there isn't one,
+ // see if this is a check for the software product name, which we use as a marker, or if not
// try for a chained property value
hr = SUCCEEDED(hr) ? _pStore->GetValue(key, pPropVar) : hr;
- if (_pChainedPropStore != NULL && SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY)
+ if (SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY && key == PKEY_Software_ProductName)
+ hr = InitPropVariantFromString(L"FileMetadata", pPropVar);
+ else if (_pChainedPropStore != NULL && SUCCEEDED(hr) && pPropVar->vt == VT_EMPTY)
hr = _pChainedPropStore->GetValue(key, pPropVar);
return hr;
}
diff --git a/Setup/Product.wxs b/Setup/Product.wxs
index 6668330..797b212 100644
--- a/Setup/Product.wxs
+++ b/Setup/Product.wxs
@@ -6,7 +6,7 @@ Also thanks to Alek Davis for the 32/64-bit strategy: http://alekdavis.blogspot.
-->
-
+
@@ -34,7 +34,7 @@ Also thanks to Alek Davis for the 32/64-bit strategy: http://alekdavis.blogspot.
+ Language="1033" Version="1.6.7318.0" Manufacturer="Dijji" UpgradeCode="$(var.ProductUpgradeCode)">
= 600)]]>
diff --git a/TestDriver/MainWindow.xaml b/TestDriver/MainWindow.xaml
index 93b76d0..9f74f37 100644
--- a/TestDriver/MainWindow.xaml
+++ b/TestDriver/MainWindow.xaml
@@ -39,10 +39,12 @@
-
+
+
+
diff --git a/TestDriver/MainWindow.xaml.cs b/TestDriver/MainWindow.xaml.cs
index 7b3b0db..17cf612 100644
--- a/TestDriver/MainWindow.xaml.cs
+++ b/TestDriver/MainWindow.xaml.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
@@ -60,6 +61,7 @@ private void RunSelected_Click(object sender, RoutedEventArgs e)
state.Results.Clear();
state.TestsToRun.Clear();
+
foreach (Test t in lbTests.SelectedItems)
{
state.TestsToRun.Add(t);
@@ -69,6 +71,21 @@ private void RunSelected_Click(object sender, RoutedEventArgs e)
}
}
+ private void OneOff_Click(object sender, RoutedEventArgs e)
+ {
+ if (state.Running == RunState.Idle)
+ {
+ state.Status = "Running One-off tests...";
+ state.Running = RunState.Running;
+
+ state.Results.Clear();
+
+ // Wire this up to whatever it is we think we need to run
+
+ ThreadPool.QueueUserWorkItem(RunPropertyTests, state);
+ }
+ }
+
private void LoopAll_Click(object sender, RoutedEventArgs e)
{
if (state.Running == RunState.Idle)
@@ -104,6 +121,59 @@ void state_PropertyChanged(object sender, PropertyChangedEventArgs e)
}
}
+ private string resultsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+ private void SaveResults_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new System.Windows.Forms.SaveFileDialog();
+
+ dialog.Title = "Specify file to save to";
+ dialog.InitialDirectory = resultsDirectory;
+ dialog.Filter = "txt Files (*.txt)|*.txt";
+ dialog.FileName = "results";
+
+ if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
+ {
+ resultsDirectory = Path.GetDirectoryName(dialog.FileName);
+ using(var tw = new StreamWriter(dialog.FileName))
+ {
+ foreach (var r in state.Results)
+ tw.WriteLine(r);
+ }
+ }
+ }
+
+ private static void RunPropertyTests(Object state)
+ {
+ State s = (State)state;
+ List summary = new List();
+
+ // Iterate over the files in a hard coded folder,
+ // pumping as many properties as possible into each of them, and recording the results
+ string targetDir = @"D:\TestProperties";
+ string resultsDir = Path.Combine(targetDir, "Results");
+ Directory.CreateDirectory(resultsDir); // Ensure all directories exist
+ foreach (string fileName in Directory.GetFiles(targetDir))
+ {
+ var ext = Path.GetExtension(fileName);
+ var test = new TestMassRoundTrip(ext, fileName);
+ test.Run(s);
+ using (var tw = new StreamWriter(resultsDir + @"\Properties" + ext + ".txt"))
+ {
+ foreach (var r in s.Results)
+ tw.WriteLine(r);
+ }
+ summary.Add(String.Format("{0}, {1}", ext, s.Results[s.Results.Count() - 2]));
+ s.window.Dispatcher.Invoke(new Action(() => s.Results.Clear()));
+ }
+ using (var tw = new StreamWriter(resultsDir + @"\Properties Summary.csv"))
+ {
+ foreach (var r in summary)
+ tw.WriteLine(r);
+ }
+ s.window.Dispatcher.Invoke(new Action(() => { s.Results.Clear(); s.Status = "Done"; }));
+ s.Running = RunState.Idle;
+ }
+
private static void RunTests(Object state)
{
State s = (State)state;
diff --git a/TestDriver/TestMassRoundTrip.cs b/TestDriver/TestMassRoundTrip.cs
index 158dcd4..4a74d5b 100644
--- a/TestDriver/TestMassRoundTrip.cs
+++ b/TestDriver/TestMassRoundTrip.cs
@@ -23,6 +23,7 @@ class TestMassRoundTrip : Test
{
public override string Name { get { return String.Format("Write, read, export and import as many properties on '{0}' file as possible", extension); } }
private string extension;
+ private string fullFileName;
private Random random = new Random();
private const int max16 = 32767;
private const int max32 = 2147483647;
@@ -30,21 +31,26 @@ class TestMassRoundTrip : Test
private long maxTocks = DateTime.MaxValue.Ticks;
private List savedProps;
- public TestMassRoundTrip(string extension)
+ public TestMassRoundTrip(string extension, string fullFileName = null)
{
this.extension = extension;
+ this.fullFileName = fullFileName;
}
public override bool RunBody(State state)
{
- RequirePropertyHandlerRegistered();
- RequireContextHandlerRegistered();
- RequireExtHasHandler(extension);
+ // Unless we are working with a specific target file, ensure the extension is setup
+ if (fullFileName == null)
+ {
+ RequirePropertyHandlerRegistered();
+ RequireContextHandlerRegistered();
+ RequireExtHasHandler(extension);
+ }
state.RecordEntry(String.Format("Starting mass property setting on '{0}'...", extension));
//Create a temp file to put metadata on
- string fileName = CreateFreshFile(1, extension);
+ string fileName = fullFileName == null ? CreateFreshFile(1, extension) : fullFileName;
savedProps = new List();
@@ -64,7 +70,15 @@ public override bool RunBody(State state)
// Use API Code Pack to set the value, except for strings, because the Code Pack blows when setting strings of length 1 !!
// Still use Code Pack elsewhere for its nullable type handling
IShellProperty prop = ShellObject.FromParsingName(fileName).Properties.GetProperty(propDesc.CanonicalName);
- SetPropertyValue(fileName, propDesc, prop);
+ try
+ {
+ SetPropertyValue(fileName, propDesc, prop);
+ }
+ catch (Exception vx)
+ {
+ state.RecordEntry(vx.Message +
+ (vx.InnerException != null ? ": " + vx.InnerException.Message : ""));
+ }
}
state.RecordEntry(String.Format("{0} property values set on {1}", savedProps.Count, fileName));
@@ -75,6 +89,8 @@ public override bool RunBody(State state)
if (errors > 0)
return false;
+ else if (fullFileName != null) // If we have a single target file, we are done
+ return true;
// Use ContextHandler to export all the values
var contextHandler = new CContextMenuHandler();
@@ -118,7 +134,7 @@ public override bool RunBody(State state)
state.RecordEntry(String.Format("{0} properties read back, {1} mismatches", savedProps.Count, errors));
- // Clean up files - checks if they have been released, too
+ // Clean up generated files - checks if they have been released, too
// Leave files around for analysis if there have been problems
if (errors == 0)
{
diff --git a/TestDriverAssoc/Const.cs b/TestDriverAssoc/Const.cs
index 27cf205..230e76d 100644
--- a/TestDriverAssoc/Const.cs
+++ b/TestDriverAssoc/Const.cs
@@ -15,7 +15,10 @@ class Const
const string OurPropertyHandlerGuid32 = "{60211757-EF87-465e-B6C1-B37CF98295F9}";
const string OurContextHandlerGuid64 = "{28D14D00-2D80-4956-9657-9D50C8BB47A5}";
const string OurContextHandlerGuid32 = "{DA38301B-BE91-4397-B2C8-E27A0BD80CC5}";
+ const string OurExportContextHandlerGuid64 = "{DE4C4CAF-C564-4EEA-9FF7-C46FB8023818}";
+ const string OurExportContextHandlerGuid32 = "{5A677F18-527D-42B3-BAA0-9785D3A8256F}";
const string OtherPropertyHandlerGuid = "{a38b883c-1682-497e-97b0-0a3a9e801682}"; // Windows image
+ const string MkvPropertyHandlerGuid = "{C591F150-4106-4141-B5C1-30B2101453BD}";
public const string FullDetailsValueName = "FullDetails";
public const string PreviewDetailsValueName = "PreviewDetails";
@@ -31,9 +34,11 @@ class Const
public static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid64; } }
public static string OurPropertyHandlerGuid32bit { get { return OurPropertyHandlerGuid32; } }
public static string OurContextHandlerGuid { get { return OurContextHandlerGuid64; } }
+ public static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid64; } }
#elif x86
public static string OurPropertyHandlerGuid { get { return OurPropertyHandlerGuid32; } }
public static string OurContextHandlerGuid { get { return OurContextHandlerGuid32; } }
+ public static string OurExportContextHandlerGuid { get { return OurExportContextHandlerGuid32; } }
#endif
const string FullDetailsOfficeProfile = "prop:System.PropGroup.Description;System.Title;System.Subject;System.Keywords;System.Category;System.Comment;System.Rating;System.PropGroup.Origin;System.Author;System.Document.LastAuthor;System.Document.RevisionNumber;System.Document.Version;System.ApplicationName;System.Company;System.Document.Manager;System.Document.DateCreated;System.Document.DateSaved;System.Document.DatePrinted;System.Document.TotalEditingTime;System.PropGroup.Content;System.ContentStatus;System.ContentType;System.Document.PageCount;System.Document.WordCount;System.Document.CharacterCount;System.Document.LineCount;System.Document.ParagraphCount;System.Document.Template;System.Document.Scale;System.Document.LinksDirty;System.Language;System.PropGroup.FileSystem;System.ItemNameDisplay;System.ItemType;System.ItemFolderPathDisplay;System.DateCreated;System.DateModified;System.Size;System.FileAttributes;System.OfflineAvailability;System.OfflineStatus;System.SharedWith;System.FileOwner;System.ComputerName";
const string PreviewDetailsOfficeProfile = "prop:*System.DateModified;System.Author;System.Keywords;System.Rating;*System.Size;System.Title;System.Comment;System.Category;*System.Document.PageCount;System.ContentStatus;System.ContentType;*System.OfflineAvailability;*System.OfflineStatus;System.Subject;*System.DateCreated;*System.SharedWith";
@@ -43,6 +48,13 @@ class Const
const string PreviewDetailsSimpleProfile = "prop:System.Title;System.Subject;System.Keywords;System.Category;System.Comment;System.Rating;System.Author;System.Document.RevisionNumber";
const string InfoTipSimpleProfile = "prop:System.ItemTypeText;System.Size;System.DateModified;System.Comment";
+ const string FullDetailsMkv = "prop:System.PropGroup.Description;System.Title;System.Media.SubTitle;System.Rating;System.Keywords;System.Comment;System.PropGroup.Video;System.Media.Duration;System.Video.FrameWidth;System.Video.FrameHeight;System.Video.EncodingBitrate;System.Video.TotalBitrate;System.Video.FrameRate;System.PropGroup.Audio;System.Audio.EncodingBitrate;System.Audio.ChannelCount;System.Audio.SampleRate;System.PropGroup.Media;System.Music.Artist;System.Media.Year;System.Music.Genre;System.PropGroup.Origin;System.Video.Director;System.Media.Producer;System.Media.Writer;System.Media.Publisher;System.Media.ContentDistributor;System.Media.DateEncoded;System.Media.EncodedBy;System.Media.AuthorUrl;System.Media.PromotionUrl;System.Copyright;System.PropGroup.Content;System.ParentalRating;System.ParentalRatingReason;System.Music.Composer;System.Music.Conductor;System.Music.Period;System.Music.Mood;System.Music.PartOfSet;System.Music.InitialKey;System.Music.BeatsPerMinute;System.DRM.IsProtected;System.PropGroup.FileSystem;System.ItemNameDisplay;System.ItemType;System.ItemFolderPathDisplay;System.Size;System.DateCreated;System.DateModified;System.FileAttributes;System.OfflineAvailability;System.OfflineStatus;System.SharedWith;System.FileOwner;System.ComputerName";
+ const string PreviewDetailsMkv = "prop:*System.Title;*System.Media.Duration;*System.Size;*System.Video.FrameWidth;*System.Video.FrameHeight;System.Rating;*System.Keywords;*System.Comment;*System.Music.Artist;*System.Music.Genre;*System.ParentalRating;*System.OfflineAvailability;*System.OfflineStatus;*System.DateModified;*System.DateCreated;*System.SharedWith;*System.Media.SubTitle;*System.Media.Year;*System.Video.FrameRate;*System.Video.EncodingBitrate;*System.Video.TotalBitrate";
+ const string InfoTipMkv = "prop:System.ItemType;System.Size;System.Media.Duration;System.OfflineAvailability";
+ const string FullDetailsMkvAndSimpleProfile = "prop:System.PropGroup.Description;System.Title;System.Media.SubTitle;System.Rating;System.Keywords;System.Comment;System.Subject;System.Category;System.PropGroup.Video;System.Media.Duration;System.Video.FrameWidth;System.Video.FrameHeight;System.Video.EncodingBitrate;System.Video.TotalBitrate;System.Video.FrameRate;System.PropGroup.Audio;System.Audio.EncodingBitrate;System.Audio.ChannelCount;System.Audio.SampleRate;System.PropGroup.Media;System.Music.Artist;System.Media.Year;System.Music.Genre;System.PropGroup.Origin;System.Video.Director;System.Media.Producer;System.Media.Writer;System.Media.Publisher;System.Media.ContentDistributor;System.Media.DateEncoded;System.Media.EncodedBy;System.Media.AuthorUrl;System.Media.PromotionUrl;System.Copyright;System.Author;System.Document.RevisionNumber;System.PropGroup.Content;System.ParentalRating;System.ParentalRatingReason;System.Music.Composer;System.Music.Conductor;System.Music.Period;System.Music.Mood;System.Music.PartOfSet;System.Music.InitialKey;System.Music.BeatsPerMinute;System.DRM.IsProtected;System.PropGroup.FileSystem;System.ItemNameDisplay;System.ItemType;System.ItemFolderPathDisplay;System.Size;System.DateCreated;System.DateModified;System.FileAttributes;System.OfflineAvailability;System.OfflineStatus;System.SharedWith;System.FileOwner;System.ComputerName";
+ const string PreviewDetailsMkvAndSimpleProfile = "prop:*System.Title;*System.Media.Duration;*System.Size;*System.Video.FrameWidth;*System.Video.FrameHeight;System.Rating;*System.Keywords;*System.Comment;*System.Music.Artist;*System.Music.Genre;*System.ParentalRating;*System.OfflineAvailability;*System.OfflineStatus;*System.DateModified;*System.DateCreated;*System.SharedWith;*System.Media.SubTitle;*System.Media.Year;*System.Video.FrameRate;*System.Video.EncodingBitrate;*System.Video.TotalBitrate;System.Subject;System.Category;System.Author;System.Document.RevisionNumber";
+ const string InfoTipMkvAndSimpleProfile = "prop:System.ItemType;System.Size;System.Media.Duration;System.OfflineAvailability;System.ItemTypeText;System.DateModified;System.Comment";
+
//const string FullDetailsCustomProfile = "prop:System.PropGroup.Description;System.Keywords;System.Category;System.Comment;System.Rating;System.PropGroup.Origin;System.Author";
//const string PreviewDetailsCustomProfile = "prop:System.Keywords;System.Category;System.Comment;System.Rating;System.Author";
//const string InfoTipCustomProfile = "prop:System.ItemTypeText;System.Comment";
@@ -300,6 +312,35 @@ class Const
#if x64
PropertyHandler32 = OurPropertyHandlerGuid32,
ChainedPropertyHandler32 = OtherPropertyHandlerGuid,
+#endif
+ };
+ public static RegState V15ExtendedMkv = new RegState
+ {
+ SystemFullDetails = FullDetailsSimpleProfile,
+ SystemPreviewDetails = PreviewDetailsSimpleProfile,
+ SystemInfoTip = InfoTipSimpleProfile,
+ SystemOldFullDetails = FullDetailsMkv,
+ SystemOldPreviewDetails = PreviewDetailsMkv,
+ SystemOldInfoTip = InfoTipMkv,
+ SystemContextMenuHandler = OurExportContextHandlerGuid,
+ PropertyHandler = MkvPropertyHandlerGuid,
+#if x64
+ PropertyHandler32 = MkvPropertyHandlerGuid,
+#endif
+ };
+ public static RegState V15ExtendedMergedMkv = new RegState
+ {
+ SystemFullDetails = FullDetailsMkvAndSimpleProfile,
+ SystemPreviewDetails = PreviewDetailsMkvAndSimpleProfile,
+ SystemInfoTip = InfoTipMkvAndSimpleProfile,
+ SystemOldFullDetails = FullDetailsMkv,
+ SystemOldPreviewDetails = PreviewDetailsMkv,
+ SystemOldInfoTip = InfoTipMkv,
+ SystemCustomProfile = ".mkv",
+ SystemContextMenuHandler = OurExportContextHandlerGuid,
+ PropertyHandler = MkvPropertyHandlerGuid,
+#if x64
+ PropertyHandler32 = MkvPropertyHandlerGuid,
#endif
};
}
diff --git a/TestDriverAssoc/Properties/Resources.Designer.cs b/TestDriverAssoc/Properties/Resources.Designer.cs
index 6c133d7..5d3c624 100644
--- a/TestDriverAssoc/Properties/Resources.Designer.cs
+++ b/TestDriverAssoc/Properties/Resources.Designer.cs
@@ -8,10 +8,10 @@
//
//------------------------------------------------------------------------------
-namespace TestDriverAssoc.Properties
-{
-
-
+namespace TestDriverAssoc.Properties {
+ using System;
+
+
///
/// A strongly-typed resource class, for looking up localized strings, etc.
///
@@ -22,48 +22,40 @@ namespace TestDriverAssoc.Properties
[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
- {
-
+ 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()
- {
+ 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))
- {
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TestDriverAssoc.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
- {
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
return resourceCulture;
}
- set
- {
+ set {
resourceCulture = value;
}
}
diff --git a/TestDriverAssoc/Properties/Settings.Designer.cs b/TestDriverAssoc/Properties/Settings.Designer.cs
index 397d93c..3dc4bed 100644
--- a/TestDriverAssoc/Properties/Settings.Designer.cs
+++ b/TestDriverAssoc/Properties/Settings.Designer.cs
@@ -8,21 +8,17 @@
//
//------------------------------------------------------------------------------
-namespace TestDriverAssoc.Properties
-{
-
-
+namespace TestDriverAssoc.Properties {
+
+
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")]
- internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
- {
-
+ [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
- {
+
+ public static Settings Default {
+ get {
return defaultInstance;
}
}
diff --git a/TestDriverAssoc/TestCommandLine.cs b/TestDriverAssoc/TestCommandLine.cs
index 85e6bec..c685a14 100644
--- a/TestDriverAssoc/TestCommandLine.cs
+++ b/TestDriverAssoc/TestCommandLine.cs
@@ -1,5 +1,6 @@
// Copyright (c) 2016, Dijji, and released under Ms-PL. This, with other relevant licenses, can be found in the root of this distribution.
+using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -19,6 +20,19 @@ public enum WindowsErrorCode
ERROR_XML_PARSE_ERROR = 1465,
}
+ private static int? releaseVersion = null;
+ private static int ReleaseVersion
+ {
+ get
+ {
+ if (releaseVersion == null)
+ {
+ var val = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ReleaseId", null);
+ releaseVersion = val != null ? Int32.Parse(val.ToString()) : 0;
+ }
+ return (int)releaseVersion;
+ }
+ }
///
///
///
@@ -112,6 +126,17 @@ public static void Run(Object obj)
Add(state, "Extend existing .bmp property handler with both settings", "V15ExtendedBmpBoth", "-p=.bmp -d=SavedState.xml", ref Const.V15ExtendedBmpBoth, ref pass, Const.V15UnExtendedBoth);
Common.ResetProfile();
+ if (ReleaseVersion >= 1903)
+ {
+ // Test profile only
+ var initial = AddExtend(state, "Profile existing .mkv property handler with simple", ".mkv", "-p=simple", ref Const.V15ExtendedMkv, ref pass);
+ RemoveExtend(state, "Remove extended .mkv profile", ".mkv", ref initial, ref pass);
+ initial = AddExtend(state, "Profile existing .mkv property handler with merge", ".mkv", "-p=simple -m", ref Const.V15ExtendedMergedMkv, ref pass);
+ RemoveExtend(state, "Remove extended .mkv profile", ".mkv", ref initial, ref pass);
+ initial = AddExtend(state, "Profile existing .mkv property handler with custom", ".mkv", "-p=.mkv", ref Const.V15ExtendedMergedMkv, ref pass);
+ RemoveExtend(state, "Remove extended .mkv profile", ".mkv", ref initial, ref pass);
+ }
+
state.AddReport(String.Format("#{0}: {1}", state.TestCounter++, pass ? "Passed" : "Failed"));
overallPass &= pass;
@@ -120,16 +145,16 @@ public static void Run(Object obj)
state.AddReport("");
state.AddReport(String.Format("#{0}: Test various error conditions", state.TestCounter));
- Error(state, "No arguments at all", "", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Bad command", "-x", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Two commands", "-a -r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Remove without an extension", "-r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Remove with a bad extension", "-r nosuch", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Remove with a non-existent extension", "-r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, false);
- Error(state, "Add without an extension", "-a", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, true);
- Error(state, "Add without a profile", "-a", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, false);
- Error(state, "Add with a bad profile", "-a -p=nosuch", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, false);
- Error(state, "Add with a data file and a bad profile", "-a -p=nosuch -d=SavedState.xml", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, false);
+ Error(state, "No arguments at all", "", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Bad command", "-x", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Two commands", "-a -r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Remove without an extension", "-r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Remove with a bad extension", "-r nosuch", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Remove with a non-existent extension", "-r", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass);
+ Error(state, "Add without an extension", "-a", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass, null);
+ Error(state, "Add without a profile", "-a", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass);
+ Error(state, "Add with a bad profile", "-a -p=nosuch", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass);
+ Error(state, "Add with a data file and a bad profile", "-a -p=nosuch -d=SavedState.xml", WindowsErrorCode.ERROR_INVALID_PARAMETER, ref pass);
state.AddReport(String.Format("#{0}: {1}", state.TestCounter++, pass ? "Passed" : "Failed"));
overallPass &= pass;
@@ -237,10 +262,60 @@ public static void Add(State state, string description, string name, string args
RegState.Wipe(Const.TestExt);
}
- private static void Error(State state, string description, string args, WindowsErrorCode error, ref bool pass, bool noExtension)
+ public static RegState AddExtend(State state, string description, string ext, string args, ref RegState final, ref bool pass)
+ {
+ state.AddReport(description);
+ var initial = new RegState();
+ initial.Read(ext);
+
+ var result = InvokeFileMetaAssoc(state, "-a " + args, true, ext);
+ if (result != 0)
+ {
+ state.AddReport(String.Format("FileMetaAssoc -a failed for {0}", ext));
+ pass = false;
+ }
+
+ if (pass)
+ {
+ var outcome = new RegState();
+ outcome.Read(ext);
+
+ // Verify that we got the final state
+ if (outcome != final)
+ {
+ state.AddReport(String.Format("Add did not produce the expected final registry state for {0}", ext));
+ pass = false;
+ }
+ }
+
+ return initial;
+ }
+
+ public static void RemoveExtend(State state, string description, string ext, ref RegState final, ref bool pass)
+ {
+ state.AddReport(description);
+ var result = InvokeFileMetaAssoc(state, "-r", true, ext);
+ if (result != 0)
+ {
+ state.AddReport(String.Format("FileMetaAssoc -r failed for {0}", ext));
+ pass = false;
+ }
+
+ var outcome = new RegState();
+ outcome.Read(ext);
+
+ // If the final state was specified, verify that we got it
+ if (outcome != final)
+ {
+ state.AddReport(String.Format("Remove did not produce the expected final registry state for {0}", ext));
+ pass = false;
+ }
+ }
+
+ private static void Error(State state, string description, string args, WindowsErrorCode error, ref bool pass, string extension = Const.TestExt)
{
state.AddReport(description);
- var result = InvokeFileMetaAssoc(state, args, true, noExtension);
+ var result = InvokeFileMetaAssoc(state, args, true, extension);
if (result != (int)error)
{
@@ -249,7 +324,7 @@ private static void Error(State state, string description, string args, WindowsE
}
}
- private static int InvokeFileMetaAssoc(State state, string args, bool noErrorReport = false, bool noExtension = false)
+ private static int InvokeFileMetaAssoc(State state, string args, bool noErrorReport = false, string extension = Const.TestExt)
{
Process p = new Process();
string line;
@@ -260,10 +335,10 @@ private static int InvokeFileMetaAssoc(State state, string args, bool noErrorRep
p.StartInfo.RedirectStandardError = true;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.FileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"File Metadata\FileMetaAssoc.exe");
- if (noExtension)
+ if (extension == null)
p.StartInfo.Arguments = args;
else
- p.StartInfo.Arguments = String.Format("{0} \"{1}\"", args, Const.TestExt);
+ p.StartInfo.Arguments = String.Format("{0} \"{1}\"", args, extension);
p.Start();
while (true)
{
diff --git a/TestDriverAssoc/TestDriverAssoc.csproj b/TestDriverAssoc/TestDriverAssoc.csproj
index 95fe78b..1bfff8a 100644
--- a/TestDriverAssoc/TestDriverAssoc.csproj
+++ b/TestDriverAssoc/TestDriverAssoc.csproj
@@ -10,10 +10,11 @@
Properties
TestDriverAssoc
TestDriverAssoc
- v3.5
+ v4.0
512
{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
4
+
x86
@@ -62,6 +63,7 @@
+
@@ -118,6 +120,7 @@
ResXFileCodeGenerator
Resources.Designer.cs
+
Designer
diff --git a/TestDriverAssoc/app.config b/TestDriverAssoc/app.config
new file mode 100644
index 0000000..e365603
--- /dev/null
+++ b/TestDriverAssoc/app.config
@@ -0,0 +1,3 @@
+
+
+