/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Collections.Generic; using System.Drawing; using System.Threading; using System.Windows.Forms; using Greenshot.IniFile; using Greenshot.Plugin; using GreenshotPlugin.UnmanagedHelpers; using log4net; namespace GreenshotPlugin.Core { /// /// Description of AbstractDestination. /// public abstract class AbstractDestination : IDestination { private static readonly ILog Log = LogManager.GetLogger(typeof(AbstractDestination)); private static readonly CoreConfiguration CoreConfig = IniConfig.GetIniSection(); public virtual int CompareTo(object obj) { if (!(obj is IDestination other)) { return 1; } if (Priority == other.Priority) { return string.Compare(Description, other.Description, StringComparison.Ordinal); } return Priority - other.Priority; } public abstract string Designation { get; } public abstract string Description { get; } public virtual int Priority => 10; public virtual Image DisplayIcon => null; public virtual Keys EditorShortcutKeys => Keys.None; public virtual IEnumerable DynamicDestinations() { yield break; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { //if (disposing) {} } public virtual bool IsDynamic => false; public virtual bool UseDynamicsOnly => false; public virtual bool IsLinkable => false; public virtual bool IsActive { get { if (CoreConfig.ExcludeDestinations != null && CoreConfig.ExcludeDestinations.Contains(Designation)) { return false; } return true; } } public abstract ExportInformation ExportCapture(bool manuallyInitiated, ISurface surface, ICaptureDetails captureDetails); /// /// A small helper method to perform some default destination actions, like inform the surface of the export /// /// /// public void ProcessExport(ExportInformation exportInformation, ISurface surface) { if (exportInformation != null && exportInformation.ExportMade) { if (!string.IsNullOrEmpty(exportInformation.Uri)) { surface.UploadUrl = exportInformation.Uri; surface.SendMessageEvent(this, SurfaceMessageTyp.UploadedUri, Language.GetFormattedString("exported_to", exportInformation.DestinationDescription)); } else if (!string.IsNullOrEmpty(exportInformation.Filepath)) { surface.LastSaveFullPath = exportInformation.Filepath; surface.SendMessageEvent(this, SurfaceMessageTyp.FileSaved, Language.GetFormattedString("exported_to", exportInformation.DestinationDescription)); } else { surface.SendMessageEvent(this, SurfaceMessageTyp.Info, Language.GetFormattedString("exported_to", exportInformation.DestinationDescription)); } surface.Modified = false; } else if (!string.IsNullOrEmpty(exportInformation?.ErrorMessage)) { surface.SendMessageEvent(this, SurfaceMessageTyp.Error, Language.GetFormattedString("exported_to_error", exportInformation.DestinationDescription) + " " + exportInformation.ErrorMessage); } } public override string ToString() { return Description; } /// /// Helper method to add events which set the tag, this way we can see why there might be a close. /// /// Item to add the events to /// Menu to set the tag /// Value for the tag private void AddTagEvents(ToolStripMenuItem menuItem, ContextMenuStrip menu, string tagValue) { if (menuItem != null && menu != null) { menuItem.MouseDown += delegate { Log.DebugFormat("Setting tag to '{0}'", tagValue); menu.Tag = tagValue; }; menuItem.MouseUp += delegate { Log.Debug("Deleting tag"); menu.Tag = null; }; } } /// /// This method will create and show the destination picker menu /// /// Boolean if the dynamic values also need to be added /// The surface which can be exported /// Details for the surface /// The list of destinations to show /// public ExportInformation ShowPickerMenu(bool addDynamics, ISurface surface, ICaptureDetails captureDetails, IEnumerable destinations) { // Generate an empty ExportInformation object, for when nothing was selected. ExportInformation exportInformation = new ExportInformation(Designation, Language.GetString("settings_destination_picker")); var menu = new ContextMenuStrip { ImageScalingSize = CoreConfig.ScaledIconSize, Tag = null, TopLevel = true }; menu.Closing += delegate(object source, ToolStripDropDownClosingEventArgs eventArgs) { Log.DebugFormat("Close reason: {0}", eventArgs.CloseReason); switch (eventArgs.CloseReason) { case ToolStripDropDownCloseReason.AppFocusChange: if (menu.Tag == null) { // Do not allow the close if the tag is not set, this means the user clicked somewhere else. eventArgs.Cancel = true; } else { Log.DebugFormat("Letting the menu 'close' as the tag is set to '{0}'", menu.Tag); } break; case ToolStripDropDownCloseReason.ItemClicked: case ToolStripDropDownCloseReason.CloseCalled: // The ContextMenuStrip can be "closed" for these reasons. break; case ToolStripDropDownCloseReason.Keyboard: // Dispose as the close is clicked if (!captureDetails.HasDestination("Editor")) { surface.Dispose(); surface = null; } break; default: eventArgs.Cancel = true; break; } }; menu.MouseEnter += delegate { // in case the menu has been unfocused, focus again so that dropdown menus will still open on mouseenter if(!menu.ContainsFocus) menu.Focus(); }; foreach (IDestination destination in destinations) { // Fix foreach loop variable for the delegate ToolStripMenuItem item = destination.GetMenuItem(addDynamics, menu, delegate(object sender, EventArgs e) { ToolStripMenuItem toolStripMenuItem = sender as ToolStripMenuItem; IDestination clickedDestination = (IDestination) toolStripMenuItem?.Tag; if (clickedDestination == null) { return; } menu.Tag = clickedDestination.Designation; // Export exportInformation = clickedDestination.ExportCapture(true, surface, captureDetails); if (exportInformation != null && exportInformation.ExportMade) { Log.InfoFormat("Export to {0} success, closing menu", exportInformation.DestinationDescription); // close menu if the destination wasn't the editor menu.Close(); // Cleanup surface, only if there is no editor in the destinations and we didn't export to the editor if (!captureDetails.HasDestination("Editor") && !"Editor".Equals(clickedDestination.Designation)) { surface.Dispose(); surface = null; } } else { Log.Info("Export cancelled or failed, showing menu again"); // Make sure a click besides the menu don't close it. menu.Tag = null; // This prevents the problem that the context menu shows in the task-bar ShowMenuAtCursor(menu); } } ); if (item != null) { menu.Items.Add(item); } } // Close menu.Items.Add(new ToolStripSeparator()); ToolStripMenuItem closeItem = new ToolStripMenuItem(Language.GetString("editor_close")) { Image = GreenshotResources.getImage("Close.Image") }; closeItem.Click += delegate { // This menu entry is the close itself, we can dispose the surface menu.Close(); if (!captureDetails.HasDestination("Editor")) { surface.Dispose(); surface = null; } }; menu.Items.Add(closeItem); ShowMenuAtCursor(menu); return exportInformation; } /// /// This method will show the supplied context menu at the mouse cursor, also makes sure it has focus and it's not visible in the taskbar. /// /// private static void ShowMenuAtCursor(ContextMenuStrip menu) { // find a suitable location Point location = Cursor.Position; Rectangle menuRectangle = new Rectangle(location, menu.Size); menuRectangle.Intersect(WindowCapture.GetScreenBounds()); if (menuRectangle.Height < menu.Height) { location.Offset(-40, -(menuRectangle.Height - menu.Height)); } else { location.Offset(-40, -10); } // This prevents the problem that the context menu shows in the task-bar User32.SetForegroundWindow(PluginUtils.Host.NotifyIcon.ContextMenuStrip.Handle); menu.Show(location); menu.Focus(); // Wait for the menu to close, so we can dispose it. while (true) { if (menu.Visible) { Application.DoEvents(); Thread.Sleep(100); } else { menu.Dispose(); break; } } } /// /// Return a menu item /// /// /// /// /// ToolStripMenuItem public virtual ToolStripMenuItem GetMenuItem(bool addDynamics, ContextMenuStrip menu, EventHandler destinationClickHandler) { var basisMenuItem = new ToolStripMenuItem(Description) { Image = DisplayIcon, Tag = this, Text = Description }; AddTagEvents(basisMenuItem, menu, Description); basisMenuItem.Click -= destinationClickHandler; basisMenuItem.Click += destinationClickHandler; if (IsDynamic && addDynamics) { basisMenuItem.DropDownOpening += delegate { if (basisMenuItem.DropDownItems.Count == 0) { List subDestinations = new List(); // Fixing Bug #3536968 by catching the COMException (every exception) and not displaying the "subDestinations" try { subDestinations.AddRange(DynamicDestinations()); } catch (Exception ex) { Log.ErrorFormat("Skipping {0}, due to the following error: {1}", Description, ex.Message); } if (subDestinations.Count > 0) { if (UseDynamicsOnly && subDestinations.Count == 1) { basisMenuItem.Tag = subDestinations[0]; basisMenuItem.Text = subDestinations[0].Description; basisMenuItem.Click -= destinationClickHandler; basisMenuItem.Click += destinationClickHandler; } else { foreach (IDestination subDestination in subDestinations) { var destinationMenuItem = new ToolStripMenuItem(subDestination.Description) { Tag = subDestination, Image = subDestination.DisplayIcon }; destinationMenuItem.Click += destinationClickHandler; AddTagEvents(destinationMenuItem, menu, subDestination.Description); basisMenuItem.DropDownItems.Add(destinationMenuItem); } } } } }; } return basisMenuItem; } } }