Newer
Older
AutoTypeSearch / AutoTypeSearch / SearchWindow.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Media;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using AutoTypeSearch.Properties;
using KeePass.Forms;
using KeePass.Resources;
using KeePass.UI;
using KeePass.Util;
using KeePassLib;
using KeePassLib.Collections;
using KeePassLib.Native;

namespace AutoTypeSearch
{
	public partial class SearchWindow : Form
	{
		private const int SecondLineInset = 10;

		// HACK to work around mono bug
		private static readonly FieldInfo sMonoListBoxTopIndex = typeof(ListBox).GetField("top_index", BindingFlags.Instance | BindingFlags.NonPublic);

		private readonly MainForm mMainForm;
		private readonly Bitmap mBannerImage;
		private readonly Searcher mSearcher;

		private readonly Stream mThrobberImageStream;

		private int? mWindowTopBorderHeight;
		private int mBannerWidth = -1;
		private int mMaximumExpandHeight;
		private bool mManualSizeApplied;
		private SearchResults mCurrentSearch;
		private SearchResults mLastResultsUpdated;
		private int mLastResultsUpdatedNextAvailableIndex;

		#region Opening
		public SearchWindow()
		{
			InitializeComponent();

			// Mono can't load animated gifs from resx without crashing, so load it from an embedded resource instead
			try
			{
				mThrobberImageStream = GetType().Assembly.GetManifestResourceStream("AutoTypeSearch.Throbber.gif");
				if (mThrobberImageStream != null)
				{
					mThrobber.Image = Image.FromStream(mThrobberImageStream);
				}
			}
			catch (Exception ex)
			{
				Debug.Fail("Failed to load Throbber.gif from embedded resource: " + ex.Message);
			}

			GlobalWindowManager.CustomizeControl(this);
			UIUtil.SetExplorerTheme(mResults, true);
			SetItemHeight();
		}

		public SearchWindow(MainForm mainForm, string infoBanner) : this()
		{
			mMainForm = mainForm;

			mInfoBanner.Height = Math.Max(mInfoBannerImage.Height, mInfoLabel.Font.Height) + mInfoBanner.Margin.Vertical;
			mInfoLabel.Padding = new Padding(0, (mInfoBanner.Height - mInfoLabel.Font.Height) / 2, 0, 0);
			mInfoLabel.Text = infoBanner;

			if (infoBanner == null)
			{
				mInfoBanner.Visible = false;
				mInfoBanner.Height = 0;
			}
			
			mSearcher = new Searcher(mMainForm.DocumentManager.GetOpenDatabases().ToArray());

			Icon = mMainForm.Icon;
			using (var bannerIcon = new Icon(Icon, 48, 48))
			{
				mBannerImage = bannerIcon.ToBitmap();
			}
			UpdateBanner();

			ShowThrobber = false;

			FontUtil.AssignDefaultItalic(mNoResultsLabel);
		}


		protected override void OnCreateControl()
		{
			base.OnCreateControl();

			if (NativeMethods.IsWindows10())
			{
				mWindowTopBorderHeight = PointToScreen(Point.Empty).Y - this.Top;
				NativeMethods.RefreshWindowFrame(Handle);
			}

			var windowRect = Settings.Default.WindowPosition;
			var collapsedWindowRect = windowRect;
			
			collapsedWindowRect.Height = mSeq.Bottom + (Height - ClientSize.Height);

			MinimumSize = new Size(MinimumSize.Width, collapsedWindowRect.Height);

			if (windowRect.IsEmpty || !IsOnScreen(collapsedWindowRect))
			{
				windowRect = new Rectangle(0, 0, Width, Height);
				Height = collapsedWindowRect.Height;

				CenterToScreen();
			}
			else
			{
				Location = windowRect.Location;
				Size = collapsedWindowRect.Size;
			}

			mMaximumExpandHeight = Math.Max(windowRect.Height, MinimumSize.Height + mResults.ItemHeight);

			AutoCompleteStringCollection sequences = new AutoCompleteStringCollection();
			if (Settings.Default.AdditionalCustomSequences != null)
			{
				foreach (string s in Settings.Default.AdditionalCustomSequences)
				{
					sequences.Add(s);
				}
			}
			mSeq.AutoCompleteCustomSource = sequences;
		}
		

