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
}
}