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.