		private static bool IsOnScreen(Rectangle rectangle)
		{
			return Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(rectangle));
		}

		private void SetItemHeight()
		{
			mResults.ItemHeight = mResults.Font.Height * 2 + 2;
		}

		protected override void WndProc(ref Message m)
		{
			if (mWindowTopBorderHeight.HasValue)
			{
				NativeMethods.RemoveWindowFrameTopBorder(ref m, mWindowTopBorderHeight.Value);
			}
			base.WndProc(ref m);
		}

		#endregion

		#region Closing
		protected override void OnActivated(EventArgs e)
		{
			base.OnActivated(e);
			Deactivate += OnDeactivate;
		}

		private void OnDeactivate(object sender, EventArgs eventArgs)
		{
			Close();
		}

		protected override void OnClosed(EventArgs e)
		{
			Deactivate -= OnDeactivate;
			base.OnClosed(e);
		}

		/// <summary>
		/// Clean up any resources being used.
		/// </summary>
		/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
		protected override void Dispose(bool disposing)
		{
			if (disposing && (components != null))
			{
				if (mBannerImage != null)
				{
					mBannerImage.Dispose();
				}
				if (mThrobber.Image != null)
				{
					mThrobber.Image.Dispose();
					mThrobber.Image = null;
					mThrobberImageStream.Dispose();
				}
				components.Dispose();
			}
			base.Dispose(disposing);
		}
		#endregion

