doublecmd/src/uhotkeymanager.pas

1343 lines
36 KiB
ObjectPascal

{
Double Commander
-------------------------------------------------------------------------
HotKey Manager.
Allow to set it's own bindings to each TWinControl on form.
Copyright (C) 2008 Dmitry Kolomiets (B4rr4cuda@rambler.ru)
Copyright (C) 2011-2012 Przemyslaw Nagay (cobines@gmail.com)
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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
}
unit uHotkeyManager;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Controls, LCLProc, LCLType, LCLIntf, Forms, ActnList,
DCClassesUtf8, fgl, contnrs, DCXmlConfig, DCBasicTypes;
type
generic THMObjectInstance<InstanceClass> = class
Instance: InstanceClass;
KeyDownProc: TKeyEvent;
end;
THMFormInstance = specialize THMObjectInstance<TCustomForm>;
THMControlInstance = specialize THMObjectInstance<TWinControl>;
{ THotkey }
THotkey = class
Shortcuts: array of String;
Command: String;
Params: array of String;
procedure Assign(Hotkey: THotkey);
function Clone: THotkey;
function HasParam(const aParam: String): Boolean; overload;
function HasParam(const aParams: array of String): Boolean; overload;
function SameAs(Hotkey: THotkey): Boolean;
function SameParams(const aParams: array of String): Boolean;
function SameShortcuts(const aShortcuts: array of String): Boolean;
end;
TBaseHotkeysList = specialize TFPGObjectList<THotkey>;
{ TFreeNotifier }
TFreeNotifier = class(TComponent)
private
FFreeEvent: TNotifyEvent;
protected
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
public
property OnFree: TNotifyEvent read FFreeEvent write FFreeEvent;
end;
THotkeyOperation = (hopAdd, hopRemove, hopClear, hopUpdate);
THotkeyEvent = procedure (hotkey: THotkey; operation: THotkeyOperation) of object;
{ THotkeys }
THotkeys = class(TBaseHotkeysList)
private
FOnChange: THotkeyEvent;
procedure DoOnChange(hotkey: THotkey; operation: THotkeyOperation);
public
constructor Create(AFreeObjects: Boolean = True); reintroduce;
function Add(const Shortcuts, Params: array of String; Command: String): THotkey; overload;
function AddIfNotExists(const Shortcuts, Params: array of String; Command: String): THotkey; overload;
{en
Adds multiple shortcuts to the same command.
@param(ShortcutsWithParams
Array of shortcuts followed by any number of parameters.
Each shortcuts array must end with an empty string,
and similarly each parameters must end with an empty string.
[Shortcut1A, Shortcut1B, '', S1ParamA, '',
Shortcut2, '', S2ParamA, S2ParamB, '', ...])
@param(Command
Command to which the shortcuts should be added.)
@param(OldShortcuts, OldParams
Adds new shortcuts even if old shortcut exists.
If a different shortcuts exists however then doesn't add new one.)
}
procedure AddIfNotExists(const ShortcutsWithParams: array of String; Command: String;
const OldShortcuts, OldParams: array of String); overload;
procedure AddIfNotExists(const ShortcutsWithParams: array of String; Command: String); overload;
procedure AddIfNotExists(Key: Word; Shift: TShiftState;
const Command: String; const Param: String = ''); overload;
procedure Clear; reintroduce;
procedure Remove(var hotkey: THotkey); reintroduce;
function Find(const Shortcuts: array of String): THotkey;
{en
Find hotkey which shortcuts begin with Shortcuts parameter.
If BothWays=@true then also looks for shortcuts which are the beginning
of Shortcuts parameter.
}
function FindByBeginning(const Shortcuts: TDynamicStringArray; BothWays: Boolean): THotkey;
function FindByCommand(Command: String): THotkey;
function FindByContents(Hotkey: THotkey): THotkey;
{en
Should be called whenever a hotkey has shortcut updated to update the
shortcuts in ActionLists.
}
procedure UpdateHotkey(Hotkey: THotkey);
property OnChange: THotkeyEvent read FOnChange write FOnChange;
end;
{ THMBaseObject }
generic THMBaseObject<InstanceClass, InstanceInfoClass> = class
private
FObjects: TFPObjectList;
FHotkeys: THotkeys;
FName: String;
public
constructor Create(AName: String); virtual;
destructor Destroy; override;
function Add(AInstanceInfo: InstanceInfoClass): Integer;
procedure Delete(AInstance: InstanceClass);
function Find(AInstance: InstanceClass): InstanceInfoClass;
property Hotkeys: THotkeys read FHotkeys;
property Name: String read FName;
end;
THMControl = specialize THMBaseObject<TWinControl, THMControlInstance>;
THMBaseControls = specialize TFPGObjectList<THMControl>;
{ THMControls }
THMControls = class(THMBaseControls)
procedure Delete(AName: String); overload;
function Find(AName: String): THMControl;
function Find(AControl: TWinControl): THMControl;
function FindOrCreate(AName: String): THMControl;
end;
THMBaseForm = specialize THMBaseObject<TCustomForm, THMFormInstance>;
TActionLists = specialize TFPGObjectList<TActionList>;
{ THMForm }
THMForm = class(THMBaseForm)
private
{en
Used for notifying when an ActionList is destroyed.
}
FFreeNotifier: TFreeNotifier;
FActionLists: TActionLists;
function GetActionByCommand(ActionList: TActionList; Command: String): TAction;
procedure OnActionListFree(Sender: TObject);
procedure OnHotkeyEvent(hotkey: THotkey; operation: THotkeyOperation);
procedure RemoveActionShortcut(hotkey: THotkey; AssignNextShortcut: Boolean);
procedure SetActionShortcut(hotkey: THotkey; OverridePrevious: Boolean);
public
Controls: THMControls;
constructor Create(AName: String); override;
destructor Destroy; override;
procedure RegisterActionList(ActionList: TActionList);
procedure UnregisterActionList(ActionList: TActionList);
end;
TBaseForms = specialize TFPGObjectList<THMForm>;
{ THMForms }
THMForms = class(TBaseForms)
procedure Delete(AName: String); overload;
function Find(AName: String): THMForm;
function Find(AForm: TCustomForm): THMForm;
function FindOrCreate(AName: String): THMForm;
end;
{ THotKeyManager }
THotKeyManager = class
private
FForms: THMForms;
FLastShortcutTime: Double; // When last shortcut was received (used for sequences of shortcuts)
FSequenceStep: Integer; // Which hotkey we are waiting for (from 0)
FShortcutsSequence: TDynamicStringArray; // Sequence of shortcuts that has been processed since last key event
FVersion: Integer;
//---------------------
procedure ClearAllHotkeys;
//Hotkey Handler
procedure KeyDownHandler(Sender: TObject; var Key: Word; Shift: TShiftState);
//---------------------
//This function is called from KeyDownHandler to find registered hotkey and execute assigned action
function HotKeyEvent(Form: TCustomForm; Hotkeys: THotkeys): Boolean;
//---------------------
function RegisterForm(AFormName: String): THMForm;
function RegisterControl(AFormName: String; AControlName: String): THMControl;
//---------------------
procedure Save(Config: TXmlConfig; Root: TXmlNode);
procedure Load(Config: TXmlConfig; Root: TXmlNode);
procedure LoadIni(FileName: String);
public
constructor Create;
destructor Destroy; override;
//---------------------
procedure Save(FileName: String);
procedure Load(FileName: String);
//---------------------
function Register(AForm: TCustomForm; AFormName: String): THMForm;
function Register(AControl: TWinControl; AControlName: String): THMControl;
procedure UnRegister(AForm: TCustomForm);
procedure UnRegister(AControl: TWinControl);
//---------------------
property Forms: THMForms read FForms;
property Version: Integer read FVersion;
end;
implementation
uses
Laz2_XMLRead, uKeyboard, uGlobs, uDebug, uDCVersion, uFormCommands,
DCOSUtils, DCStrUtils;
const
MaxShortcutSequenceInterval = 1000; // in ms
{ THotkey }
procedure THotkey.Assign(Hotkey: THotkey);
begin
Shortcuts := Copy(Hotkey.Shortcuts);
Params := Copy(Hotkey.Params);
Command := Hotkey.Command;
end;
function THotkey.Clone: THotkey;
begin
Result := THotkey.Create;
Result.Assign(Self);
end;
function THotkey.HasParam(const aParams: array of String): Boolean;
begin
Result := ContainsOneOf(Params, aParams);
end;
function THotkey.HasParam(const aParam: String): Boolean;
begin
Result := Contains(Params, aParam);
end;
function THotkey.SameAs(Hotkey: THotkey): Boolean;
begin
Result := (Command = Hotkey.Command) and
(SameShortcuts(Hotkey.Shortcuts)) and
(SameParams(Hotkey.Params));
end;
function THotkey.SameParams(const aParams: array of String): Boolean;
begin
Result := Compare(Params, aParams);
end;
function THotkey.SameShortcuts(const aShortcuts: array of String): Boolean;
begin
Result := Compare(Shortcuts, aShortcuts);
end;
{ TFreeNotifier }
procedure TFreeNotifier.Notification(AComponent: TComponent; Operation: TOperation);
begin
if (Operation = opRemove) and Assigned(FFreeEvent) then
FFreeEvent(AComponent);
inherited Notification(AComponent, Operation);
end;
{ THotkeys }
constructor THotkeys.Create(AFreeObjects: Boolean);
begin
FOnChange := nil;
inherited Create(AFreeObjects);
end;
function THotkeys.Add(const Shortcuts, Params: array of String; Command: String): THotkey;
begin
if (Command <> EmptyStr) and (Length(Shortcuts) > 0) then
begin
Result := THotkey.Create;
Result.Shortcuts := CopyArray(Shortcuts);
Result.Params := CopyArray(Params);
Result.Command := Command;
Add(Result);
DoOnChange(Result, hopAdd);
end
else
Result := nil;
end;
function THotkeys.AddIfNotExists(const Shortcuts, Params: array of String; Command: String): THotkey;
var
i: Integer;
begin
// Check if the shortcuts aren't already assigned to a different command
// or if a different shortcut isn't already assigned to the command.
// Also check if the shortucts aren't a partial match to another shortcuts.
for i := 0 to Count - 1 do
begin
if ArrBegins(Items[i].Shortcuts, Shortcuts, True) or (Items[i].Command = Command) then
Exit(nil);
end;
Result := Add(Shortcuts, Params, Command);
end;
procedure THotkeys.AddIfNotExists(const ShortcutsWithParams: array of String; Command: String);
begin
AddIfNotExists(ShortcutsWithParams, Command, [], []);
end;
procedure THotkeys.AddIfNotExists(Key: Word; Shift: TShiftState;
const Command: String; const Param: String);
var
AParams: TDynamicStringArray;
begin
if (Length(Param) > 0) then AddString(AParams, Param);
AddIfNotExists([VirtualKeyToText(Key, Shift)], AParams, Command);
end;
procedure THotkeys.AddIfNotExists(const ShortcutsWithParams: array of String; Command: String;
const OldShortcuts, OldParams: array of String);
var
s: String;
StartIndex: Integer;
function GetArray: TDynamicStringArray;
var
Index: Integer;
begin
Result := nil;
Index := StartIndex;
while Index <= High(ShortcutsWithParams) do
begin
s := ShortcutsWithParams[Index];
if s <> '' then
AddString(Result, s)
else
Break;
Inc(Index);
end;
StartIndex := Index + 1;
end;
function CheckIfOldOrEmpty: Boolean;
var
i: Integer;
begin
for i := 0 to Count - 1 do
if Items[i].Command = Command then
begin
if not (Items[i].SameShortcuts(OldShortcuts) and
Items[i].SameParams(OldParams)) then
Exit(False);
end;
Result := True;
end;
var
Shortcuts, Params: array of String;
begin
// Check if a different shortcut isn't already assigned to the command.
// If there is only the old shortcut then allow adding new one.
if not CheckIfOldOrEmpty then
Exit;
StartIndex := Low(ShortcutsWithParams);
while True do
begin
Shortcuts := GetArray;
Params := GetArray;
if Length(Shortcuts) > 0 then
begin
// Check if the shortcuts aren't already assigned to a different command.
if not Assigned(FindByBeginning(Shortcuts, True)) then
Add(Shortcuts, Params, Command);
end
else
Break;
end;
end;
procedure THotkeys.Clear;
var
i: Integer;
begin
for i := 0 to Count - 1 do
begin
DoOnChange(Items[0], hopClear);
inherited Delete(0);
end;
end;
procedure THotkeys.Remove(var hotkey: THotkey);
begin
if Assigned(hotkey) then
begin
DoOnChange(hotkey, hopRemove);
inherited Remove(hotkey);
if FreeObjects then
hotkey := nil;
end;
end;
procedure THotkeys.UpdateHotkey(Hotkey: THotkey);
begin
DoOnChange(Hotkey, hopUpdate);
end;
function THotkeys.Find(const Shortcuts: array of String): THotkey;
var
i: Integer;
begin
for i := 0 to Count - 1 do
if Items[i].SameShortcuts(Shortcuts) then
Exit(Items[i]);
Result := nil;
end;
function THotkeys.FindByBeginning(const Shortcuts: TDynamicStringArray; BothWays: Boolean): THotkey;
var
i: Integer;
begin
for i := 0 to Count - 1 do
if ArrBegins(Items[i].Shortcuts, Shortcuts, BothWays) then
Exit(Items[i]);
Result := nil;
end;
function THotkeys.FindByCommand(Command: String): THotkey;
var
i: Integer;
begin
for i := 0 to Count - 1 do
if Items[i].Command = Command then
Exit(Items[i]);
Result := nil;
end;
function THotkeys.FindByContents(Hotkey: THotkey): THotkey;
var
i: Integer;
begin
for i := 0 to Count - 1 do
begin
Result := Items[i];
if Result.SameAs(Hotkey) then
Exit;
end;
Result := nil;
end;
procedure THotkeys.DoOnChange(hotkey: THotkey; operation: THotkeyOperation);
begin
if Assigned(FOnChange) then
FOnChange(hotkey, operation);
end;
{ THMForm }
constructor THMForm.Create(AName: String);
begin
FFreeNotifier := nil;
inherited;
Controls := THMControls.Create(True);
FActionLists := TActionLists.Create(False);
end;
destructor THMForm.Destroy;
begin
inherited;
Controls.Free;
FActionLists.Free;
FFreeNotifier.Free;
end;
procedure THMForm.RegisterActionList(ActionList: TActionList);
var
i: Integer;
begin
if FActionLists.IndexOf(ActionList) < 0 then
begin
FActionLists.Add(ActionList);
Hotkeys.OnChange := @OnHotkeyEvent;
if not Assigned(FFreeNotifier) then
begin
FFreeNotifier := TFreeNotifier.Create(nil);
FFreeNotifier.OnFree := @OnActionListFree;
end;
ActionList.FreeNotification(FFreeNotifier);
// Initialize actionlist with shortcuts.
for i := 0 to hotkeys.Count - 1 do
SetActionShortcut(hotkeys[i], False);
end;
end;
procedure THMForm.UnregisterActionList(ActionList: TActionList);
begin
if FActionLists.Remove(ActionList) >= 0 then
ActionList.RemoveFreeNotification(FFreeNotifier);
end;
function THMForm.GetActionByCommand(ActionList: TActionList; Command: String): TAction;
var
action: TContainedAction;
begin
action := ActionList.ActionByName('act' + Copy(Command, 4, Length(Command) - 3));
if action is TAction then
Result := action as TAction
else
Result := nil;
end;
procedure THMForm.OnActionListFree(Sender: TObject);
begin
if Sender is TActionList then
UnregisterActionList(Sender as TActionList);
end;
procedure THMForm.OnHotkeyEvent(hotkey: THotkey; operation: THotkeyOperation);
begin
case operation of
hopAdd:
SetActionShortcut(hotkey, False);
hopRemove:
RemoveActionShortcut(hotkey, True);
hopClear:
RemoveActionShortcut(hotkey, False);
hopUpdate:
SetActionShortcut(hotkey, True);
end;
end;
procedure THMForm.RemoveActionShortcut(hotkey: THotkey; AssignNextShortcut: Boolean);
var
action: TAction;
i, j: Integer;
shortcut, newShortcut: TShortCut;
begin
shortcut := TextToShortCutEx(hotkey.Shortcuts[0]);
for i := 0 to FActionLists.Count - 1 do
begin
action := GetActionByCommand(FActionLists[i], hotkey.Command);
if Assigned(action) then
begin
if action.Shortcut = shortcut then
begin
newShortcut := VK_UNKNOWN;
if AssignNextShortcut then
begin
// Search for another possible hotkey assigned for the same command.
for j := 0 to hotkeys.Count - 1 do
if (hotkeys[j].Command = hotkey.Command) and (hotkeys[j] <> hotkey) then
begin
newShortcut := TextToShortCutEx(hotkeys[j].Shortcuts[0]);
Break;
end;
end;
action.ShortCut := newShortcut;
end;
end;
end;
end;
procedure THMForm.SetActionShortcut(hotkey: THotkey; OverridePrevious: Boolean);
var
action: TAction;
i: Integer;
shortcut: TShortCut;
begin
if Length(hotkey.Params) > 0 then Exit;
shortcut := TextToShortCutEx(hotkey.Shortcuts[0]);
for i := 0 to FActionLists.Count - 1 do
begin
action := GetActionByCommand(FActionLists[i], hotkey.Command);
if Assigned(action) then
begin
if OverridePrevious or (action.Shortcut = VK_UNKNOWN) then
action.ShortCut := shortcut;
end;
end;
end;
{ THMBaseObject }
constructor THMBaseObject.Create(AName: String);
begin
FName := AName;
FHotkeys := THotkeys.Create(True);
FObjects := TFPObjectList.Create(True);
end;
destructor THMBaseObject.Destroy;
begin
inherited Destroy;
FHotkeys.Free;
FObjects.Free;
end;
function THMBaseObject.Add(AInstanceInfo: InstanceInfoClass): Integer;
begin
Result := FObjects.Add(AInstanceInfo);
end;
procedure THMBaseObject.Delete(AInstance: InstanceClass);
var
i: Integer;
begin
for i := 0 to FObjects.Count - 1 do
if InstanceInfoClass(FObjects[i]).Instance = AInstance then
begin
FObjects.Delete(i);
Exit;
end;
end;
function THMBaseObject.Find(AInstance: InstanceClass): InstanceInfoClass;
var
i: Integer;
begin
for i := 0 to FObjects.Count - 1 do
begin
if InstanceInfoClass(FObjects[i]).Instance = AInstance then
Exit(InstanceInfoClass(FObjects[i]));
end;
Result := nil;
end;
{ THMControls }
procedure THMControls.Delete(AName: String);
var
i: Integer;
begin
for i := 0 to Count - 1 do
if SameText(Items[i].Name, AName) then
begin
Delete(i);
Exit;
end;
end;
function THMControls.Find(AName: String): THMControl;
var
i: Integer;
begin
for i := 0 to Count - 1 do
if SameText(Items[i].Name, AName) then
Exit(Items[i]);
Result := nil;
end;
function THMControls.Find(AControl: TWinControl): THMControl;
var
i: Integer;
begin
for i := 0 to Count - 1 do
begin
if Assigned(Items[i].Find(AControl)) then
Exit(Items[i]);
end;
Result := nil;
end;
function THMControls.FindOrCreate(AName: String): THMControl;
begin
Result := Find(AName);
if not Assigned(Result) then
begin
Result := THMControl.Create(AName);
Add(Result);
end;
end;
{ THMForms }
procedure THMForms.Delete(AName: String);
var
i: Integer;
begin
for i := 0 to Count - 1 do
if SameText(Items[i].Name, AName) then
begin
Delete(i);
Exit;
end;
end;
function THMForms.Find(AName: String): THMForm;
var
i: Integer;
begin
for i := 0 to Count - 1 do
begin
if SameText(Items[i].Name, AName) then
Exit(Items[i]);
end;
Result := nil;
end;
function THMForms.Find(AForm: TCustomForm): THMForm;
var
i: Integer;
begin
for i := 0 to Count - 1 do
begin
if Assigned(Items[i].Find(AForm)) then
Exit(Items[i]);
end;
Result := nil;
end;
function THMForms.FindOrCreate(AName: String): THMForm;
begin
Result := Find(AName);
if not Assigned(Result) then
begin
Result := THMForm.Create(AName);
Add(Result);
end;
end;
{ THotKeyManager }
constructor THotKeyManager.Create;
begin
FForms := THMForms.Create(True);
FSequenceStep := 0;
end;
destructor THotKeyManager.Destroy;
begin
inherited Destroy;
FForms.Free;
end;
procedure THotKeyManager.Save(FileName: String);
var
Config: TXmlConfig = nil;
begin
try
Config := TXmlConfig.Create(FileName, True);
Config.SetAttr(Config.RootNode, 'DCVersion', dcVersion);
Save(Config, Config.RootNode);
Config.Save;
finally
Config.Free;
end;
end;
procedure THotKeyManager.Load(FileName: String);
var
Config: TXmlConfig = nil;
NotAnXML: Boolean = False;
begin
try
Config := TXmlConfig.Create(FileName);
try
if Config.Load then Load(Config, Config.RootNode);
finally
Config.Free;
end;
except
on EXMLReadError do
NotAnXML := True;
end;
if NotAnXML then
begin
LoadIni(FileName);
// Immediately save as xml so that configuration isn't lost.
if mbRenameFile(FileName, FileName + '.ini.obsolete') then
Save(FileName);
end;
end;
procedure THotKeyManager.Save(Config: TXmlConfig; Root: TXmlNode);
var
SavedHotkeys: THotkeys;
procedure SaveHotkeys(Form: THMForm; Hotkeys: THotkeys; ControlIndex: Integer; Node: TXmlNode);
var
i, j: Integer;
HotkeyNode, ControlNode: TXmlNode;
Control: THMControl;
procedure AddControl(AName: String);
begin
ControlNode := Config.AddNode(HotkeyNode, 'Control');
Config.SetContent(ControlNode, AName);
end;
begin
for i := 0 to Hotkeys.Count - 1 do
begin
// Save Form's hotkeys and hotkeys which have not been saved yet.
if (ControlIndex < 0) or (not Assigned(SavedHotkeys.FindByContents(Hotkeys[i]))) then
begin
HotkeyNode := Config.AddNode(Node, 'Hotkey');
for j := Low(Hotkeys[i].Shortcuts) to High(Hotkeys[i].Shortcuts) do
Config.AddValue(HotkeyNode, 'Shortcut', Hotkeys[i].Shortcuts[j]);
Config.AddValue(HotkeyNode, 'Command', Hotkeys[i].Command);
for j := Low(Hotkeys[i].Params) to High(Hotkeys[i].Params) do
Config.AddValue(HotkeyNode, 'Param', Hotkeys[i].Params[j]);
if ControlIndex >= 0 then
AddControl(Form.Controls[ControlIndex].Name);
// Search all successive controls for the same hotkey.
for j := Succ(ControlIndex) to Form.Controls.Count - 1 do
begin
Control := Form.Controls[j];
if Assigned(Control.Hotkeys.FindByContents(Hotkeys[i])) then
AddControl(Control.Name);
end;
SavedHotkeys.Add(Hotkeys[i]);
end;
end;
end;
var
i, j: Integer;
FormNode: TXmlNode;
Form: THMForm;
begin
Root := Config.FindNode(Root, 'Hotkeys', True);
Config.ClearNode(Root);
Config.SetAttr(Root, 'Version', hkVersion);
SavedHotkeys := THotkeys.Create(False);
try
for i := 0 to FForms.Count - 1 do
begin
Form := FForms[i];
FormNode := Config.AddNode(Root, 'Form');
Config.SetAttr(FormNode, 'Name', Form.Name);
SaveHotkeys(Form, Form.Hotkeys, -1, FormNode);
for j := 0 to Form.Controls.Count - 1 do
SaveHotkeys(Form, Form.Controls[j].Hotkeys, j, FormNode);
end;
finally
SavedHotkeys.Free;
end;
end;
procedure THotKeyManager.Load(Config: TXmlConfig; Root: TXmlNode);
var
Form: THMForm;
procedure AddIfNotEmpty(var Arr: TDynamicStringArray; const Value: String);
begin
if Value <> '' then
AddString(Arr, Value);
end;
procedure LoadHotkey(FormName: String; Hotkeys: THotkeys; Node: TXmlNode);
const
RenamedCommandsMain: array [0..1] of
record OldName, NewName: String; SinceVersion: Integer end =
( (OldName: 'cm_RemoveTab'; NewName: 'cm_CloseTab'; SinceVersion: 14),
(OldName: 'cm_RemoveAllTabs'; NewName: 'cm_CloseAllTabs'; SinceVersion: 14)
);
var
Shortcut, Command, Param: String;
Shortcuts: array of String = nil;
Params: array of String = nil;
Controls: array of String = nil;
HMControl: THMControl;
i: Integer;
begin
// These checks for version may be removed after 0.5.5 release because
// the XML format for hotkeys has only been added in development version 0.5.5.
// Only Command needs to be retrieved here.
if FVersion <= 1 then
Command := Config.GetAttr(Node, 'Command', '')
else
Command := Config.GetValue(Node, 'Command', ''); // Leave only this or move this to the loop "while Assigned(Node) do" below
if FVersion <= 1 then
Param := Config.GetAttr(Node, 'Params', '')
else if FVersion < 9 then
Param := Config.GetValue(Node, 'Params', '');
if FVersion < 10 then
begin
Shortcut := Config.GetAttr(Node, 'Key', '');
if Shortcut <> '' then
begin
Shortcut := NormalizeModifiers(Shortcut);
AddIfNotEmpty(Shortcuts, Shortcut);
end;
end;
if (FVersion < 9) then
AddIfNotEmpty(Params, Param);
// Up to here may be deleted after 0.5.5 release.
Node := Node.FirstChild;
while Assigned(Node) do
begin
if Node.CompareName('Shortcut') = 0 then
AddIfNotEmpty(Shortcuts, NormalizeModifiers(Config.GetContent(Node)))
else if Node.CompareName('Control') = 0 then
AddIfNotEmpty(Controls, Config.GetContent(Node))
else if Node.CompareName('Param') = 0 then
AddIfNotEmpty(Params, Config.GetContent(Node));
Node := Node.NextSibling;
end;
if Command <> EmptyStr then
begin
// Rename commands that have changed names.
if FormName = 'Main' then
begin
for i := Low(RenamedCommandsMain) to High(RenamedCommandsMain) do
begin
if (FVersion <= RenamedCommandsMain[i].SinceVersion) and
(Command = RenamedCommandsMain[i].OldName) then
Command := RenamedCommandsMain[i].NewName;
end;
end;
if Length(Shortcuts) > 0 then
begin
if Length(Controls) = 0 then
begin
// This "if" block may also be deleted after 0.5.5 release.
if (FVersion <= 3) and IsShortcutConflictingWithOS(Shortcuts[0]) then
begin
HMControl := Form.Controls.FindOrCreate('Files Panel');
HMControl.Hotkeys.AddIfNotExists(Shortcuts, Params, Command);
end
else
Hotkeys.Add(Shortcuts, Params, Command); // Leave only this
end
else
begin
for i := Low(Controls) to High(Controls) do
begin
HMControl := Form.Controls.FindOrCreate(Controls[i]);
HMControl.Hotkeys.Add(Shortcuts, Params, Command);
end;
end;
end;
end;
end;
var
FormNode, HotkeyNode: TXmlNode;
AName: String;
begin
ClearAllHotkeys;
Root := Config.FindNode(Root, 'Hotkeys');
if Assigned(Root) then
begin
FVersion := Config.GetAttr(Root, 'Version', hkVersion);
FormNode := Root.FirstChild;
while Assigned(FormNode) do
begin
if (FormNode.CompareName('Form') = 0) and
(Config.TryGetAttr(FormNode, 'Name', AName)) and
(AName <> EmptyStr) then
begin
Form := FForms.FindOrCreate(AName);
HotkeyNode := FormNode.FirstChild;
while Assigned(HotkeyNode) do
begin
if HotkeyNode.CompareName('Hotkey') = 0 then
LoadHotkey(Form.Name, Form.Hotkeys, HotkeyNode);
HotkeyNode := HotkeyNode.NextSibling;
end;
end;
FormNode := FormNode.NextSibling;
end;
end;
end;
procedure THotKeyManager.LoadIni(FileName: String);
var
st: TStringList;
ini: TIniFileEx;
i, j: Integer;
section: String;
shortCut: String;
hotkeys: THotkeys;
form: THMForm;
control: THMControl;
Command, Param, FormName, ControlName: String;
Params: array of String = nil;
procedure RemoveFrmPrexif(var s: String);
begin
if SameText(Copy(s, 1, 3), 'Frm') then
Delete(s, 1, 3);
end;
begin
ClearAllHotkeys;
st := TStringList.Create;
ini := TIniFileEx.Create(FileName);
ini.ReadSections(st);
for i := 0 to st.Count - 1 do
begin
section := st[i];
shortCut := NormalizeModifiers(section);
if shortCut <> '' then
begin
j := 0;
while ini.ValueExists(section, 'Command' + IntToStr(j)) do
begin
Command := ini.ReadString(section, 'Command' + IntToStr(j), '');
Param := ini.ReadString(section, 'Param' + IntToStr(j), '');
ControlName := ini.ReadString(section, 'Object' + IntToStr(j), '');
FormName := ini.ReadString(section, 'Form' + IntToStr(j), '');
RemoveFrmPrexif(FormName);
RemoveFrmPrexif(ControlName);
form := FForms.FindOrCreate(FormName);
if IsShortcutConflictingWithOS(shortCut) then
ControlName := 'Files Panel';
// Old config had FormName=ControlName for main form.
if SameText(FormName, ControlName) then
begin
hotkeys := form.Hotkeys;
end
else
begin
control := form.Controls.FindOrCreate(ControlName);
hotkeys := control.Hotkeys;
end;
if Param <> '' then
begin
SetLength(Params, 1);
Params[0] := Param;
end
else
Params := nil;
hotkeys.Add([shortcut], Params, Command);
j := j + 1;
end;
end;
end;
FreeAndNil(st);
FreeAndNil(ini);
end;
function THotKeyManager.Register(AForm: TCustomForm; AFormName: String): THMForm;
var
formInstance: THMFormInstance;
begin
Result := RegisterForm(AFormName);
formInstance := Result.Find(AForm);
if not Assigned(formInstance) then
begin
formInstance := THMFormInstance.Create;
formInstance.Instance := AForm;
formInstance.KeyDownProc := AForm.OnKeyDown;
Result.Add(formInstance);
AForm.OnKeyDown := @KeyDownHandler;
AForm.KeyPreview := True;
end;
end;
function THotKeyManager.Register(AControl: TWinControl; AControlName: String): THMControl;
var
ParentForm: TCustomForm;
form: THMForm;
controlInstance: THMControlInstance;
begin
ParentForm := GetParentForm(AControl);
if Assigned(ParentForm) then
begin
form := FForms.Find(ParentForm);
if not Assigned(form) then
begin
DCDebug('HotMan: Failed registering ' + AControlName + ': Form ' +
ParentForm.ClassName + ':' + ParentForm.Name + ' not registered.');
Exit(nil);
end;
Result := form.Controls.Find(AControlName);
if not Assigned(Result) then
begin
Result := THMControl.Create(AControlName);
form.Controls.Add(Result);
end;
controlInstance := Result.Find(AControl);
if not Assigned(controlInstance) then
begin
controlInstance := THMControlInstance.Create;
controlInstance.Instance := AControl;
controlInstance.KeyDownProc := AControl.OnKeyDown;
Result.Add(controlInstance);
//AControl.OnKeyDown := @KeyDownHandler;
end;
end;
end;
function THotKeyManager.RegisterForm(AFormName: String): THMForm;
begin
Result := FForms.Find(AFormName);
if not Assigned(Result) then
begin
Result := THMForm.Create(AFormName);
FForms.Add(Result);
end;
end;
function THotKeyManager.RegisterControl(AFormName: String; AControlName: String): THMControl;
var
form: THMForm;
begin
form := RegisterForm(AFormName);
Result := form.Controls.Find(AControlName);
if not Assigned(Result) then
begin
Result := THMControl.Create(AControlName);
form.Controls.Add(Result);
end;
end;
procedure THotKeyManager.UnRegister(AForm: TCustomForm);
var
form: THMForm;
formInstance: THMFormInstance;
begin
form := FForms.Find(AForm);
if Assigned(form) then
begin
formInstance := form.Find(AForm);
AForm.OnKeyDown := formInstance.KeyDownProc;
form.Delete(AForm);
end;
end;
procedure THotKeyManager.UnRegister(AControl: TWinControl);
var
ParentForm: TCustomForm;
form: THMForm;
control: THMControl;
i: Integer;
begin
ParentForm := GetParentForm(AControl);
if Assigned(ParentForm) then
begin
form := FForms.Find(ParentForm);
if Assigned(form) then
begin
control := form.Controls.Find(AControl);
if Assigned(control) then
control.Delete(AControl);
end;
end
else
begin
// control lost its parent, find through all forms
for i := 0 to FForms.Count - 1 do
begin
form := FForms[i];
control := form.Controls.Find(AControl);
if Assigned(control) then
control.Delete(AControl);
end;
end;
end;
function THotKeyManager.HotKeyEvent(Form: TCustomForm; Hotkeys: THotkeys): Boolean;
var
hotkey: THotkey;
FormCommands: IFormCommands;
begin
hotkey := Hotkeys.FindByBeginning(FShortcutsSequence, False);
if Assigned(hotkey) then
begin
if High(hotkey.Shortcuts) > FSequenceStep then
begin
// There are more shortcuts to match.
FLastShortcutTime := SysUtils.Now;
Inc(FSequenceStep);
Result := True;
end
else
begin
FSequenceStep := 0;
FormCommands := Form as IFormCommands;
Result := Assigned(FormCommands) and
(FormCommands.ExecuteCommand(hotkey.Command, hotkey.Params) = cfrSuccess);
end;
end
else
Result := False;
end;
procedure THotKeyManager.ClearAllHotkeys;
var
i, j: Integer;
Form: THMForm;
begin
for i := 0 to FForms.Count - 1 do
begin
Form := FForms[i];
Form.Hotkeys.Clear;
for j := 0 to Form.Controls.Count - 1 do
Form.Controls[j].Hotkeys.Clear;
end;
end;
procedure THotKeyManager.KeyDownHandler(Sender: TObject; var Key: Word; Shift: TShiftState);
//------------------------------------------------------
var
i: Integer;
Shortcut: TShortCut;
TextShortcut: String;
Form: TCustomForm;
Control: TWinControl;
HMForm: THMForm;
HMControl: THMControl;
HMFormInstance: THMFormInstance;
HMControlInstance: THMControlInstance;
ShiftEx: TShiftState;
function OrigKeyDown(AKeyDownProc: TKeyEvent): Boolean;
begin
if Assigned(AKeyDownProc) then
begin
AKeyDownProc(Sender, Key, ShiftEx);
Result := True;
end
else
Result := False;
end;
begin
Form := GetParentForm(Sender as TWinControl);
HMForm := FForms.Find(Form);
if not Assigned(HMForm) then
Exit;
ShiftEx := GetKeyShiftStateEx;
Shortcut := KeyToShortCutEx(Key, ShiftEx);
TextShortcut := ShortCutToTextEx(Shortcut);
Control := Form.ActiveControl;
// Don't execute hotkeys that coincide with key typing actions.
if (TextShortcut <> '') and
((FSequenceStep > 0) or
(not ((((GetKeyTypingAction(ShiftEx) <> ktaNone) and (HMForm.Name = 'Main'))
{$IFDEF MSWINDOWS}
// Don't execute hotkeys with Ctrl+Alt = AltGr on Windows.
or (HasKeyboardAltGrKey and
(ShiftEx * KeyModifiersShortcutNoText = [ssCtrl, ssAlt]) and
(gKeyTyping[ktmNone] <> ktaNone))
// Don't execute hotkeys with AltGr on Windows.
or (ShiftEx = [ssAltGr])
{$ENDIF}
) and (Key in [VK_0..VK_9, VK_A..VK_Z])))) then
begin
// If too much time has passed reset sequence.
if (FSequenceStep > 0) and (DateTimeToTimeStamp(SysUtils.Now - FLastShortcutTime).Time > MaxShortcutSequenceInterval) then
FSequenceStep := 0;
// Add shortcut to sequence.
if Length(FShortcutsSequence) <> FSequenceStep + 1 then
SetLength(FShortcutsSequence, FSequenceStep + 1);
FShortcutsSequence[FSequenceStep] := TextShortcut;
if Assigned(Control) then
begin
for i := 0 to HMForm.Controls.Count - 1 do
begin
HMControl := HMForm.Controls[i];
HMControlInstance := HMControl.Find(Control);
if Assigned(HMControlInstance) then
begin
if HotKeyEvent(Form, HMControl.Hotkeys) then
begin
Key := VK_UNKNOWN;
Exit;
end
else
Break;
end;
end;
end;
// Hotkey for the whole form
if (Key <> VK_UNKNOWN) and HotKeyEvent(Form, HMForm.Hotkeys) then
begin
Key := VK_UNKNOWN;
Exit;
end;
FSequenceStep := 0; // Hotkey was not matched - reset sequence.
end;
if Key <> VK_UNKNOWN then
begin
HMFormInstance := HMForm.Find(Form);
OrigKeyDown(HMFormInstance.KeyDownProc);
end;
end;
end.