diff --git a/src/DataModule.pas b/src/DataModule.pas new file mode 100644 index 0000000..512a58c --- /dev/null +++ b/src/DataModule.pas @@ -0,0 +1,386 @@ +{ + This file is part of the NppUISpy plugin for Notepad++ + Author: Andreas Heim + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License version 3 as published + by the Free Software Foundation. + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +} + +unit DataModule; + + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.StrUtils, System.DateUtils, + System.IOUtils, System.Math, System.Types, System.Classes, System.Generics.Collections, + System.Generics.Defaults, System.TypInfo, System.IniFiles, + + NppSupport, NppMenuCmdID, NppPlugin; + + +type + TSearchFocus = (sfMenuItemTree, sfToolbarButtonTree); + + TSearchFocusHelper = record + class function ToString(Value: TSearchFocus): string; static; + class function FromString(const Value: string; Default: TSearchFocus): TSearchFocus; static; + end; + + TSearchType = (stMenuItem, stCommandId); + + TSearchTypeHelper = record + class function ToString(Value: TSearchType): string; static; + class function FromString(const Value: string; Default: TSearchType): TSearchType; static; + end; + + + // Abstraction of the settings file + TSettings = class(TObject) + strict private + FIniFile: TIniFile; + FValid: boolean; + FSearchFocus: TSearchFocus; + FSearchType: TSearchType; + FWrapAround: boolean; + FSearchHistory: TStringList; + + class function GetFilePath: string; static; + + procedure SetSearchHistory(Idx: integer; const Value: string); + function GetSearchHistory(Idx: integer): string; + + function GetSearchHistoryLength: integer; + + procedure LoadSettings; + procedure SaveSettings; + + public + constructor Create(const AFilePath: string); + destructor Destroy; override; + + // Class properties + class property FilePath: string read GetFilePath; + + // Common properties + property Valid: boolean read FValid; + property SearchFocus: TSearchFocus read FSearchFocus write FSearchFocus; + property SearchType: TSearchType read FSearchType write FSearchType; + property WrapAround: boolean read FWrapAround write FWrapAround; + property SearchHistory[Idx: integer]: string read GetSearchHistory write SetSearchHistory; + property SearchHistoryLength: integer read GetSearchHistoryLength; + + end; + + + +implementation + +uses + Main; + + +const + // Data for INI file section "Header" + SECTION_HEADER: string = 'Header'; + KEY_VERSION: string = 'Version'; + VALUE_VERSION: string = '1.0'; + + // Data for INI file section "Settings" + SECTION_SETTINGS: string = 'Settings'; + KEY_SETTINGS_SEARCH_FOCUS_NAME: string = 'SearchFocus'; + KEY_SETTINGS_SEARCH_TYPE_NAME: string = 'SearchType'; + KEY_SETTINGS_WRAP_AROUND_NAME: string = 'WrapAround'; + + // Data for INI file section "SearchHistory" + SECTION_SEARCH_HISTORY: string = 'SearchHistory'; + KEY_SEARCH_HISTORY_ITEM_NAME: string = 'Item'; + + +const + MAX_SEARCH_HISTORY_LENGTH = 20; + + +// ============================================================================= +// Class TSettings +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Create / Destroy +// ----------------------------------------------------------------------------- + +constructor TSettings.Create(const AFilePath: string); +begin + inherited Create; + + FValid := false; + FIniFile := TIniFile.Create(AFilePath); + + FSearchHistory := TStringList.Create; + FSearchHistory.Sorted := false; + FSearchHistory.CaseSensitive := false; + FSearchHistory.Duplicates := dupIgnore; + FSearchHistory.StrictDelimiter := true; + FSearchHistory.Delimiter := ';'; + + LoadSettings; +end; + + +destructor TSettings.Destroy; +begin + // Settings are saved to disk at instance destruction + SaveSettings; + + FSearchHistory.Free; + FIniFile.Free; + + inherited; +end; + + +// ----------------------------------------------------------------------------- +// Getter / Setter +// ----------------------------------------------------------------------------- + +// Get path of settings file +class function TSettings.GetFilePath: string; +begin + Result := TPath.Combine(Plugin.GetPluginConfigDir, ReplaceStr(Plugin.GetName, ' ', '') + '.ini'); +end; + + +// Store search term in search history list +procedure TSettings.SetSearchHistory(Idx: integer; const Value: string); +var + Cnt: integer; + FoundIdx: integer; + +begin + // Check if search term is already part of search history + FoundIdx := -1; + + for Cnt := 0 to Pred(FSearchHistory.Count) do + begin + if SameText(FSearchHistory.ValueFromIndex[Cnt], Value) then + begin + FoundIdx := Cnt; + break; + end; + end; + + // If search term is not part of search history, push existing items of search + // history towards end of list and add new search term to beginning of list. + // If search history exceeds its maximum length, the oldest item will be lost. + if FoundIdx = -1 then + begin + // If search history is empty, add new item + if FSearchHistory.Count = 0 then + FSearchHistory.Add(Format('%s%d=', [KEY_SEARCH_HISTORY_ITEM_NAME, 0])) + + // If search history is not empty, append new item and push existing items + // towards end of list + else + for Cnt := Min(FSearchHistory.Count, Pred(MAX_SEARCH_HISTORY_LENGTH)) downto 1 do + begin + if Cnt = FSearchHistory.Count then + FSearchHistory.Add(Format('%s%d=', [KEY_SEARCH_HISTORY_ITEM_NAME, Cnt])); + + FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Cnt])] := FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Pred(Cnt)])]; + end; + + // Store search term at beginning of search history + FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, 0])] := Value; + end + + // If search term is already part of search history, ... + else + begin + // ... push preceding items towards end of list ... + for Cnt := FoundIdx downto 1 do + FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Cnt])] := FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Pred(Cnt)])]; + + // ... and store search term at beginning of list + FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, 0])] := Value; + end; +end; + + +function TSettings.GetSearchHistory(Idx: integer): string; +begin + Idx := EnsureRange(Idx, 0, Min(Pred(FSearchHistory.Count), Pred(MAX_SEARCH_HISTORY_LENGTH))); + Result := FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Idx])]; +end; + + +function TSettings.GetSearchHistoryLength: integer; +begin + Result := FSearchHistory.Count; +end; + + +// ----------------------------------------------------------------------------- +// I/O methods +// ----------------------------------------------------------------------------- + +// Parse settings file and store its content in a data model +procedure TSettings.LoadSettings; +var + Header: TStringList; + Settings: TStringList; + Cnt: integer; + +begin + Header := TStringList.Create; + Header.Sorted := false; + Header.CaseSensitive := false; + Header.Duplicates := dupIgnore; + Header.StrictDelimiter := true; + Header.Delimiter := ';'; + + try + // Skip header checking if the settings file doesn't exist + if FileExists(FIniFile.FileName) then + begin + // In future versions of the plugin here we could call an update function + // for the settings file of older plugin versions + FIniFile.ReadSectionValues(SECTION_HEADER, Header); + if not SameText(Header.Values[KEY_VERSION], VALUE_VERSION) then exit; + end; + + Settings := TStringList.Create; + Settings.Sorted := false; + Settings.CaseSensitive := false; + Settings.Duplicates := dupIgnore; + Settings.StrictDelimiter := true; + Settings.Delimiter := ';'; + + try + // Retrieve settings data... + FIniFile.ReadSectionValues(SECTION_SETTINGS, Settings); + + // ...and transfer it to the datamodel + if Settings.IndexOfName(KEY_SETTINGS_SEARCH_FOCUS_NAME) >= 0 then + FSearchFocus := TSearchFocusHelper.FromString(Settings.Values[KEY_SETTINGS_SEARCH_FOCUS_NAME], sfMenuItemTree) + else + FSearchFocus := sfMenuItemTree; + + if Settings.IndexOfName(KEY_SETTINGS_SEARCH_TYPE_NAME) >= 0 then + FSearchType := TSearchTypeHelper.FromString(Settings.Values[KEY_SETTINGS_SEARCH_TYPE_NAME], stMenuItem) + else + FSearchType := stMenuItem; + + if Settings.IndexOfName(KEY_SETTINGS_WRAP_AROUND_NAME) >= 0 then + FWrapAround := StrToBoolDef(Settings.Values[KEY_SETTINGS_WRAP_AROUND_NAME], false) + else + FWrapAround := false; + + // Retrieve search history data + FIniFile.ReadSectionValues(SECTION_SEARCH_HISTORY, FSearchHistory); + + for Cnt := Pred(FSearchHistory.Count) downto 0 do + begin + if FSearchHistory.Count <= MAX_SEARCH_HISTORY_LENGTH then break; + FSearchHistory.Delete(Cnt); + end; + + // If we reached this point we can mark settings as valid + FValid := true; + + finally + Settings.Free; + end; + + finally + Header.Free; + end; +end; + + +// Save settings data model to a disk file +procedure TSettings.SaveSettings; +var + Settings: TStringList; + Cnt: integer; + +begin + if not FValid then exit; + + // Clear whole settings file + Settings := TStringList.Create; + + try + FIniFile.ReadSections(Settings); + + for Cnt := 0 to Pred(Settings.Count) do + FIniFile.EraseSection(Settings[Cnt]); + + finally + Settings.Free; + end; + + // Write Header + FIniFile.WriteString(SECTION_HEADER, KEY_VERSION, VALUE_VERSION); + + // Write settings data + FIniFile.WriteString(SECTION_SETTINGS, KEY_SETTINGS_SEARCH_FOCUS_NAME, TSearchFocusHelper.ToString(FSearchFocus)); + FIniFile.WriteString(SECTION_SETTINGS, KEY_SETTINGS_SEARCH_TYPE_NAME, TSearchTypeHelper.ToString(FSearchType)); + FIniFile.WriteString(SECTION_SETTINGS, KEY_SETTINGS_WRAP_AROUND_NAME, BoolToStr(FWrapAround, true)); + + // Write search history + for Cnt := 0 to Min(Pred(FSearchHistory.Count), Pred(MAX_SEARCH_HISTORY_LENGTH)) do + FIniFile.WriteString(SECTION_SEARCH_HISTORY, Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Cnt]), FSearchHistory.Values[Format('%s%d', [KEY_SEARCH_HISTORY_ITEM_NAME, Cnt])]); +end; + + + +// ============================================================================= +// TSearchFocusHelper +// ============================================================================= + +class function TSearchFocusHelper.FromString(const Value: string; Default: TSearchFocus): TSearchFocus; +begin + Result := TSearchFocus(GetEnumValue(TypeInfo(TSearchFocus), Value)); + + if not (Result in [Low(TSearchFocus)..High(TSearchFocus)]) then + Result := Default; +end; + + +class function TSearchFocusHelper.ToString(Value: TSearchFocus): string; +begin + Result := GetEnumName(TypeInfo(TSearchFocus), Ord(Value)); +end; + + + +// ============================================================================= +// TSearchTypeHelper +// ============================================================================= + +class function TSearchTypeHelper.FromString(const Value: string; Default: TSearchType): TSearchType; +begin + Result := TSearchType(GetEnumValue(TypeInfo(TSearchType), Value)); + + if not (Result in [Low(TSearchType)..High(TSearchType)]) then + Result := Default; +end; + + +class function TSearchTypeHelper.ToString(Value: TSearchType): string; +begin + Result := GetEnumName(TypeInfo(TSearchType), Ord(Value)); +end; + + +end. diff --git a/src/dialog_TfrmSpy.dfm b/src/dialog_TfrmSpy.dfm index c2aed39..bb6077d 100644 --- a/src/dialog_TfrmSpy.dfm +++ b/src/dialog_TfrmSpy.dfm @@ -1,9 +1,10 @@ object frmSpy: TfrmSpy Left = 525 Top = 410 + ActiveControl = cbxSearchText BorderIcons = [biSystemMenu] BorderStyle = bsDialog - ClientHeight = 344 + ClientHeight = 357 ClientWidth = 785 Color = clBtnFace Constraints.MinHeight = 380 @@ -13,16 +14,27 @@ object frmSpy: TfrmSpy Font.Height = -11 Font.Name = 'Tahoma' Font.Style = [] + KeyPreview = True OldCreateOrder = False PopupMode = pmAuto OnCreate = FormCreate OnDestroy = FormDestroy + OnKeyPress = FormKeyPress OnShow = FormShow DesignSize = ( 785 - 344) + 357) PixelsPerInch = 96 TextHeight = 13 + object lblSearchFor: TLabel + Left = 228 + Top = 332 + Width = 54 + Height = 13 + Anchors = [akLeft, akBottom] + Caption = 'Search for:' + ExplicitTop = 319 + end object vstMenuItems: TVirtualStringTree Left = 8 Top = 8 @@ -45,7 +57,6 @@ object frmSpy: TfrmSpy PopupMenu = mnuItemContextMenu ShowHint = True TabOrder = 0 - TabStop = False TreeOptions.AutoOptions = [toAutoDropExpand, toAutoScrollOnExpand, toAutoTristateTracking, toAutoDeleteMovedNodes, toAutoChangeScale] TreeOptions.PaintOptions = [toHideFocusRect, toShowButtons, toShowDropmark, toShowHorzGridLines, toShowRoot, toShowTreeLines, toShowVertGridLines, toThemeAware, toUseBlendedImages, toFullVertGridLines, toAlwaysHideSelection] TreeOptions.SelectionOptions = [toFullRowSelect, toRightClickSelect] @@ -54,7 +65,9 @@ object frmSpy: TfrmSpy OnPaintText = vstMenuItemsPaintText OnGetImageIndex = vstMenuItemsGetImageIndex OnGetHint = vstMenuItemsGetHint + OnHeaderClick = vstMenuItemsHeaderClick OnIncrementalSearch = vstMenuItemsIncrementalSearch + OnKeyPress = vstMenuItemsKeyPress OnMouseDown = vstMenuItemsMouseDown OnNodeClick = vstMenuItemsNodeClick Columns = < @@ -71,41 +84,41 @@ object frmSpy: TfrmSpy MinWidth = 80 Options = [coEnabled, coParentBidiMode, coParentColor, coResizable, coShowDropMark, coVisible, coAutoSpring, coSmartResize, coAllowFocus] Position = 1 - Width = 241 + Width = 234 WideText = 'Menu Items' end item MinWidth = 30 Options = [coEnabled, coParentBidiMode, coParentColor, coResizable, coShowDropMark, coVisible, coAutoSpring, coSmartResize, coAllowFocus] Position = 2 - Width = 76 + Width = 69 WideText = 'Command Id' end> end object btnExpand: TButton - Left = 208 - Top = 286 - Width = 25 - Height = 25 + Left = 170 + Top = 285 + Width = 22 + Height = 22 Hint = 'Expand all' - Anchors = [akRight, akBottom] - Caption = '-' + Anchors = [akLeft, akBottom] + Caption = '&-' ParentShowHint = False ShowHint = True - TabOrder = 2 + TabOrder = 5 OnClick = btnExpandClick end object btnCollapse: TButton - Left = 168 - Top = 286 - Width = 25 - Height = 25 + Left = 142 + Top = 285 + Width = 22 + Height = 22 Hint = 'Collapse all' - Anchors = [akRight, akBottom] - Caption = '+' + Anchors = [akLeft, akBottom] + Caption = '&+' ParentShowHint = False ShowHint = True - TabOrder = 1 + TabOrder = 4 OnClick = btnCollapseClick end object vstToolbarButtons: TVirtualStringTree @@ -129,8 +142,7 @@ object frmSpy: TfrmSpy ParentShowHint = False PopupMenu = mnuItemContextMenu ShowHint = True - TabOrder = 3 - TabStop = False + TabOrder = 1 TreeOptions.AutoOptions = [toAutoDropExpand, toAutoScrollOnExpand, toAutoTristateTracking, toAutoDeleteMovedNodes, toAutoChangeScale] TreeOptions.MiscOptions = [toFullRepaintOnResize, toInitOnSave, toToggleOnDblClick, toWheelPanning, toEditOnClick] TreeOptions.PaintOptions = [toHideFocusRect, toShowButtons, toShowDropmark, toShowHorzGridLines, toShowRoot, toShowVertGridLines, toThemeAware, toUseBlendedImages, toFullVertGridLines, toAlwaysHideSelection] @@ -140,7 +152,9 @@ object frmSpy: TfrmSpy OnPaintText = vstToolbarButtonsPaintText OnGetImageIndex = vstToolbarButtonsGetImageIndex OnGetHint = vstToolbarButtonsGetHint + OnHeaderClick = vstToolbarButtonsHeaderClick OnIncrementalSearch = vstToolbarButtonsIncrementalSearch + OnKeyPress = vstToolbarButtonsKeyPress OnMouseDown = vstToolbarButtonsMouseDown OnNodeClick = vstToolbarButtonsNodeClick Columns = < @@ -170,34 +184,140 @@ object frmSpy: TfrmSpy end object btnQuit: TButton Left = 697 - Top = 311 + Top = 324 Width = 80 Height = 25 Anchors = [akRight, akBottom] - Caption = 'Close' - TabOrder = 5 + Caption = 'Cl&ose' + TabOrder = 13 OnClick = btnQuitClick end object btnReloadData: TButton Left = 598 - Top = 311 + Top = 324 Width = 80 Height = 25 Anchors = [akRight, akBottom] - Caption = 'Reload' - Default = True - TabOrder = 4 + Caption = '&Reload' + TabOrder = 12 OnClick = btnReloadDataClick end + object btnSearchBackwards: TButton + Left = 396 + Top = 304 + Width = 22 + Height = 22 + Hint = 'Search backwards' + Anchors = [akRight, akBottom] + Caption = '&<' + ParentShowHint = False + ShowHint = True + TabOrder = 7 + OnClick = btnSearchClick + end + object btnSearchForwards: TButton + Left = 424 + Top = 304 + Width = 22 + Height = 22 + Hint = 'Search forwards' + Anchors = [akRight, akBottom] + Caption = '&>' + ParentShowHint = False + ShowHint = True + TabOrder = 8 + OnClick = btnSearchClick + end + object rbtSearchForMenuItem: TRadioButton + Left = 292 + Top = 331 + Width = 73 + Height = 17 + Anchors = [akLeft, akBottom] + Caption = '&Menu item' + Checked = True + TabOrder = 10 + TabStop = True + OnClick = rbtSearchForClick + end + object rbtSearchForCommandId: TRadioButton + Left = 370 + Top = 331 + Width = 85 + Height = 17 + Anchors = [akLeft, akBottom] + Caption = '&Command id' + TabOrder = 11 + TabStop = True + OnClick = rbtSearchForClick + end + object chkWrapAround: TCheckBox + Left = 458 + Top = 306 + Width = 83 + Height = 17 + Hint = + 'When search reaches start/end of list,'#13#10'continue from other end ' + + 'of list' + Anchors = [akRight, akBottom] + Caption = '&Wrap around' + ParentShowHint = False + ShowHint = True + TabOrder = 9 + OnClick = chkWrapAroundClick + end + object chkSearchSelectMenuItems: TCheckBox + Left = 8 + Top = 287 + Width = 97 + Height = 17 + Hint = 'Search in menu items tree' + Anchors = [akLeft, akBottom] + Caption = '&Select for search' + ParentShowHint = False + ShowHint = True + TabOrder = 2 + OnClick = chkSearchSelectClick + end + object chkSearchSelectToolbarButtons: TCheckBox + Left = 680 + Top = 287 + Width = 97 + Height = 17 + Hint = 'Search in toolbar buttons tree' + Alignment = taLeftJustify + Anchors = [akRight, akBottom] + Caption = 'Selec&t for search' + ParentShowHint = False + ShowHint = True + TabOrder = 3 + OnClick = chkSearchSelectClick + end + object cbxSearchText: TComboBox + Left = 228 + Top = 304 + Width = 162 + Height = 21 + Hint = + 'Enter search term (case insensitive)'#13#10'Press ENTER to search forw' + + 'ards'#13#10'Press CTRL+ENTER to search backwards' + AutoComplete = False + Anchors = [akLeft, akRight, akBottom] + ParentShowHint = False + ShowHint = True + TabOrder = 6 + OnChange = cbxSearchTextChange + OnKeyUp = cbxSearchTextKeyUp + end object imlToolbarButtonIcons: TImageList - Left = 64 - Top = 296 + Left = 560 + Top = 216 end object mnuItemContextMenu: TPopupMenu AutoPopup = False OnPopup = mnuItemContextMenuPopup - Left = 296 - Top = 296 + Left = 696 + Top = 216 object mniCopyIcon: TMenuItem Caption = 'Copy Icon' OnClick = mniCopyItemDataClick diff --git a/src/dialog_TfrmSpy.pas b/src/dialog_TfrmSpy.pas index c2ed8d6..06eedac 100644 --- a/src/dialog_TfrmSpy.pas +++ b/src/dialog_TfrmSpy.pas @@ -29,14 +29,14 @@ interface VirtualTrees, - NppPlugin, NppPluginForms; + NppPlugin, NppPluginForms, + + DataModule; type TfrmSpy = class(TNppPluginForm) vstMenuItems: TVirtualStringTree; - btnCollapse: TButton; - btnExpand: TButton; vstToolbarButtons: TVirtualStringTree; imlToolbarButtonIcons: TImageList; @@ -46,11 +46,25 @@ TfrmSpy = class(TNppPluginForm) mniCopyText: TMenuItem; mniCopyCommandId: TMenuItem; + chkSearchSelectMenuItems: TCheckBox; + chkSearchSelectToolbarButtons: TCheckBox; + + btnCollapse: TButton; + btnExpand: TButton; + cbxSearchText: TComboBox; + btnSearchBackwards: TButton; + btnSearchForwards: TButton; + chkWrapAround: TCheckBox; + lblSearchFor: TLabel; + rbtSearchForMenuItem: TRadioButton; + rbtSearchForCommandId: TRadioButton; + btnReloadData: TButton; btnQuit: TButton; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); + procedure FormKeyPress(Sender: TObject; var Key: Char); procedure FormDestroy(Sender: TObject); // ......................................................................... @@ -58,9 +72,13 @@ TfrmSpy = class(TNppPluginForm) procedure vstMenuItemsIncrementalSearch(Sender: TBaseVirtualTree; Node: PVirtualNode; const SearchText: string; var Result: Integer); + procedure vstMenuItemsKeyPress(Sender: TObject; var Key: Char); + procedure vstMenuItemsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); + procedure vstMenuItemsHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); + procedure vstMenuItemsNodeClick(Sender: TBaseVirtualTree; const HitInfo: THitInfo); procedure vstMenuItemsGetImageIndex(Sender: TBaseVirtualTree; @@ -87,9 +105,13 @@ TfrmSpy = class(TNppPluginForm) procedure vstToolbarButtonsIncrementalSearch(Sender: TBaseVirtualTree; Node: PVirtualNode; const SearchText: string; var Result: Integer); + procedure vstToolbarButtonsKeyPress(Sender: TObject; var Key: Char); + procedure vstToolbarButtonsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); + procedure vstToolbarButtonsHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); + procedure vstToolbarButtonsNodeClick(Sender: TBaseVirtualTree; const HitInfo: THitInfo); procedure vstToolbarButtonsGetImageIndex(Sender: TBaseVirtualTree; @@ -116,6 +138,13 @@ TfrmSpy = class(TNppPluginForm) procedure mnuItemContextMenuPopup(Sender: TObject); procedure mniCopyItemDataClick(Sender: TObject); + procedure chkSearchSelectClick(Sender: TObject); + procedure cbxSearchTextChange(Sender: TObject); + procedure cbxSearchTextKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); + procedure btnSearchClick(Sender: TObject); + procedure chkWrapAroundClick(Sender: TObject); + procedure rbtSearchForClick(Sender: TObject); + procedure btnCollapseClick(Sender: TObject); procedure btnExpandClick(Sender: TObject); @@ -164,12 +193,20 @@ TToolbarButtonTreeInfo = class(TObject) end; private + FSettings: TSettings; + FSearchTree: TBaseVirtualTree; + FSearchText: string; + FSearchDirection: TVTSearchDirection; + FMenuItems: TMenuItemTreeInfoList; FToolbarButtons: TToolbarButtonTreeInfoList; FMouseButtonState: TShiftState; FHitInfo: THitInfo; + FInternalChange: boolean; procedure UpdateGUI; + function IterateTree(Tree: TBaseVirtualTree; StartNode: PVirtualNode; Direction: TVTSearchDirection; Callback: TVTGetNodeProc; Data: Pointer): PVirtualNode; + procedure CheckNode(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean); procedure ListMenuItems(AMenu: HMENU = 0; AList: TMenuItemTreeInfoList = nil); procedure FillMenuItemTree(ANode: PVirtualNode = nil; AList: TMenuItemTreeInfoList = nil); @@ -181,6 +218,7 @@ TToolbarButtonTreeInfo = class(TObject) function GetToolbarButtonIdx(CmdId: cardinal): integer; function FindNppToolbar(NppWnd: HWND): HWND; + public constructor Create(NppParent: TNppPlugin); override; destructor Destroy; override; @@ -238,14 +276,56 @@ destructor TfrmSpy.Destroy; // ----------------------------------------------------------------------------- procedure TfrmSpy.FormCreate(Sender: TObject); +var + Cnt: integer; + begin - Caption := TXT_TITLE; + Caption := TXT_TITLE; + // Init trees FMenuItems := TMenuItemTreeInfoList.Create(true); FToolbarButtons := TToolbarButtonTreeInfoList.Create(true); vstMenuItems.NodeDataSize := SizeOf(TMenuItemTreeData); vstToolbarButtons.NodeDataSize := SizeOf(TToolbarButtonTreeData); + + // Load settings file + FSettings := TSettings.Create(TSettings.FilePath); + + // Apply settings to internal variables + case FSettings.SearchFocus of + sfMenuItemTree: FSearchTree := vstMenuItems; + sfToolbarButtonTree: FSearchTree := vstToolbarButtons; + else FSearchTree := vstMenuItems; + end; + + // Init more internal variables + FSearchDirection := sdForward; + FSearchText := ''; + + // Apply settings to GUI + if FSearchTree = vstMenuItems then + chkSearchSelectMenuItems.Checked := true + + else if FSearchTree = vstToolbarButtons then + chkSearchSelectToolbarButtons.Checked := true; + + chkWrapAround.Checked := FSettings.WrapAround; + rbtSearchForMenuItem.Checked := (FSettings.SearchType = stMenuItem); + rbtSearchForCommandId.Checked := (FSettings.SearchType = stCommandId); + + // Apply search history to GUI + FInternalChange := true; + + try + cbxSearchText.Clear; + + for Cnt := 0 to Pred(FSettings.SearchHistoryLength) do + cbxSearchText.Items.Add(FSettings.SearchHistory[Cnt]); + + finally + FInternalChange := false; + end; end; @@ -255,10 +335,31 @@ procedure TfrmSpy.FormShow(Sender: TObject); end; +procedure TfrmSpy.FormKeyPress(Sender: TObject; var Key: Char); +begin + if Key = Chr(VK_ESCAPE) then + Close; +end; + + procedure TfrmSpy.FormDestroy(Sender: TObject); begin + // Write back local variables to settings object + if FSearchTree = vstMenuItems then + FSettings.SearchFocus := sfMenuItemTree + + else if FSearchTree = vstToolbarButtons then + FSettings.SearchFocus := sfToolbarButtonTree + + else + FSettings.SearchFocus := sfMenuItemTree; + + // Free internal lists FMenuItems.Clear; FToolbarButtons.Clear; + + // Free and save settings object + FSettings.Free; end; @@ -281,6 +382,23 @@ procedure TfrmSpy.vstMenuItemsIncrementalSearch(Sender: TBaseVirtualTree; end; +procedure TfrmSpy.vstMenuItemsKeyPress(Sender: TObject; var Key: Char); +begin + if not Assigned(vstMenuItems.FocusedNode) then exit; + + if Key = Chr(VK_RETURN) then + begin + FMouseButtonState := [ssLeft]; + + FHitInfo.HitNode := vstMenuItems.FocusedNode; + FHitInfo.HitPositions := [hiOnItemLabel]; + FHitInfo.HitColumn := COL_MENUITEM_TEXT; + + vstMenuItemsNodeClick(vstMenuItems, FHitInfo); + end; +end; + + procedure TfrmSpy.vstMenuItemsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin @@ -288,6 +406,12 @@ procedure TfrmSpy.vstMenuItemsMouseDown(Sender: TObject; Button: TMouseButton; end; +procedure TfrmSpy.vstMenuItemsHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); +begin + chkSearchSelectClick(chkSearchSelectMenuItems); +end; + + procedure TfrmSpy.vstMenuItemsNodeClick(Sender: TBaseVirtualTree; const HitInfo: THitInfo); var NodeData: PMenuItemTreeData; @@ -440,6 +564,23 @@ procedure TfrmSpy.vstToolbarButtonsIncrementalSearch(Sender: TBaseVirtualTree; end; +procedure TfrmSpy.vstToolbarButtonsKeyPress(Sender: TObject; var Key: Char); +begin + if not Assigned(vstToolbarButtons.FocusedNode) then exit; + + if Key = Chr(VK_RETURN) then + begin + FMouseButtonState := [ssLeft]; + + FHitInfo.HitNode := vstToolbarButtons.FocusedNode; + FHitInfo.HitPositions := [hiOnItemLabel]; + FHitInfo.HitColumn := COL_TOOLBARBUTTON_HINT_TEXT; + + vstToolbarButtonsNodeClick(vstToolbarButtons, FHitInfo); + end; +end; + + procedure TfrmSpy.vstToolbarButtonsMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin @@ -447,6 +588,12 @@ procedure TfrmSpy.vstToolbarButtonsMouseDown(Sender: TObject; Button: TMouseButt end; +procedure TfrmSpy.vstToolbarButtonsHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo); +begin + chkSearchSelectClick(chkSearchSelectToolbarButtons); +end; + + procedure TfrmSpy.vstToolbarButtonsNodeClick(Sender: TBaseVirtualTree; const HitInfo: THitInfo); var NodeData: PToolbarButtonTreeData; @@ -670,6 +817,177 @@ procedure TfrmSpy.mniCopyItemDataClick(Sender: TObject); end; +procedure TfrmSpy.chkSearchSelectClick(Sender: TObject); +begin + if FInternalChange then exit; + + // Determine from clicked checkbox which tree should be searched and + // ensure that only ONE of the two checkboxes is checked + FInternalChange := true; + + try + if Sender = chkSearchSelectMenuItems then + begin + chkSearchSelectMenuItems.Checked := true; + chkSearchSelectToolbarButtons.Checked := false; + FSearchTree := vstMenuItems; + end + + else if Sender = chkSearchSelectToolbarButtons then + begin + chkSearchSelectMenuItems.Checked := false; + chkSearchSelectToolbarButtons.Checked := true; + FSearchTree := vstToolbarButtons; + end; + + finally + FInternalChange := false; + end; +end; + + +procedure TfrmSpy.cbxSearchTextChange(Sender: TObject); +var + CursPos: integer; + +begin + if FInternalChange then exit; + + // Set color of search term to default value and + // retrieve current search term + FSearchText := cbxSearchText.Text; + CursPos := cbxSearchText.SelStart + cbxSearchText.SelLength; + cbxSearchText.Font.Color := clWindowText; + cbxSearchText.SelStart := CursPos; + cbxSearchText.SelLength := 0; +end; + + +procedure TfrmSpy.cbxSearchTextKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState); +begin + if Key = VK_RETURN then + begin + if not (ssCtrl in Shift) then + // Pressing ENTER when combobox with search term has input focus + // performs a forward search + btnSearchClick(btnSearchForwards) + else + // Pressing CTRL+ENTER when combobox with search term has input focus + // performs a backward search + btnSearchClick(btnSearchBackwards); + end; +end; + + +procedure TfrmSpy.chkWrapAroundClick(Sender: TObject); +begin + FSettings.WrapAround := chkWrapAround.Checked; +end; + + +procedure TfrmSpy.btnSearchClick(Sender: TObject); +var + Node: PVirtualNode; + Cnt: integer; + CursPos: integer; + +begin + // Ignore empty search term + if FSearchText = '' then exit; + + // Determine search direction from clicked button + if Sender = btnSearchBackwards then + FSearchDirection := sdBackward + + else if Sender = btnSearchForwards then + FSearchDirection := sdForward + + else + exit; + + // Store search term in search history + FInternalChange := true; + + try + // This does all the nitty-gritty details like limiting maximum lenght of + // search history, pushing older items to the end of the history list and + // pushing already existing items to the beginning of the list + FSettings.SearchHistory[0] := FSearchText; + + // Mirror search history to GUI + cbxSearchText.Clear; + + for Cnt := 0 to Pred(FSettings.SearchHistoryLength) do + cbxSearchText.Items.Add(FSettings.SearchHistory[Cnt]); + + // Set search term as selected item of combobox again + cbxSearchText.ItemIndex := cbxSearchText.Items.IndexOf(FSearchText); + + finally + FInternalChange := false; + end; + + // Search for search term + if not Assigned(FSearchTree.FocusedNode) then + // If there is no selected item in the tree, search from first item onwards + Node := IterateTree(FSearchTree, FSearchTree.GetFirst, FSearchDirection, CheckNode, PChar(FSearchText)) + else + begin + // Search from succesor of currently selected item onwards. + // Depends on choosen search direction. + case FSearchDirection of + sdForward: Node := IterateTree(FSearchTree, FSearchTree.GetNext(FSearchTree.FocusedNode), FSearchDirection, CheckNode, PChar(FSearchText)); + sdBackward: Node := IterateTree(FSearchTree, FSearchTree.GetPrevious(FSearchTree.FocusedNode), FSearchDirection, CheckNode, PChar(FSearchText)); + else Node := nil; + end; + + // If search has reached first/last element of the tree (depending on choosen + // search direction) without finding a match and "Wrap around" is ticked, + // search again from the opposite end of the tree + if not Assigned(Node) and FSettings.WrapAround then + begin + case FSearchDirection of + sdForward: Node := IterateTree(FSearchTree, FSearchTree.GetFirst, FSearchDirection, CheckNode, PChar(FSearchText)); + sdBackward: Node := IterateTree(FSearchTree, FSearchTree.GetLast, FSearchDirection, CheckNode, PChar(FSearchText)); + else Node := nil; + end; + end; + end; + + if not Assigned(Node) then + begin + // If the search didn't match, change color of search term to red + CursPos := cbxSearchText.SelStart + cbxSearchText.SelLength; + cbxSearchText.Font.Color := clRed; + cbxSearchText.SelStart := CursPos; + cbxSearchText.SelLength := 0; + end + else + begin + // If the search matched, set color of search term to default value ... + CursPos := cbxSearchText.SelStart + cbxSearchText.SelLength; + cbxSearchText.Font.Color := clWindowText; + cbxSearchText.SelStart := CursPos; + cbxSearchText.SelLength := 0; + + // ... and select and focus matching tree node + FSearchTree.Selected[Node] := true; + FSearchTree.FocusedNode := Node; + end; +end; + + +procedure TfrmSpy.rbtSearchForClick(Sender: TObject); +begin + // Determine search type from clicked radio button + if Sender = rbtSearchForMenuItem then + FSettings.SearchType := stMenuItem + + else if Sender = rbtSearchForCommandId then + FSettings.SearchType := stCommandId; +end; + + procedure TfrmSpy.btnCollapseClick(Sender: TObject); begin vstMenuItems.FullCollapse(); @@ -718,6 +1036,98 @@ procedure TfrmSpy.UpdateGUI; end; +function TfrmSpy.IterateTree(Tree: TBaseVirtualTree; StartNode: PVirtualNode; Direction: TVTSearchDirection; Callback: TVTGetNodeProc; Data: Pointer): PVirtualNode; +var + CurNode: PVirtualNode; + TmpNode: PVirtualNode; + StopIteration: boolean; + +begin + Result := nil; + StopIteration := false; + + CurNode := StartNode; + + while Assigned(CurNode) do + begin + Callback(Tree, CurNode, Data, StopIteration); + if StopIteration then exit(CurNode); + + if Tree.HasChildren[CurNode] then + begin + case Direction of + sdForward: TmpNode := IterateTree(Tree, Tree.GetNext(CurNode), Direction, Callback, Data); + sdBackward: TmpNode := IterateTree(Tree, Tree.GetPrevious(CurNode), Direction, Callback, Data); + else TmpNode := nil; + end; + + if Assigned(TmpNode) then + exit(TmpNode); + + CurNode := TmpNode; + end + else + begin + case Direction of + sdForward: CurNode := Tree.GetNext(CurNode); + sdBackward: CurNode := Tree.GetPrevious(CurNode); + else CurNode := nil; + end; + end; + end; +end; + + +procedure TfrmSpy.CheckNode(Sender: TBaseVirtualTree; Node: PVirtualNode; Data: Pointer; var Abort: Boolean); +var + SearchText: string; + CmdId: integer; + CmdIdValid: boolean; + MenuItemNodeData: PMenuItemTreeData; + ToolbarButtonNodeData: PToolbarButtonTreeData; + +begin + SearchText := PChar(Data); + CmdIdValid := TryStrToInt(SearchText, CmdId); + + if Sender = vstMenuItems then + begin + MenuItemNodeData := PMenuItemTreeData(Sender.GetNodeData(Node)); + + case FSettings.SearchType of + stMenuItem: + begin + Abort := ContainsText(MenuItemNodeData.NppMenuItem.Text, SearchText); + end; + + stCommandId: + begin + if CmdIdValid then + Abort := (MenuItemNodeData.NppMenuItem.CmdId = cardinal(CmdId)); + end; + end; + end + + else if Sender = vstToolbarButtons then + begin + ToolbarButtonNodeData := PToolbarButtonTreeData(Sender.GetNodeData(Node)); + + case FSettings.SearchType of + stMenuItem: + begin + Abort := ContainsText(ToolbarButtonNodeData.NppToolbarButton.HintText, SearchText); + end; + + stCommandId: + begin + if CmdIdValid then + Abort := (ToolbarButtonNodeData.NppToolbarButton.CmdId = cardinal(CmdId)); + end; + end; + end; +end; + + // ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // Menu items tree // ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' @@ -847,6 +1257,10 @@ procedure TfrmSpy.FillMenuItemTree(ANode: PVirtualNode = nil; AList: TMenuItemTr begin vstMenuItems.EndUpdate; vstMenuItems.Refresh; + + Node := vstMenuItems.GetFirst; + vstMenuItems.Selected[Node] := true; + vstMenuItems.FocusedNode := Node; end; end end; @@ -1077,6 +1491,10 @@ procedure TfrmSpy.FillToolbarButtonTree(); finally vstToolbarButtons.EndUpdate; vstToolbarButtons.Refresh; + + Node := vstToolbarButtons.GetFirst; + vstToolbarButtons.Selected[Node] := true; + vstToolbarButtons.FocusedNode := Node; end end;