		#region Item Drawing
		private void mResults_DrawItem(object sender, DrawItemEventArgs e)
		{
			var searchResult = mResults.Items[e.Index] as SearchResult;
			if (searchResult == null)
			{
				Debug.Fail("Unexpected item in mResults");
// ReSharper disable once HeuristicUnreachableCode - Not unreachable
				return;
			}
			var drawingArea = e.Bounds;
			drawingArea.Height--; // Leave room for a dividing line at the bottom
			
			if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
			{
				DrawBorderedRectangle(e.Graphics, drawingArea, SystemColors.Highlight);
			}
			else
			{
				e.Graphics.FillRectangle(SystemBrushes.Window, drawingArea);
			}

			var image = GetImage(searchResult.Database, searchResult.Entry.CustomIconUuid, searchResult.Entry.IconId);
			var imageMargin = (drawingArea.Height - image.Height) / 2;
			e.Graphics.DrawImage(image, drawingArea.Left + imageMargin, drawingArea.Top + imageMargin, image.Width, image.Height);

			var textLeftMargin = drawingArea.Left + imageMargin * 2 + image.Width;
			var textBounds = new Rectangle(textLeftMargin, drawingArea.Top + 1, drawingArea.Width - textLeftMargin - 1, drawingArea.Height - 2);

			var line1Bounds = textBounds;
			line1Bounds.Height = e.Font.Height;
			var line2Bounds = line1Bounds;
			line2Bounds.Y += line2Bounds.Height - 1;
			line2Bounds.X += SecondLineInset;
			line2Bounds.Width -= SecondLineInset;

			var resultInTitleField = searchResult.FieldName == PwDefs.TitleField;

			var title = (resultInTitleField ? searchResult.FieldValue : searchResult.Title).Replace('\n', ' '); // The FieldValue may have references resolved, whereas the title is always read directly.

			var uniqueTitlePartWidth = 0;
			if (!String.IsNullOrEmpty(searchResult.UniqueTitlePart))
			{
				var uniqueTitlePart = searchResult.UniqueTitlePart.Replace('\n', ' ');

				var titleWidth = TextRenderer.MeasureText(e.Graphics, title, e.Font, line1Bounds.Size, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis).Width;

				var availableWidthForUniqueTitlePart = line1Bounds.Width - titleWidth;
				if (availableWidthForUniqueTitlePart > 20) // Don't bother including a unique part if there's no room for it
				{
					var uniqueTitlePartReversed = ReverseString(uniqueTitlePart);

					uniqueTitlePartWidth = TextRenderer.MeasureText(e.Graphics, uniqueTitlePartReversed, e.Font, new Size(availableWidthForUniqueTitlePart, line1Bounds.Height), TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis | TextFormatFlags.ModifyString).Width;

					uniqueTitlePart = ReverseString(uniqueTitlePartReversed);

					TextRenderer.DrawText(e.Graphics, uniqueTitlePart, e.Font, new Rectangle(line1Bounds.X, line1Bounds.Y, uniqueTitlePartWidth, line1Bounds.Height), SystemColors.GrayText, TextFormatFlags.NoPadding);
				}
			}

			var titleBounds = new Rectangle(line1Bounds.X + uniqueTitlePartWidth, line1Bounds.Y, line1Bounds.Width - uniqueTitlePartWidth, line1Bounds.Height);

			if (resultInTitleField)
			{
				// Found the result in the title field. Highlight title in first line.
				DrawHighlight(e, titleBounds, title, searchResult.Start, searchResult.Length);
			}

			TextRenderer.DrawText(e.Graphics, searchResult.Title, e.Font, titleBounds, SystemColors.WindowText, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis);

			if (resultInTitleField)
			{
				// Found the result in the title field. Use Username for second line.
				TextRenderer.DrawText(e.Graphics, KPRes.UserName + ": " + searchResult.Entry.Strings.ReadSafeEx(PwDefs.UserNameField), e.Font, line2Bounds, SystemColors.GrayText, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis);
			}
			else
			{
				// Found the result in not title field. Show the matching result on second line
				
				var fieldValue = searchResult.FieldValue.Replace('\n',' ');
				var fieldNamePrefix = GetDisplayFieldName(searchResult.FieldName) + ": ";

				var remainingSpace = line2Bounds.Width;
				var fieldNamePrefixWidth = TextRenderer.MeasureText(e.Graphics, fieldNamePrefix, e.Font, new Size(remainingSpace, line2Bounds.Height), TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis).Width;
				remainingSpace -= fieldNamePrefixWidth;

				int fieldValueHighlightWidth = 0, fieldValueLeftContextWidth = 0, fieldValueRightContextWidth = 0;

				var leftContext = fieldValue.Substring(0, searchResult.Start);
				var highlight = fieldValue.Substring(searchResult.Start, searchResult.Length);
				var rightContext = fieldValue.Substring(searchResult.Start + searchResult.Length);

				if (searchResult.Length == 0)
				{
					fieldValueHighlightWidth = remainingSpace;
				}
				else
				{
					if (remainingSpace > 0)
					{
						var availableSpace = remainingSpace;
						fieldValueHighlightWidth = TextRenderer.MeasureText(e.Graphics, highlight, e.Font, new Size(availableSpace, line2Bounds.Height), TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis).Width;
						remainingSpace -= fieldValueHighlightWidth;
					}

					// Of the space remaining, divide it equally between that which comes before, and that which comes after
					if (!String.IsNullOrEmpty(leftContext))
					{
						var leftContextReversed = ReverseString(leftContext);
						fieldValueLeftContextWidth = TextRenderer.MeasureText(e.Graphics, leftContextReversed, e.Font, new Size(remainingSpace / 2, line2Bounds.Height), TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis | TextFormatFlags.ModifyString).Width;

						if (fieldValueLeftContextWidth > remainingSpace)
						{
							// Always allow space for the minimal left context
							fieldValueHighlightWidth -= (fieldValueLeftContextWidth - remainingSpace);
							remainingSpace = 0;
						}
						else
						{
							remainingSpace -= fieldValueLeftContextWidth;							
						}
						
						// Replace left context with the truncated reversed left context.
						leftContext = ReverseString(leftContextReversed);
					}

					if (remainingSpace > 0 && !String.IsNullOrEmpty(rightContext))
					{
						fieldValueRightContextWidth = TextRenderer.MeasureText(e.Graphics, rightContext, e.Font, new Size(remainingSpace, line2Bounds.Height), TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis).Width;
						if (fieldValueRightContextWidth > remainingSpace)
						{
							fieldValueRightContextWidth = 0;
						}
					}
				}

				// Now draw it all
				var bounds = line2Bounds;
				bounds.Width = fieldNamePrefixWidth;
				TextRenderer.DrawText(e.Graphics, fieldNamePrefix, e.Font, bounds, SystemColors.GrayText, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis);
				if (fieldValueLeftContextWidth > 0)
				{
					bounds.X += bounds.Width;
					bounds.Width = fieldValueLeftContextWidth;
					TextRenderer.DrawText(e.Graphics, leftContext, e.Font, bounds, SystemColors.GrayText, TextFormatFlags.NoPadding); // No ellipsis as the leftContext string has already been truncated appropriately
				}
				if (fieldValueHighlightWidth > 0)
				{
					bounds.X += bounds.Width;
					bounds.Width = fieldValueHighlightWidth;

					if (searchResult.Length > 0)
					{
						DrawHighlightRectangle(e, bounds);
					}
					TextRenderer.DrawText(e.Graphics, highlight, e.Font, bounds, SystemColors.GrayText, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis);
				}
				if (fieldValueRightContextWidth > 0)
				{
					bounds.X += bounds.Width;
					bounds.Width = fieldValueRightContextWidth;
					TextRenderer.DrawText(e.Graphics, rightContext, e.Font, bounds, SystemColors.GrayText, TextFormatFlags.NoPadding | TextFormatFlags.EndEllipsis);
				}
			}

			e.Graphics.DrawLine(SystemPens.ButtonFace, drawingArea.Left, drawingArea.Bottom, drawingArea.Right, drawingArea.Bottom);
		}

