From 7d28b6a3a552f4c9de330e0ad92d18c3c8a26949 Mon Sep 17 00:00:00 2001 From: RKrom Date: Tue, 9 Sep 2014 17:35:51 +0200 Subject: [PATCH] Enhanced the external command plug-in to capture the first Uri in the output, and places this on the clipboard. Additionally the Uri is linked in the notify bubble or when started from the editor. The behaviour can only be modified via the greenshot.ini and is for all commands. --- Greenshot/Forms/ImageEditorForm.cs | 37 ++-- .../additional_files/readme.txt.template | 1 + .../ExternalCommandConfiguration.cs | 21 ++- .../ExternalCommandDestination.cs | 131 +++++++++---- GreenshotPlugin/Core/ClipboardHelper.cs | 2 +- GreenshotPlugin/Core/PluginUtils.cs | 6 + GreenshotPlugin/Interfaces/IDestination.cs | 3 + GreenshotPlugin/UnmanagedHelpers/Shell32.cs | 172 ++++++++++++++++++ 8 files changed, 319 insertions(+), 54 deletions(-) diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index a39be467d..13fac00d1 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -316,6 +316,7 @@ namespace Greenshot { fileStripMenuItem.DropDownItems.Add(closeToolStripMenuItem); } + private delegate void SurfaceMessageReceivedThreadSafeDelegate(object sender, SurfaceMessageEventArgs eventArgs); /// /// This is the SufraceMessageEvent receiver which display a message in the status bar if the /// surface is exported. It also updates the title to represent the filename, if there is one. @@ -323,22 +324,26 @@ namespace Greenshot { /// /// private void SurfaceMessageReceived(object sender, SurfaceMessageEventArgs eventArgs) { - string dateTime = DateTime.Now.ToLongTimeString(); - // TODO: Fix that we only open files, like in the tooltip - switch (eventArgs.MessageType) { - case SurfaceMessageTyp.FileSaved: - // Put the event message on the status label and attach the context menu - updateStatusLabel(dateTime + " - " + eventArgs.Message, fileSavedStatusContextMenu); - // Change title - Text = eventArgs.Surface.LastSaveFullPath + " - " + Language.GetString(LangKey.editor_title); - break; - case SurfaceMessageTyp.Error: - case SurfaceMessageTyp.Info: - case SurfaceMessageTyp.UploadedUri: - default: - // Put the event message on the status label - updateStatusLabel(dateTime + " - " + eventArgs.Message); - break; + if (InvokeRequired) { + this.Invoke(new SurfaceMessageReceivedThreadSafeDelegate(SurfaceMessageReceived), new object[] { sender, eventArgs }); + } else { + string dateTime = DateTime.Now.ToLongTimeString(); + // TODO: Fix that we only open files, like in the tooltip + switch (eventArgs.MessageType) { + case SurfaceMessageTyp.FileSaved: + // Put the event message on the status label and attach the context menu + updateStatusLabel(dateTime + " - " + eventArgs.Message, fileSavedStatusContextMenu); + // Change title + Text = eventArgs.Surface.LastSaveFullPath + " - " + Language.GetString(LangKey.editor_title); + break; + case SurfaceMessageTyp.Error: + case SurfaceMessageTyp.Info: + case SurfaceMessageTyp.UploadedUri: + default: + // Put the event message on the status label + updateStatusLabel(dateTime + " - " + eventArgs.Message); + break; + } } } diff --git a/Greenshot/releases/additional_files/readme.txt.template b/Greenshot/releases/additional_files/readme.txt.template index e6dbd6eca..784dd9612 100644 --- a/Greenshot/releases/additional_files/readme.txt.template +++ b/Greenshot/releases/additional_files/readme.txt.template @@ -16,6 +16,7 @@ Features: * Editor: a settings window for the torn-edge effect has been added. * Editor: a settings window for the drop shadow effect has been added. * OneNote: Enabled and enhanced the OneNote destination, so we can test this and see if it's worth releasing. +* External command: If a command outputs an URI this will be captured and placed on the clipboard, the behaviour currently can only be modified in the greenshot.ini Bugs resolved: * BUG-1559, BUG-1643: Repeating hotkeys are now prevented. diff --git a/GreenshotExternalCommandPlugin/ExternalCommandConfiguration.cs b/GreenshotExternalCommandPlugin/ExternalCommandConfiguration.cs index 9ddb195ce..d4ca201da 100644 --- a/GreenshotExternalCommandPlugin/ExternalCommandConfiguration.cs +++ b/GreenshotExternalCommandPlugin/ExternalCommandConfiguration.cs @@ -32,9 +32,24 @@ namespace ExternalCommand { public class ExternalCommandConfiguration : IniSection { [IniProperty("Commands", Description="The commands that are available.")] public List commands; - - [IniProperty("DoNotRedirect", Description="Skip redirect of standard output", DefaultValue="false")] - public bool DoNotRedirect; + + [IniProperty("RedirectStandardError", Description = "Redirect the standard error of all external commands, used to output as warning to the greenshot.log.", DefaultValue = "true")] + public bool RedirectStandardError; + + [IniProperty("RedirectStandardOutput", Description = "Redirect the standard output of all external commands, used for different other functions (more below).", DefaultValue = "true")] + public bool RedirectStandardOutput; + + [IniProperty("ShowStandardOutputInLog", Description = "Depends on 'RedirectStandardOutput': Show standard output of all external commands to the Greenshot log, this can be usefull for debugging.", DefaultValue = "false")] + public bool ShowStandardOutputInLog; + + [IniProperty("ParseForUri", Description = "Depends on 'RedirectStandardOutput': Parse the output and take the first found URI, if a URI is found than clicking on the notify bubble goes there.", DefaultValue = "true")] + public bool ParseOutputForUri; + + [IniProperty("OutputToClipboard", Description = "Depends on 'RedirectStandardOutput': Place the standard output on the clipboard.", DefaultValue = "false")] + public bool OutputToClipboard; + + [IniProperty("UriToClipboard", Description = "Depends on 'RedirectStandardOutput' & 'ParseForUri': If an URI is found in the standard input, place it on the clipboard. (This overwrites the output from OutputToClipboard setting.)", DefaultValue = "true")] + public bool UriToClipboard; [IniProperty("Commandline", Description="The commandline for the output command.")] public Dictionary commandlines; diff --git a/GreenshotExternalCommandPlugin/ExternalCommandDestination.cs b/GreenshotExternalCommandPlugin/ExternalCommandDestination.cs index 94f272cec..190009e8e 100644 --- a/GreenshotExternalCommandPlugin/ExternalCommandDestination.cs +++ b/GreenshotExternalCommandPlugin/ExternalCommandDestination.cs @@ -27,6 +27,7 @@ using Greenshot.IniFile; using Greenshot.Plugin; using GreenshotPlugin.Core; using System.ComponentModel; +using System.Text.RegularExpressions; namespace ExternalCommand { /// @@ -34,6 +35,7 @@ namespace ExternalCommand { /// public class ExternalCommandDestination : AbstractDestination { private static log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(ExternalCommandDestination)); + private static Regex URI_REGEXP = new Regex(@"((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)"); private static ExternalCommandConfiguration config = IniConfig.GetIniSection(); private string presetCommand; @@ -78,39 +80,81 @@ namespace ExternalCommand { fullPath = ImageOutput.SaveNamedTmpFile(surface, captureDetails, outputSettings); } - string output = null; + string output; + string error; if (runInBackground) { Thread commandThread = new Thread(delegate() { - CallExternalCommand(presetCommand, fullPath, out output); + CallExternalCommand(exportInformation, presetCommand, fullPath, out output, out error); + ProcessExport(exportInformation, surface); }); commandThread.Name = "Running " + presetCommand; commandThread.IsBackground = true; + commandThread.SetApartmentState(ApartmentState.STA); commandThread.Start(); exportInformation.ExportMade = true; } else { - try { - if (CallExternalCommand(presetCommand, fullPath, out output) == 0) { - exportInformation.ExportMade = true; - } else { - exportInformation.ErrorMessage = output; - } - } catch (Exception ex) { - exportInformation.ErrorMessage = ex.Message; - } + CallExternalCommand(exportInformation, presetCommand, fullPath, out output, out error); + ProcessExport(exportInformation, surface); } - - //exportInformation.Uri = "file://" + fullPath; } - ProcessExport(exportInformation, surface); return exportInformation; } - private int CallExternalCommand(string commando, string fullPath, out string output) { + /// + /// Wrapper method for the background and normal call, this does all the logic: + /// Call the external command, parse for URI, place to clipboard and set the export information + /// + /// + /// + /// + /// + /// + private void CallExternalCommand(ExportInformation exportInformation, string commando, string fullPath, out string output, out string error) { + output = null; + error = null; try { - return CallExternalCommand(commando, fullPath, null, out output); + if (CallExternalCommand(presetCommand, fullPath, out output, out error) == 0) { + exportInformation.ExportMade = true; + if (!string.IsNullOrEmpty(output)) { + MatchCollection uriMatches = URI_REGEXP.Matches(output); + // Place output on the clipboard before the URI, so if one is found this overwrites + if (config.OutputToClipboard) { + ClipboardHelper.SetClipboardData(output); + } + if (uriMatches != null && uriMatches.Count >= 0) { + exportInformation.Uri = uriMatches[0].Groups[1].Value; + LOG.InfoFormat("Got URI : {0} ", exportInformation.Uri); + if (config.UriToClipboard) { + ClipboardHelper.SetClipboardData(exportInformation.Uri); + } + } + } + } else { + LOG.WarnFormat("Error calling external command: {0} ", output); + exportInformation.ExportMade = false; + exportInformation.ErrorMessage = error; + } + } catch (Exception ex) { + LOG.WarnFormat("Error calling external command: {0} ", exportInformation.ErrorMessage); + exportInformation.ExportMade = false; + exportInformation.ErrorMessage = ex.Message; + } + } + + /// + /// Wrapper to retry with a runas + /// + /// + /// + /// + /// + /// + private int CallExternalCommand(string commando, string fullPath, out string output, out string error) { + try { + return CallExternalCommand(commando, fullPath, null, out output, out error); } catch (Win32Exception w32ex) { try { - return CallExternalCommand(commando, fullPath, "runas", out output); + return CallExternalCommand(commando, fullPath, "runas", out output, out error); } catch { w32ex.Data.Add("commandline", config.commandlines[presetCommand]); w32ex.Data.Add("arguments", config.arguments[presetCommand]); @@ -123,32 +167,51 @@ namespace ExternalCommand { } } - private int CallExternalCommand(string commando, string fullPath, string verb, out string output) { + /// + /// The actual executing code for the external command + /// + /// + /// + /// + /// + /// + /// + private int CallExternalCommand(string commando, string fullPath, string verb, out string output, out string error) { string commandline = config.commandlines[commando]; string arguments = config.arguments[commando]; output = null; + error = null; if (!string.IsNullOrEmpty(commandline)) { - using (Process p = new Process()) { - p.StartInfo.FileName = commandline; - p.StartInfo.Arguments = String.Format(arguments, fullPath); - p.StartInfo.UseShellExecute = false; - if (!config.DoNotRedirect) { - p.StartInfo.RedirectStandardOutput = true; + using (Process process = new Process()) { + process.StartInfo.FileName = commandline; + process.StartInfo.Arguments = String.Format(arguments, fullPath); + process.StartInfo.UseShellExecute = false; + if (config.RedirectStandardOutput) { + process.StartInfo.RedirectStandardOutput = true; + } + if (config.RedirectStandardError) { + process.StartInfo.RedirectStandardError = true; } if (verb != null) { - p.StartInfo.Verb = verb; + process.StartInfo.Verb = verb; } - LOG.Info("Starting : " + p.StartInfo.FileName + " " + p.StartInfo.Arguments); - p.Start(); - p.WaitForExit(); - if (!config.DoNotRedirect) { - output = p.StandardOutput.ReadToEnd(); - if (output != null && output.Trim().Length > 0) { - LOG.Info("Output:\n" + output); + LOG.InfoFormat("Starting : {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + process.Start(); + process.WaitForExit(); + if (config.RedirectStandardOutput) { + output = process.StandardOutput.ReadToEnd(); + if (config.ShowStandardOutputInLog && output != null && output.Trim().Length > 0) { + LOG.InfoFormat("Output:\n{0}", output); + } } + if (config.RedirectStandardError) { + error = process.StandardError.ReadToEnd(); + if (error != null && error.Trim().Length > 0) { + LOG.WarnFormat("Error:\n{0}", error); + } } - LOG.Info("Finished : " + p.StartInfo.FileName + " " + p.StartInfo.Arguments); - return p.ExitCode; + LOG.InfoFormat("Finished : {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + return process.ExitCode; } } return -1; diff --git a/GreenshotPlugin/Core/ClipboardHelper.cs b/GreenshotPlugin/Core/ClipboardHelper.cs index a786872da..bd56f112e 100644 --- a/GreenshotPlugin/Core/ClipboardHelper.cs +++ b/GreenshotPlugin/Core/ClipboardHelper.cs @@ -130,7 +130,7 @@ EndSelection:<<<<<<<4 /// private static void SetDataObject(IDataObject ido, bool copy) { lock (clipboardLockObject) { - int retryCount = 2; + int retryCount = 5; while (retryCount >= 0) { try { Clipboard.SetDataObject(ido, copy); diff --git a/GreenshotPlugin/Core/PluginUtils.cs b/GreenshotPlugin/Core/PluginUtils.cs index c72574a2c..94cbc6242 100644 --- a/GreenshotPlugin/Core/PluginUtils.cs +++ b/GreenshotPlugin/Core/PluginUtils.cs @@ -25,6 +25,7 @@ using System.Windows.Forms; using Greenshot.Plugin; using log4net; using Microsoft.Win32; +using GreenshotPlugin.UnmanagedHelpers; namespace GreenshotPlugin.Core { /// @@ -83,6 +84,11 @@ namespace GreenshotPlugin.Core { return appIcon.ToBitmap(); } } + using (Icon appIcon = Shell32.GetFileIcon(path, Shell32.IconSize.Small, false)) { + if (appIcon != null) { + return appIcon.ToBitmap(); + } + } } catch (Exception exIcon) { LOG.Error("error retrieving icon: ", exIcon); } diff --git a/GreenshotPlugin/Interfaces/IDestination.cs b/GreenshotPlugin/Interfaces/IDestination.cs index a73840889..d4cd9849e 100644 --- a/GreenshotPlugin/Interfaces/IDestination.cs +++ b/GreenshotPlugin/Interfaces/IDestination.cs @@ -56,6 +56,9 @@ namespace Greenshot.Plugin { } } + /// + /// Set to true to specify if the export worked. + /// public bool ExportMade { get { return exportMade; diff --git a/GreenshotPlugin/UnmanagedHelpers/Shell32.cs b/GreenshotPlugin/UnmanagedHelpers/Shell32.cs index 7ef749ca4..6c3e6397e 100644 --- a/GreenshotPlugin/UnmanagedHelpers/Shell32.cs +++ b/GreenshotPlugin/UnmanagedHelpers/Shell32.cs @@ -33,6 +33,178 @@ namespace GreenshotPlugin.UnmanagedHelpers { public static extern int ExtractIconEx(string sFile, int iIndex, out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons); [DllImport("shell32", CharSet = CharSet.Unicode)] internal static extern IntPtr ExtractAssociatedIcon(HandleRef hInst, StringBuilder iconPath, ref int index); + [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags); + + #region Structs + [StructLayout(LayoutKind.Sequential)] + private struct SHITEMID { + public ushort cb; + [MarshalAs(UnmanagedType.LPArray)] + public byte[] abID; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ITEMIDLIST { + public SHITEMID mkid; + } + + [StructLayout(LayoutKind.Sequential)] + private struct BROWSEINFO { + public IntPtr hwndOwner; + public IntPtr pidlRoot; + public IntPtr pszDisplayName; + [MarshalAs(UnmanagedType.LPTStr)] + public string lpszTitle; + public uint ulFlags; + public IntPtr lpfn; + public int lParam; + public IntPtr iImage; + } + [StructLayout(LayoutKind.Sequential)] + private struct SHFILEINFO { + public IntPtr hIcon; + public int iIcon; + public uint dwAttributes; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szDisplayName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] + public string szTypeName; + }; + + #endregion + + #region Constants + // Browsing for directory. + private const uint BIF_RETURNONLYFSDIRS = 0x0001; + private const uint BIF_DONTGOBELOWDOMAIN = 0x0002; + private const uint BIF_STATUSTEXT = 0x0004; + private const uint BIF_RETURNFSANCESTORS = 0x0008; + private const uint BIF_EDITBOX = 0x0010; + private const uint BIF_VALIDATE = 0x0020; + private const uint BIF_NEWDIALOGSTYLE = 0x0040; + private const uint BIF_USENEWUI = (BIF_NEWDIALOGSTYLE | BIF_EDITBOX); + private const uint BIF_BROWSEINCLUDEURLS = 0x0080; + private const uint BIF_BROWSEFORCOMPUTER = 0x1000; + private const uint BIF_BROWSEFORPRINTER = 0x2000; + private const uint BIF_BROWSEINCLUDEFILES = 0x4000; + private const uint BIF_SHAREABLE = 0x8000; + + private const uint SHGFI_ICON = 0x000000100; // get icon + private const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name + private const uint SHGFI_TYPENAME = 0x000000400; // get type name + private const uint SHGFI_ATTRIBUTES = 0x000000800; // get attributes + private const uint SHGFI_ICONLOCATION = 0x000001000; // get icon location + private const uint SHGFI_EXETYPE = 0x000002000; // return exe type + private const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index + private const uint SHGFI_LINKOVERLAY = 0x000008000; // put a link overlay on icon + private const uint SHGFI_SELECTED = 0x000010000; // show icon in selected state + private const uint SHGFI_ATTR_SPECIFIED = 0x000020000; // get only specified attributes + private const uint SHGFI_LARGEICON = 0x000000000; // get large icon + private const uint SHGFI_SMALLICON = 0x000000001; // get small icon + private const uint SHGFI_OPENICON = 0x000000002; // get open icon + private const uint SHGFI_SHELLICONSIZE = 0x000000004; // get shell size icon + private const uint SHGFI_PIDL = 0x000000008; // pszPath is a pidl + private const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; // use passed dwFileAttribute + private const uint SHGFI_ADDOVERLAYS = 0x000000020; // apply the appropriate overlays + private const uint SHGFI_OVERLAYINDEX = 0x000000040; // Get the index of the overlay + + private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080; + + #endregion + + /// + /// Options to specify the size of icons to return. + /// + public enum IconSize { + /// + /// Specify large icon - 32 pixels by 32 pixels. + /// + Large = 0, + /// + /// Specify small icon - 16 pixels by 16 pixels. + /// + Small = 1 + } + + /// + /// Options to specify whether folders should be in the open or closed state. + /// + public enum FolderType { + /// + /// Specify open folder. + /// + Open = 0, + /// + /// Specify closed folder. + /// + Closed = 1 + } + + /// + /// Returns an icon for a given file - indicated by the name parameter. + /// + /// Pathname for file. + /// Large or small + /// Whether to include the link icon + /// System.Drawing.Icon + public static Icon GetFileIcon(string name, IconSize size, bool linkOverlay) { + SHFILEINFO shfi = new SHFILEINFO(); + uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES; + + if (true == linkOverlay) { + flags += Shell32.SHGFI_LINKOVERLAY; + } + + /* Check the size specified for return. */ + if (IconSize.Small == size) { + flags += Shell32.SHGFI_SMALLICON; + } else { + flags += Shell32.SHGFI_LARGEICON; + } + + SHGetFileInfo(name, Shell32.FILE_ATTRIBUTE_NORMAL, ref shfi, (uint)Marshal.SizeOf(shfi), flags); + + // Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly + Icon icon = (Icon)Icon.FromHandle(shfi.hIcon).Clone(); + // Cleanup + User32.DestroyIcon(shfi.hIcon); + return icon; + } + + /// + /// Used to access system folder icons. + /// + /// Specify large or small icons. + /// Specify open or closed FolderType. + /// System.Drawing.Icon + public static Icon GetFolderIcon(IconSize size, FolderType folderType) { + // Need to add size check, although errors generated at present! + uint flags = SHGFI_ICON | SHGFI_USEFILEATTRIBUTES; + + if (FolderType.Open == folderType) { + flags += SHGFI_OPENICON; + } + + if (IconSize.Small == size) { + flags += SHGFI_SMALLICON; + } else { + flags += SHGFI_LARGEICON; + } + + // Get the folder icon + SHFILEINFO shfi = new SHFILEINFO(); + SHGetFileInfo(null, FILE_ATTRIBUTE_DIRECTORY, ref shfi, (uint)Marshal.SizeOf(shfi), flags); + + //Icon.FromHandle(shfi.hIcon); // Load the icon from an HICON handle + // Now clone the icon, so that it can be successfully stored in an ImageList + Icon icon = (Icon)Icon.FromHandle(shfi.hIcon).Clone(); + + // Cleanup + User32.DestroyIcon(shfi.hIcon); + return icon; + } /// /// Returns an icon representation of an image contained in the specified file.