		private static string ReverseString(string value)
		{
			return new String(value.ToCharArray().TakeWhile(c => c != '\0').Reverse().ToArray());
		}

		private static void DrawHighlight(DrawItemEventArgs e, Rectangle lineBounds, string text, int highlightFrom, int highlightLength)
		{
			var highlightX = TextRenderer.MeasureText(e.Graphics, text.Substring(0, highlightFrom), e.Font, Size.Empty, TextFormatFlags.NoPadding).Width;
			var highlightWidth = TextRenderer.MeasureText(e.Graphics, text.Substring(0, highlightFrom + highlightLength), e.Font, Size.Empty, TextFormatFlags.NoPadding).Width - highlightX;

			DrawHighlightRectangle(e, new Rectangle(lineBounds.Left + highlightX, lineBounds.Top, highlightWidth, lineBounds.Height));
		}

		private static void DrawHighlightRectangle(DrawItemEventArgs e, Rectangle rectangle)
		{
			DrawBorderedRectangle(e.Graphics, rectangle, Color.PaleTurquoise);
		}

		private static void DrawBorderedRectangle(Graphics graphics, Rectangle rectangle, Color colour)
		{
			var border = rectangle;
			border.Width--;
			border.Height--;

			using (var brush = new SolidBrush(MergeColors(colour, SystemColors.Window, 0.2)))
			{
				graphics.FillRectangle(brush, rectangle);
			}
			using (var pen = new Pen(colour, 1f))
			{
				graphics.DrawRectangle(pen, border);
			}
		}

		private Image GetImage(PwDatabase database, PwUuid customIconId, PwIcon iconId)
		{
			Image image = null;
			if (!customIconId.Equals(PwUuid.Zero))
			{
				image = database.GetCustomIcon(customIconId, DpiUtil.ScaleIntX(16), DpiUtil.ScaleIntY(16));
			}
			if (image == null)
			{
				try { image = mMainForm.ClientIcons.Images[(int)iconId]; }
				catch (Exception) { Debug.Assert(false); }
			}

			return image;
		}

		private static string GetDisplayFieldName(string fieldName)
		{
			switch (fieldName)
			{
				case PwDefs.TitleField:
					return KPRes.Title;
				case PwDefs.UserNameField:
					return KPRes.UserName;
				case PwDefs.PasswordField:
					return KPRes.Password;
				case PwDefs.UrlField:
					return KPRes.Url;
				case PwDefs.NotesField:
					return KPRes.Notes;
				case AutoTypeSearchExt.TagsVirtualFieldName:
					return KPRes.Tags;
				default:
					return fieldName;
			}
		}

		public static Color MergeColors(Color from, Color to, double amount)
		{
			var r = (byte)((from.R * amount) + to.R * (1 - amount));
			var g = (byte)((from.G * amount) + to.G * (1 - amount));
			var b = (byte)((from.B * amount) + to.B * (1 - amount));
			return Color.FromArgb(r, g, b);
		}
		#endregion

		#region Mouse tracking
		private Point mMouseEntryPosition;
		
		private void mResults_MouseEnter(object sender, EventArgs e)
		{
			mMouseEntryPosition = MousePosition;
		}

		private void mResults_MouseMove(object sender, MouseEventArgs e)
		{
			// Discard the location the mouse has on entering the control (as it may be that the control has just moved under the mouse, not the other way around)
			if (MousePosition == mMouseEntryPosition)
			{
				return;
			}

			// Hot tracking
			var hoverIndex = mResults.IndexFromPoint(e.X, e.Y);
			if (hoverIndex >= 0 && mResults.SelectedIndex != hoverIndex)
			{
				if (mResults.GetItemRectangle(hoverIndex).Bottom <= mResults.ClientRectangle.Bottom)
				{
					mResults.SelectedIndex = hoverIndex;
				}
				else
				{
					// Avoid the control scrolling
					mResults.BeginUpdate();
					var topIndex = mResults.TopIndex;
					mResults.SelectedIndex = hoverIndex;
					mResults.TopIndex = topIndex;
					mResults.EndUpdate();
				}
			}
		}
		#endregion

		#region Resizing
		protected override void OnResizeBegin(EventArgs e)
		{
			// Stop automatically sizing - the user is picking a size they want.
			mManualSizeApplied = true;
			base.OnResizeBegin(e);
		}

		protected override void OnResize(EventArgs e)
		{
			base.OnResize(e);

			UpdateBanner();

			mResults.Invalidate();
		}

		protected override void OnResizeEnd(EventArgs e)
		{
			base.OnResizeEnd(e);

			if (Height > MinimumSize.Height && Height != mMaximumExpandHeight)
			{
				mMaximumExpandHeight = Math.Max(Height, MinimumSize.Height + mResults.ItemHeight);
			}
			else
			{
				mManualSizeApplied = false;
			}

			Settings.Default.WindowPosition = new Rectangle(Left, Top, Width, mMaximumExpandHeight);
		}

		private void UpdateBanner()
		{
			if (mBannerImage != null)
			{
				BannerFactory.UpdateBanner(this, mBanner, mBannerImage, PwDefs.ProductName, Resources.BannerText, ref mBannerWidth);
			}
		}

		private void mSearch_LocationChanged(object sender, EventArgs e)
		{
			mThrobber.Location = new Point(mSearch.Right - mThrobber.Width - mThrobber.Margin.Right, mSearch.Top + (mSearch.Height - mThrobber.Height) / 2);
		}

		private void mResults_LocationChanged(object sender, EventArgs e)
		{
			mNoResultsLabel.Top = mResults.Top + (mResults.ItemHeight - mNoResultsLabel.Height) / 2;
		}
		#endregion

		#region Searching
		private static readonly SearchResultPrecedence SearchResultPrecedenceComparer = new SearchResultPrecedence();
		private void mSearch_TextChanged(object sender, EventArgs e)
		{
			if (mSearch.Text.Length < 2)
			{
				// Stop searching
				mResultsUpdater.Enabled = false;
				ShowThrobber = false;
				Height = MinimumSize.Height;
				mManualSizeApplied = false;
				mResults.Items.Clear();
				mLastResultsUpdated = null;
				mLastResultsUpdatedNextAvailableIndex = 0;
			}
			else
			{
				// Start searching
				mNoResultsLabel.Visible = false;
				mCurrentSearch = mSearcher.Search(mSearch.Text);
				mResultsUpdater.Enabled = true;
				ShowThrobber = true;
				mResultsUpdater_Tick(null, EventArgs.Empty); // Quick poke just in case the results are already done.
			}
		}

		[SuppressMessage("ReSharper", "CoVariantArrayConversion", Justification = "Object arrays for Listbox.Items, known to be of correct type")]
		private void mResultsUpdater_Tick(object sender, EventArgs e)
		{
			if (mLastResultsUpdated != mCurrentSearch)
			{
				// Clear out old results and replace with new ones
				mResults.Items.Clear();
				mLastResultsUpdated = mCurrentSearch;
				mLastResultsUpdatedNextAvailableIndex = 0;
			}
			var existingResultsCount = mResults.Items.Count;
			
			bool complete;
			var newResults = mLastResultsUpdated.GetAvailableResults(ref mLastResultsUpdatedNextAvailableIndex, out complete);
			if (newResults.Length > 0)
			{
				mResults.BeginUpdate();
				
				SearchResult[] allResults;
				if (existingResultsCount > 0)
				{
					allResults = new SearchResult[existingResultsCount + newResults.Length];
					mResults.Items.CopyTo(allResults, 0);
					newResults.CopyTo(allResults, existingResultsCount);

					mResults.Items.Clear();
				}
				else
				{
					allResults = newResults;
				}

				CalculateUniqueTitles(allResults);

				Array.Sort(allResults, SearchResultPrecedenceComparer);
				mResults.Items.AddRange(allResults);
				
				mResults.EndUpdate();

				if (allResults.Length > 0)
				{
					if (mResults.SelectedIndex == -1)
					{
						try
						{
							// HACK to work around mono bug
							if (sMonoListBoxTopIndex != null)
							{
								sMonoListBoxTopIndex.SetValue(mResults, 1); // Set the top_index to 1 so that when selected index is set to 0, and calls EnsureVisible(0), it follows the index < top_index pass and not the broken index >= top_index + rows path. 
							}

							mResults.SelectedIndex = 0;
							mResults.TopIndex = 0;
						}
						catch (Exception ex)
						{
							Debug.Fail("Failed to set selection on count of " + allResults.Length + ": " + ex.Message);
						}
					}

					if (!mManualSizeApplied)
					{
						Height = Math.Min(mMaximumExpandHeight, MinimumSize.Height + (allResults.Length * mResults.ItemHeight));
					}
				}
			}

			if (complete)
			{
				ShowThrobber = false;
				mResultsUpdater.Enabled = false;

				if (mResults.Items.Count == 0)
				{
					mNoResultsLabel.Visible = true;
					Height = MinimumSize.Height + mResults.ItemHeight;
					mManualSizeApplied = false;
				}
			}
		}

		private void CalculateUniqueTitles(IEnumerable<SearchResult> results, int depth = 0)
		{
			// Where results have identical titles, include group titles to make them unique
			depth += 1;

			// First create a lookup by title
			var titles = new Dictionary<string, List<SearchResult>>();
			foreach (var searchResult in results)
			{
				List<SearchResult> resultsWithSameTitle;
				if (titles.TryGetValue(searchResult.UniqueTitle, out resultsWithSameTitle))
				{
					resultsWithSameTitle.Add(searchResult);
				}
				else
				{
					titles.Add(searchResult.UniqueTitle, new List<SearchResult> { searchResult });
				}
			}

			// Attempt to unique-ify any non-unique titles
			foreach (var resultsSharingTitle in titles.Values)
			{
				if (resultsSharingTitle.Count > 1)
				{
					var titlesModified = false;
					foreach (var searchResult in resultsSharingTitle)
					{
						titlesModified |= searchResult.SetUniqueTitleDepth(depth);
					}

					if (titlesModified)
					{
						// Recurse in case of continuing non-uniqueness
						CalculateUniqueTitles(resultsSharingTitle, depth);
					}
				}
			}
		}

		private class SearchResultPrecedence : IComparer<SearchResult>
		{
			public int Compare(SearchResult x, SearchResult y)
			{
				// First precedence is that if the result is the start of the field value, it's higher precedence than if it doesn't.
				var result = -(x.Start == 0).CompareTo(y.Start == 0);

				// Second precedence is that the start of the title field is higher precedence than the start of any other field
				if (result == 0)
				{
					result = -(x.FieldName == PwDefs.TitleField).CompareTo(y.FieldName == PwDefs.TitleField);
				}

				// Both start the title field, so both equal. Have to have consistent ordering, so return final precedence based search index
				if (result == 0)
				{
					result = x.ResultIndex.CompareTo(y.ResultIndex);
				}
				
				return result;
			}
		}

		private bool ShowThrobber
		{
			get { return mThrobber.Visible; }
			set
			{
				if (value != ShowThrobber)
				{
					if (value)
					{
						mThrobber.Visible = true;

						// Set the margin on the textbox to allow room for the throbber
						NativeMethods.SetTextBoxRightMargin(mSearch, mThrobber.Width + mThrobber.Margin.Right);
					}
					else
					{
						mThrobber.Visible = false;

						NativeMethods.SetTextBoxRightMargin(mSearch, 0);
					}
				}
			}
		}
		#endregion

		private void mBannerImage_MouseDown(object sender, MouseEventArgs e)
		{
			// Allow drag by banner image
			if (e.Button == MouseButtons.Left)
			{
				if (e.Clicks == 2)
				{
					// Re-center the form on double-click
					CenterToScreen();

					Settings.Default.WindowPosition = new Rectangle(Left, Top, Width, mMaximumExpandHeight);
				}
				else if (!NativeLib.IsUnix())
				{
					NativeMethods.StartFormDrag(this);
				}
			}
		}

		protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
		{
			switch (keyData)
			{
				case Keys.Escape:
					Close();
					return true;
				case Keys.Up:
					TryChangeSelection(-1);
					return true;
				case Keys.Down:
					TryChangeSelection(1);
					return true;
				case Keys.PageUp:
					TryChangeSelection(-mResults.ClientSize.Height / mResults.ItemHeight);
					return true;
				case Keys.PageDown:
					TryChangeSelection(mResults.ClientSize.Height / mResults.ItemHeight);
					return true;
				case Keys.Home | Keys.Control:
					mResults.SelectedIndex = 0;
					return true;
				case Keys.End | Keys.Control:
					mResults.SelectedIndex = mResults.Items.Count - 1;
					return true;
				case Keys.Enter:
					PerformAction(Settings.Default.DefaultAction, mResults.SelectedItem as SearchResult);
					break;
				case Keys.Enter | Keys.Shift:
					PerformAction(Settings.Default.AlternativeAction, mResults.SelectedItem as SearchResult);
					break;
			}
			
			return base.ProcessCmdKey(ref msg, keyData);
		}

		#region Selection Changing

		protected override void OnMouseWheel(MouseEventArgs e)
		{
			mResults.TopIndex -= (e.Delta / Math.Abs(e.Delta));
		}

		private void TryChangeSelection(int delta)
		{
			if (mResults.Items.Count > 0)
			{
				mResults.SelectedIndex = Math.Max(Math.Min(mResults.Items.Count - 1, mResults.SelectedIndex + delta), 0);
			}
		}
		#endregion

		#region Actions

		private void mResults_MouseClick(object sender, MouseEventArgs e)
		{
			var clickIndex = mResults.IndexFromPoint(e.X, e.Y);
			if (clickIndex >= 0)
			{
				var clickedResult = mResults.Items[clickIndex] as SearchResult;
				if (clickedResult != null)
				{
					PerformAction((ModifierKeys & Keys.Shift) == Keys.Shift ? Settings.Default.AlternativeAction : Settings.Default.DefaultAction, clickedResult);
				}
			}
		}

		private void PerformAction(Actions action, SearchResult searchResult)
		{
			string seq = mSeq.Text.Length == 0 ? null : mSeq.Text;

			Close();

			if (searchResult != null)
			{
				switch (action)
				{
					case Actions.PerformAutoType:
						AutoTypeEntry(searchResult, seq);
						break;
					case Actions.EditEntry:
						EditEntry(searchResult);
						break;
					case Actions.ShowEntry:
						ShowEntry(searchResult);
						break;
					case Actions.OpenEntryUrl:
						OpenEntryUrl(searchResult);
						break;
					case Actions.CopyPassword:
						CopyPassword(searchResult);
						break;
					default:
						throw new ArgumentOutOfRangeException("action");
				}
			}
		}


		private void AutoTypeEntry(SearchResult searchResult, string seq)
		{
			bool result;
			if (ActiveForm != null)
			{
				result = AutoType.PerformIntoPreviousWindow(mMainForm, searchResult.Entry, searchResult.Database, seq);
			}
			else
			{
				result = AutoType.PerformIntoCurrentWindow(searchResult.Entry, searchResult.Database, seq);
			}
			if (!result)
			{
				SystemSounds.Beep.Play();

				if (Settings.Default.AlternativeAction != Actions.PerformAutoType)
				{
					PerformAction(Settings.Default.AlternativeAction, searchResult);
				}
			}
		}

		private void EditEntry(SearchResult searchResult)
		{
			using (var entryForm = new PwEntryForm())
			{
				mMainForm.MakeDocumentActive(mMainForm.DocumentManager.FindDocument(searchResult.Database));
				
				entryForm.InitEx(searchResult.Entry, PwEditMode.EditExistingEntry, searchResult.Database, mMainForm.ClientIcons, false, false);

				ShowForegroundDialog(entryForm);

				mMainForm.UpdateUI(false, null, searchResult.Database.UINeedsIconUpdate, null, true, null, entryForm.HasModifiedEntry);
			}
		}

// ReSharper disable once UnusedMethodReturnValue.Local - Generic helper, result may be used in future
		private DialogResult ShowForegroundDialog(Form form)
		{
			mMainForm.EnsureVisibleForegroundWindow(false, false);
			form.StartPosition = FormStartPosition.CenterScreen;
			if (mMainForm.IsTrayed())
			{
				form.ShowInTaskbar = true;
			}

			form.Shown += ActivateFormOnShown;
			return form.ShowDialog(mMainForm);
		}

		private static void ActivateFormOnShown(object sender, EventArgs eventArgs)
		{
			var form = (Form)sender;
			form.Shown -= ActivateFormOnShown;
			form.Activate();
		}

		private void ShowEntry(SearchResult searchResult)
		{
			// Show this entry
			mMainForm.UpdateUI(false, mMainForm.DocumentManager.FindDocument(searchResult.Database), true, searchResult.Entry.ParentGroup, true, null, false, null);
			mMainForm.SelectEntries(new PwObjectList<PwEntry> { searchResult.Entry }, true, true);
			mMainForm.EnsureVisibleEntry(searchResult.Entry.Uuid);
			mMainForm.UpdateUI(false, null, false, null, false, null, false);
			mMainForm.EnsureVisibleForegroundWindow(true, true);
		}

		private void OpenEntryUrl(SearchResult searchResult)
		{
			WinUtil.OpenEntryUrl(searchResult.Entry);
		}

		private void CopyPassword(SearchResult searchResult)
		{
			if (ClipboardUtil.Copy(searchResult.Entry.Strings.ReadSafe(PwDefs.PasswordField), true, true, searchResult.Entry,
									mMainForm.DocumentManager.SafeFindContainerOf(searchResult.Entry),
									IntPtr.Zero))
			{
				mMainForm.StartClipboardCountdown();
			}
		}
		
		#endregion
	}
}