{*************************************************************************** * * * This source 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 code 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. * * * * A copy of the GNU General Public License is available on the World * * Wide Web at . You can also * * obtain it by writing to the Free Software Foundation, * * Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. * * * ***************************************************************************} unit MenuShortcuts; {$mode objfpc}{$H+} interface uses Classes, SysUtils, strutils, types, fgl, // LCL ActnList, ButtonPanel, Controls, Dialogs, StdCtrls, Menus, Forms, Graphics, LCLType, LCLIntf, LCLProc, // LazUtils LazUTF8, // IdeIntf IDEDialogs, PropEdits, // IDE LazarusIDEStrConsts; type TSCKind = (scUnknown, scMenuItemSC, scMenuItemKey2, scMenuItemAccel, scActionSC, scActionSecondary, scActionAccel, scOtherCompAccel); TDisplayType = (dtNone, dtBlack, dtBlackBold, dtGreyed, dtGreyedBold); TDisplayClickEvent = procedure(isHeader: boolean; index: integer) of object; const Margin = 6; Double_Margin = Margin shl 1; Leading = 4; Double_Leading = Leading shl 1; Treble_Leading = Leading + Double_Leading; VDim = 20; VTextOffset = 2; Header_Color = TColor($00EDEFD6); Accelerator_Kinds = [scMenuItemAccel, scActionAccel, scOtherCompAccel]; MenuItem_Kinds = [scMenuItemSC, scMenuItemKey2, scMenuItemAccel]; ShortcutOnly_Kinds = [scMenuItemSC, scMenuItemKey2, scActionSC, scActionSecondary]; //#todo extend this list, or use one from elsewhere in LCL? ShortCutKeys: array[0..48] of word = (VK_UNKNOWN, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_A, VK_B, VK_C, VK_D, VK_E, VK_F, VK_G, VK_H, VK_I, VK_J, VK_K, VK_L, VK_M, VK_N, VK_O, VK_P, VK_Q, VK_R, VK_S, VK_T, VK_U, VK_V, VK_W, VK_X, VK_Y, VK_Z, VK_F1, VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_F10, VK_F11, VK_F12); type { TSCInfo } TSCInfo = class(TObject) strict private FComponent: TComponent; FComponentName: string; FKind: TSCKind; FShortcut: TShortCut; function GetAction: TAction; function GetCaption: string; function GetMenuItem: TMenuItem; function GetToCompositeString: string; public constructor CreateWithParams(aComponent: TComponent; aKind: TSCKind; aSC: TShortCut); property Action: TAction read GetAction; property Caption: string read GetCaption; property Component: TComponent read FComponent; property ComponentName: string read FComponentName; property Kind: TSCKind read FKind; property MenuItem: TMenuItem read GetMenuItem; property Shortcut: TShortCut read FShortcut; property ToCompositeString: string read GetToCompositeString; end; TSCInfoList = specialize TFPGList; { TSCList } TSCList = class(TObject) strict private FAcceleratorsInContainerCount: integer; FScanList: TStringList; FShortcutsInContainerCount: integer; FInitialDuplicates: TSCInfoList; FUniqueList: TSCInfoList; function GetScanListCompName(index: integer): string; function GetUniqueCount: integer; public constructor Create; destructor Destroy; override; function FindUniqueInfoForShortcut(aSC: TShortCut): TSCInfo; function UniqueListContainsShortcut(aSC: TShortCut): boolean; procedure ClearAllLists; procedure ScanContainerForShortcutsAndAccelerators; procedure ScanContainerForShortcutsOnly; procedure ScanSCListForDuplicates; procedure SortByComponentPropertyName; property AcceleratorsInContainerCount: integer read FAcceleratorsInContainerCount write FAcceleratorsInContainerCount; property InitialDuplicates: TSCInfoList read FInitialDuplicates; property ScanList: TStringList read FScanList; property ScanListCompName[index: integer]: string read GetScanListCompName; property ShortcutsInContainerCount: integer read FShortcutsInContainerCount write FShortcutsInContainerCount; property UniqueCount: integer read GetUniqueCount; end; { TAddShortcutDialog } TAddShortcutDialog = class(TForm) strict private FButtonPanel: TButtonPanel; FMenuItem: TMenuItem; FNewShortcut: TShortCut; FOldShortcut: TShortCut; FShortCutGrabBox: TShortCutGrabBox; procedure OKButtonClick(Sender: TObject); procedure GrabBoxCloseUp(Sender: TObject); public constructor CreateWithMenuItem(AOwner: TComponent; aMI: TMenuItem; isMainSC: boolean; aSC: TShortCut); property NewShortcut: TShortCut read FNewShortcut; property OldShortcut: TShortCut write FOldShortcut; end; TMenuShortcuts = class; { TEditShortcutCaptionDialog } TEditShortcutCaptionDialog = class(TForm) strict private FEditingCaption: boolean; FInfo: TSCInfo; FNewCaption: string; FNewShortcut: TShortCut; FOldCaption: string; // GUI controls FButtonPanel: TButtonPanel; FEdit: TEdit; FGrabBox: TCustomShortCutGrabBox; FGroupBox: TGroupBox; FShortcuts: TMenuShortcuts; procedure CaptionEditChange(Sender: TObject); procedure GrabBoxEnter(Sender: TObject); procedure GrabBoxExit(Sender: TObject); procedure OKButtonOnClick(Sender: TObject); protected procedure Activate; override; public constructor {%H-}CreateNew(aShortcuts: TMenuShortcuts; aSCInfo: TSCInfo); property NewCaption: string read FNewCaption; property NewShortcut: TShortCut read FNewShortcut; end; TDualDisplay = class; TContents = class(TCustomControl) private FCol1MaxTextWidth: integer; FCol2MaxTextWidth: integer; FDualDisplay: TDualDisplay; FOnContentsClick: TModalDialogFinished; FSList: TStringList; protected procedure DoContentsClick(anIndex: integer); procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure Paint; override; property Col1MaxTextWidth: integer read FCol1MaxTextWidth; property Col2MaxTextWidth: integer read FCol2MaxTextWidth; property SList: TStringList read FSList; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure AddToList(const aLine: string; aDisplayType: TDisplayType=dtBlack); procedure Clear; property OnContentsClick: TModalDialogFinished read FOnContentsClick write FOnContentsClick; end; { THeader } THeader = class(TCustomControl) private FCol1Header: string; FCol2Header: string; FColumn1TextWidth: integer; FDisplayType: TDisplayType; FDualDisplay: TDualDisplay; FOnHeaderClick: TModalDialogFinished; protected procedure DoHeaderClick(anIndex: integer); procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure Paint; override; public constructor Create(AOwner: TComponent); override; procedure AddHeader(const aHeader: string; aDisplayType: TDisplayType); procedure Clear; property Column1TextWidth: integer read FColumn1TextWidth; property OnHeaderClick: TModalDialogFinished read FOnHeaderClick write FOnHeaderClick; end; { TDualDisplay } TDualDisplay = class(TCustomControl) private FCol1Right: integer; FContents: TContents; FHeader: THeader; FOnDisplayClick: TDisplayClickEvent; FSBox: TScrollBox; FUpdating: boolean; function GetContentsCount: integer; procedure HeaderContentsClick(Sender: TObject; index: integer); procedure SetCol1Right(AValue: integer); protected class function GetControlClassDefaultSize: TSize; override; function TextWidth(const aText: string): integer; property Updating: boolean read FUpdating; public constructor Create(AOwner: TComponent); override; procedure AddHeader(const aHeader: string; aDT: TDisplayType=dtBlackBold); procedure AddLine(const aLine: string; aDT: TDisplayType=dtBlack); procedure BeginUpdate; procedure Clear; procedure ClearContents; procedure ClearHeader; procedure EndUpdate; procedure InvalidateContents; property Col1Right: integer read FCol1Right write SetCol1Right; property ContentsCount: integer read GetContentsCount; property OnDisplayClick: TDisplayClickEvent read FOnDisplayClick write FOnDisplayClick; end; { TMenuShortcuts } TMenuShortcuts = class private FShortcutList: TSCList; FShortcutMenuItemsCount: integer; FShortcutConflictsCount: integer; public constructor Create; destructor Destroy; override; procedure Initialize; procedure UpdateShortcutList(includeAccelerators: boolean=False); procedure ResetMenuItemsCount; function Statistics(aShortcutCount: integer): string; public property ShortcutList: TSCList read FShortcutList; property ShortcutMenuItemsCount: integer read FShortcutMenuItemsCount; //property ShortcutConflictsCount: integer read FShortcutConflictsCount; end; function AmpersandStripped(const aText: string): string; function AddNewOrEditShortcutDlg(aMI: TMenuItem; isMainSCut: boolean; var aShortcut: TShortCut): boolean; function HasAccelerator(const aText: string; out aShortcut: TShortCut): boolean; function NewShortcutOrCaptionIsValidDlg(aConflictingInfo: TSCInfo; out aNewShortcut: TShortCut; out aNewCaption: string): boolean; function KindToPropertyName(aKind: TSCKind): string; function SplitCommaText(const aCommaText: string; out firstBit: string): string; function SortByComponentPropertyName(List: TStringList; Index1, Index2: Integer): Integer; implementation function AmpersandStripped(const aText: string): string; var p: integer; begin Result:=aText; p:=Pos('&', Result); while (p > 0) do begin Delete(Result, p, 1); p:=Pos('&', Result); end; end; function AddNewOrEditShortcutDlg(aMI: TMenuItem; isMainSCut: boolean; var aShortcut: TShortCut): boolean; var dlg: TAddShortcutDialog; begin dlg:=TAddShortcutDialog.CreateWithMenuItem(nil, aMI, isMainSCut, aShortcut); try if (dlg.ShowModal = mrOK) then begin aShortcut:=dlg.NewShortcut; Result:=True; end else Result:=False; finally dlg.Free; end; end; function HasAccelerator(const aText: string; out aShortcut: TShortCut): boolean; var p, UTF8Len: integer; accelStr: string; begin Result := False; aShortcut := 0; if aText = '' then Exit; p := 0; repeat p := PosEx('&', aText, p+1); if (p = 0) or (p = Length(aText)) then Break; if aText[p+1] <> '&' then // '&&' is reduced to '&' by widgetset GUI. begin UTF8Len := UTF8CodepointSize(@aText[p+1]); accelStr := UTF8UpperCase(Copy(aText, p+1, UTF8Len)); // force uppercase // ToDo: Use the whole UTF-8 character in accelStr. How? aShortcut := KeyToShortCut(Ord(accelStr[1]), {$if defined(darwin) or defined(macos) or defined(iphonesim)} [ssMeta] {$else} [ssAlt] {$endif}); Result := True; Break; end; until False; end; { function GetAcceleratedItemsCount(aMenu: TMenu): integer; var i: integer; procedure RecursiveCountAcceleratedCaptions(aMI: TMenuItem); var j: integer; sc: TShortCut; begin if HasAccelerator(aMI.Caption, sc) then Inc(Result); for j:=0 to aMI.Count-1 do RecursiveCountAcceleratedCaptions(aMI.Items[j]); end; begin Result:=0; for i:=0 to aMenu.Items.Count-1 do RecursiveCountAcceleratedCaptions(aMenu.Items[i]); end; } procedure DoShortcutAccelScanCount(const aSCList: TSCList; shortcutsOnly: boolean); var dm: TDataModule; frm: TCustomForm; i, a: integer; aLst: TActionList; ac: TAction; sc: TShortCut; container: TComponent; procedure AddInfoToScanList(aComp: TComponent; aSC: TShortCut; aKind: TSCKind); var isAccel: boolean; begin isAccel:=(aKind in Accelerator_Kinds); if isAccel and not shortcutsOnly then aSCList.AcceleratorsInContainerCount:=aSCList.AcceleratorsInContainerCount+1 else aSCList.ShortcutsInContainerCount:=aSCList.ShortcutsInContainerCount+1; aSCList.ScanList.AddObject(ShortCutToText(aSC), TSCInfo.CreateWithParams(aComp, aKind, aSC)); end; procedure ScanMenu(aMenu: TMenu); var i: integer; procedure RecursiveScanItem(anItem:TMenuItem); var j: integer; sc: TShortCut; begin if (anItem.ShortCut <> 0) then AddInfoToScanList(anItem, anItem.ShortCut, scMenuItemSC); if (anItem.ShortCutKey2 <> 0) then AddInfoToScanList(anItem, anItem.ShortCutKey2, scMenuItemKey2); if not shortcutsOnly and HasAccelerator(anItem.Caption, sc) then AddInfoToScanList(anItem, sc, scMenuItemAccel); for j:=0 to anItem.Count-1 do RecursiveScanItem(anItem.Items[j]); end; begin for i:=0 to aMenu.Items.Count-1 do RecursiveScanItem(aMenu.Items[i]); end; begin container:=GlobalDesignHook.LookupRoot as TComponent; aSCList.ClearAllLists; aSCList.AcceleratorsInContainerCount:=0; aSCList.ShortcutsInContainerCount:=0; if (container is TDataModule) then begin dm:=TDataModule(container); for i:=0 to dm.ComponentCount-1 do if (dm.Components[i] is TMenu) then ScanMenu(TMenu(dm.Components[i])); end else if (container is TCustomForm) then begin frm:=TCustomForm(container); for i:=0 to frm.ComponentCount-1 do if (frm.Components[i] is TMenu) then ScanMenu(TMenu(frm.Components[i])) else if (frm.Components[i] is TActionList) then begin aLst:=TActionList(frm.Components[i]); for a:=0 to aLst.ActionCount-1 do begin ac:=TAction(aLst.Actions[a]); if (ac.ShortCut > 0) then AddInfoToScanList(ac, ac.ShortCut, scActionSC); if (ac.SecondaryShortCuts.Count > 0) then AddInfoToScanList(ac, ac.SecondaryShortCuts.ShortCuts[0], scActionSecondary); if not shortcutsOnly and HasAccelerator(ac.Caption, sc) then AddInfoToScanList(ac, sc, scActionAccel); end; end else begin if not shortcutsOnly and (frm.Components[i] is TControl) and HasAccelerator(TControl(frm.Components[i]).Caption, sc) then AddInfoToScanList(frm.Components[i], sc, scOtherCompAccel); end; end; Assert(aSCList.AcceleratorsInContainerCount+aSCList.ShortcutsInContainerCount= aSCList.ScanList.Count,'DoShortcutAccelScanCount: internal counting error'); end; function NewShortcutOrCaptionIsValidDlg(aConflictingInfo: TSCInfo; out aNewShortcut: TShortCut; out aNewCaption: string): boolean; var dlg: TEditShortcutCaptionDialog; ok: boolean; sc: TShortCut; begin dlg:=TEditShortcutCaptionDialog.CreateNew(nil, aConflictingInfo); try Result:=(dlg.ShowModal = mrOK); case (aConflictingInfo.Kind in Accelerator_Kinds) of True: begin if HasAccelerator(dlg.NewCaption, sc) then ok:=(sc <> aConflictingInfo.Shortcut) else ok:=True; end; False: ok:=(aConflictingInfo.Shortcut <> dlg.NewShortcut); end; Result:=Result and ok; if Result then begin aNewShortcut:=dlg.NewShortcut; aNewCaption:=dlg.NewCaption; end else begin aNewShortcut:=0; aNewCaption:=''; end; finally FreeAndNil(dlg); end; end; function KindToPropertyName(aKind: TSCKind): string; begin Result:=''; case aKind of scUnknown: Result:=''; scActionAccel, scMenuItemAccel, scOtherCompAccel: Result:='Caption'; scActionSC, scMenuItemSC: Result:='ShortCut'; scActionSecondary: Result:='SecondaryShortcuts'; scMenuItemKey2: Result:='ShortCutKey2'; end; end; function SplitCommaText(const aCommaText: string; out firstBit: string): string; var p: integer; begin if (aCommaText = '') then begin firstBit:=''; Exit(''); end; p:=Pos(',', aCommaText); if (p = 0) then begin firstBit:=aCommaText; Exit(''); end; firstBit:=Copy(aCommaText, 1, Pred(p)); Result:=Copy(aCommaText, Succ(p), Length(aCommaText)-p); end; function SortByShortcut(const Item1, Item2: TSCInfo): Integer; begin if (Item1.Shortcut > Item2.Shortcut) then Result:= +1 else if (Item1.Shortcut < Item2.Shortcut) then Result:= -1 else Result:=0; end; function SortFPListByComponentPropertyName(const Item1, Item2: TSCInfo): Integer; begin if (Item1.ComponentName > Item2.ComponentName) then Result:= +1 else if (Item1.ComponentName < Item2.ComponentName) then Result:= -1 else Result:=0; end; function SortByComponentPropertyName(List: TStringList; Index1, Index2: Integer): Integer; var name1: string; name2: string; begin name1:=TSCInfo(List.Objects[Index1]).ComponentName; name2:=TSCInfo(List.Objects[Index2]).ComponentName; if (name1 > name2) then Result:= +1 else if (name2 > name1) then Result:= -1 else Result:=0; end; function SortOnComponentPropertyName(List: TStringList; Index1, Index2: Integer): Integer; var s1, s2: string; begin s1:=TSCInfo(List.Objects[Index1]).ToCompositeString; s2:=TSCInfo(List.Objects[Index2]).ToCompositeString; Result:=AnsiCompareText(s1, s2); end; { TSCInfo } constructor TSCInfo.CreateWithParams(aComponent: TComponent; aKind: TSCKind; aSC: TShortCut); begin FComponent:=aComponent; FComponentName:=aComponent.Name; FKind:=aKind; FShortcut:=aSC; end; function TSCInfo.GetAction: TAction; begin if (FComponent is TAction) then Result:=TAction(FComponent) else Result:=nil; end; function TSCInfo.GetCaption: string; begin if (FComponent is TControl) then Result:=TControl(FComponent).Caption else Result:=lisMenuEditorComponentIsUnexpectedKind; end; function TSCInfo.GetMenuItem: TMenuItem; begin if (FComponent is TMenuItem) then Result:=TMenuItem(FComponent) else Result:=nil; end; function TSCInfo.GetToCompositeString: string; begin Result:=FComponent.Name + ShortCutToText(FShortcut); end; { TSCList } constructor TSCList.Create; begin FScanList:=TStringList.Create; FUniqueList:=TSCInfoList.Create; FInitialDuplicates:=TSCInfoList.Create; ScanContainerForShortcutsAndAccelerators; end; destructor TSCList.Destroy; begin ClearAllLists; FreeAndNil(FUniqueList); FreeAndNil(FInitialDuplicates); FreeAndNil(FScanList); inherited Destroy; end; function TSCList.GetScanListCompName(index: integer): string; var inf: TSCInfo; begin if (index > -1) and (index < FScanList.Count) then begin inf:=TSCInfo(FScanList.Objects[index]); if (inf.ComponentName <> '') then Result:=inf.ComponentName else Result:=lisMenuEditorComponentIsUnnamed; end else Result:=Format(lisMenuEditorTSCListGetScanListCompNameInvalidIndexDForFScanLis, [index]); end; function TSCList.GetUniqueCount: integer; begin Result:=FUniqueList.Count; end; procedure TSCList.ClearAllLists; var i: integer; begin for i:=0 to FScanList.Count-1 do TSCInfo(FScanList.Objects[i]).Free; FScanList.Clear; FUniqueList.Clear; FInitialDuplicates.Clear; end; function TSCList.UniqueListContainsShortcut(aSC: TShortCut): boolean; var inf: TSCInfo; begin for inf in FUniqueList do if (inf.Shortcut = aSC) then Exit(True); Result:=False; end; function TSCList.FindUniqueInfoForShortcut(aSC: TShortCut): TSCInfo; var inf: TSCInfo; begin for inf in FUniqueList do if (inf.Shortcut = aSC) then Exit(inf); Result:=nil; end; procedure TSCList.ScanContainerForShortcutsAndAccelerators; begin DoShortcutAccelScanCount(Self, False); ScanSCListForDuplicates; if (FInitialDuplicates.Count > 0) then FInitialDuplicates.Sort(@SortByShortcut); if (FUniqueList.Count > 0) then FUniqueList.Sort(@SortByShortcut); end; //menushortcuts.pas(667,44) Error: Incompatible type for arg no. 1: // Got "
", // expected "" //menushortcuts.pas(669,37) Error: Incompatible type for arg no. 1: // Got "
", // expected "TFPGList$1$crc13D57BB4." procedure TSCList.ScanContainerForShortcutsOnly; begin DoShortcutAccelScanCount(Self, True); end; procedure TSCList.ScanSCListForDuplicates; var i: integer; inf2, inf1: TSCInfo; begin FreeAndNil(FUniqueList); FreeAndNil(FInitialDuplicates); FUniqueList:=TSCInfoList.Create; FInitialDuplicates:=TSCInfoList.Create; for i:=0 to FScanList.Count-1 do if UniqueListContainsShortcut(TSCInfo(FScanList.Objects[i]).Shortcut) then FInitialDuplicates.Add(FScanList.Objects[i] as TSCInfo) else FUniqueList.Add(FScanList.Objects[i] as TSCInfo); if (FInitialDuplicates.Count > 0) then begin FInitialDuplicates.Sort(@SortFPListByComponentPropertyName); for i:=FInitialDuplicates.Count-1 downto 1 do begin inf2:=FInitialDuplicates[i]; inf1:=FInitialDuplicates[i-1]; if (CompareText(inf2.ComponentName, inf1.ComponentName) = 0) and (inf2.Shortcut = inf1.Shortcut) then FInitialDuplicates.Delete(i); end; end; end; procedure TSCList.SortByComponentPropertyName; begin FScanList.CustomSort(@SortOnComponentPropertyName); end; { TAddShortcutDialog } constructor TAddShortcutDialog.CreateWithMenuItem(AOwner: TComponent; aMI: TMenuItem; isMainSC: boolean; aSC: TShortCut); var editing: boolean; key: word; shift: TShiftState; i: integer; begin inherited CreateNew(AOwner); FMenuItem:=aMI; FOldShortcut:=aSC; editing:=(aSC <> 0); Position:=poScreenCenter; BorderStyle:=bsDialog; case editing of False: if isMainSC then Caption:=Format(lisMenuEditorEnterANewShortCutForS, [FMenuItem.Name]) else Caption:=Format(lisMenuEditorEnterANewShortCutKey2ForS, [FMenuItem.Name]); True : if isMainSC then Caption:=Format(lisMenuEditorChangeTheShortCutForS, [FMenuItem.Name]) else Caption:=Format(lisMenuEditorChangeTheShortCutKey2ForS, [FMenuItem.Name]); end; FButtonPanel:=TButtonPanel.Create(Self); FButtonPanel.ShowButtons:=[pbOK, pbCancel]; FButtonPanel.OKButton.Name:='OKButton'; FButtonPanel.OKButton.DefaultCaption:=True; FButtonPanel.OKButton.OnClick:=@OKButtonClick; FButtonPanel.CancelButton.Name:='CancelButton'; FButtonPanel.CancelButton.DefaultCaption:=True; FButtonPanel.Parent:=Self; FShortCutGrabBox:=TShortCutGrabBox.Create(Self); FShortCutGrabBox.BorderSpacing.Around:=Margin; FShortCutGrabBox.GrabButton.Caption:='&Grab key'; // this rather restricted list covers most of the common values needed // #todo - extend list? with FShortCutGrabBox.KeyComboBox.Items do begin Clear; BeginUpdate; Add(lisMenuEditorNone); for i:=1 to High(ShortCutKeys) do Add(ShortCutToText(ShortCutKeys[i])); EndUpdate; end; {$if defined(darwin) or defined(macos) or defined(iphonesim)} FShortCutGrabBox.AllowedShifts:=[ssShift, ssCtrl, ssMeta] {$else} FShortCutGrabBox.AllowedShifts:=[ssShift, ssCtrl, ssAlt] {$endif}; FShortCutGrabBox.KeyComboBox.OnCloseUp:=@GrabBoxCloseUp; FShortCutGrabBox.Align:=alClient; FShortCutGrabBox.MainOkButton:=FButtonPanel.OKButton; if editing then begin ShortCutToKey(FOldShortcut, key, shift); FShortCutGrabBox.ShiftState:=shift; FShortCutGrabBox.Key:=key; end; FShortCutGrabBox.Parent:=Self; AutoSize:=True; end; procedure TAddShortcutDialog.OKButtonClick(Sender: TObject); begin if (FShortCutGrabBox.Key <> VK_UNKNOWN) then FNewShortcut:=KeyToShortCut(FShortCutGrabBox.Key, FShortCutGrabBox.ShiftState) else FNewShortcut:=0; end; procedure TAddShortcutDialog.GrabBoxCloseUp(Sender: TObject); begin if (FShortCutGrabBox.KeyComboBox.ItemIndex = 0) then FShortCutGrabBox.ShiftState:=[]; end; { TEditShortcutCaptionDialog } constructor TEditShortcutCaptionDialog.CreateNew(aShortcuts: TMenuShortcuts; aSCInfo: TSCInfo); var s: string; sse: TShiftStateEnum; i: integer; begin FShortcuts:=aShortcuts; FInfo:=aSCInfo; Assert(aSCInfo<>nil,'TEditShortcutCaptionDialog.CreateNew: aSCInfo is nil'); Assert(aSCInfo.Kind<>scUnknown,'TEditShortcutCaptionDialog.CreateNew: aSCInfo is unknown type'); Assert(FShortcuts.ShortcutList.UniqueCount>0,'TEditShortcutCaptionDialog.CreateNew: unique list is empty'); inherited CreateNew(Nil); FEditingCaption:=(FInfo.Kind in Accelerator_Kinds); Position:=poScreenCenter; BorderStyle:=bsDialog; Constraints.MinWidth:=300; FGroupBox:=TGroupBox.Create(Self); if FEditingCaption then begin Caption:=Format(lisMenuEditorChangeConflictingAcceleratorS, [ShortCutToText(FInfo.Shortcut)]); if (FInfo.Kind = scMenuItemAccel) then FOldCaption:=FInfo.MenuItem.Caption; FEdit:=TEdit.Create(Self); with FEdit do begin Align:=alClient; BorderSpacing.Around:=Margin; AutoSize:=True; Text:=FOldCaption; OnChange:=@CaptionEditChange; Parent:=FGroupBox; end; s:=lisMenuEditorCaption; end else begin Caption:=Format(lisMenuEditorChangeShortcutConflictS, [ShortCutToText(FInfo.Shortcut)]); s:=KindToPropertyName(FInfo.Kind); // don't set values to old shortcut since they need to be changed anyhow FGrabBox:=TCustomShortCutGrabBox.Create(Self); with FGrabBox do begin Align:=alClient; BorderSpacing.Around:=Margin; AutoSize:=True; GrabButton.Caption:=lisMenuEditorGrabKey; // this rather restricted list covers most of the common values needed with KeyComboBox.Items do begin Clear; BeginUpdate; for i:=Low(ShortCutKeys) to High(ShortCutKeys) do Add(ShortCutToText(ShortCutKeys[i])); EndUpdate; end; GrabButton.OnEnter:=@GrabBoxEnter; // we can't alter any grabBox OnClick event KeyComboBox.OnEnter:=@GrabBoxEnter; for sse in ShiftButtons do ShiftCheckBox[sse].OnEnter:=@GrabBoxEnter; OnExit:=@GrabBoxExit; FGrabBox.Caption:=Format(lisMenuEditorChangeShortcutCaptionForComponent, [s, FInfo.Component.Name]); Parent:=FGroupBox; end; end; FGroupBox.Caption:=Format(lisMenuEditorEditingSForS,[s, FInfo.Component.Name]); FGroupBox.Align:=alTop; FGroupBox.BorderSpacing.Around:=Margin; FGroupBox.AutoSize:=True; FGroupBox.Parent:=Self; FButtonPanel:=TButtonPanel.Create(Self); with FButtonPanel do begin ShowButtons:=[pbOK, pbCancel]; Top:=1; Align:=alTop; OKButton.OnClick:=@OKButtonOnClick; OKButton.ModalResult:=mrNone; OKButton.Enabled:=False; ShowBevel:=False; Parent:=Self; end; AutoSize:=True; end; procedure TEditShortcutCaptionDialog.CaptionEditChange(Sender: TObject); var newSC: TShortCut; hasAccel: boolean; ed: TEdit absolute Sender; inf: TSCInfo; begin if not (Sender is TEdit) then Exit; if HasAccelerator(ed.Text, newSC) then begin if FShortcuts.ShortcutList.UniqueListContainsShortcut(newSC) then begin inf:=FShortcuts.ShortcutList.FindUniqueInfoForShortcut(newSC); IDEMessageDialogAb(lisMenuEditorFurtherShortcutConflict, Format(lisMenuEditorSIsAlreadyInUse, [ShortCutToText(newSC), inf.Component.Name]), mtWarning, [mbOK], False); FEdit.Text:=AmpersandStripped(FOldCaption); FEdit.SetFocus; end else begin FNewShortcut:=newSC; FNewCaption:=ed.Text; end; end else begin FNewShortcut:=0; FNewCaption:=ed.Text; end; hasAccel:=HasAccelerator(FEdit.Text, newSC); FButtonPanel.OKButton.Enabled:=not hasAccel or (hasAccel and (newSC <> FInfo.Shortcut)); end; procedure TEditShortcutCaptionDialog.GrabBoxEnter(Sender: TObject); begin if not FButtonPanel.OKButton.Enabled then FButtonPanel.OKButton.Enabled:=True; end; procedure TEditShortcutCaptionDialog.GrabBoxExit(Sender: TObject); var newSC: TShortCut; inf: TSCInfo; begin newSC:=KeyToShortCut(FGrabBox.Key, FGrabBox.ShiftState); if (FInfo.Shortcut = newSC) then begin IDEMessageDialogAb(lisMenuEditorShortcutNotYetChanged, Format(lisMenuEditorYouHaveToChangeTheShortcutFromSStoAvoidAConflict, [ShortCutToText(FInfo.Shortcut)]), mtWarning, [mbOK], False); FGrabBox.KeyComboBox.SetFocus; Exit; end; if FShortcuts.ShortcutList.UniqueListContainsShortcut(newSC) then begin inf:=FShortcuts.ShortcutList.FindUniqueInfoForShortcut(newSC); IDEMessageDialogAb(lisMenuEditorFurtherShortcutConflict, Format(lisMenuEditorSIsAlreadyInUse, [ShortCutToText(newSC), inf.Component.Name]), mtWarning, [mbOK], False); FGrabBox.KeyComboBox.SetFocus; end else begin FNewShortcut:=newSC; FButtonPanel.OKButton.Enabled:=True; end; end; procedure TEditShortcutCaptionDialog.OKButtonOnClick(Sender: TObject); begin if FEditingCaption then begin if (FEdit.Text = '') then begin IDEMessageDialogAb(lisMenuEditorCaptionShouldNotBeBlank, lisMenuEditorYouMustEnterTextForTheCaption, mtWarning, [mbOK], False); FEdit.Text:=AmpersandStripped(FOldCaption); FEdit.SetFocus; end else ModalResult:=mrOK; end else ModalResult:=mrOK; end; procedure TEditShortcutCaptionDialog.Activate; begin inherited Activate; FButtonPanel.OKButton.Enabled:=False; end; { TContents } constructor TContents.Create(AOwner: TComponent); begin inherited Create(AOwner); FDualDisplay:=AOwner as TDualDisplay; FSList:=TStringList.Create; Color:=clBtnFace; end; destructor TContents.Destroy; begin FreeAndNil(FSList); inherited Destroy; end; procedure TContents.Clear; begin FSList.Clear; Height:=0; end; procedure TContents.DoContentsClick(anIndex: integer); begin if Assigned(FOnContentsClick) and (anIndex < FSList.Count) then FOnContentsClick(Self, anIndex); end; procedure TContents.Paint; var s, s1, s2: string; i: integer = 0; col1, col2: integer; dt: TDisplayType; begin if FDualDisplay.Updating then Exit; Canvas.FillRect(ClientRect); col2:=FDualDisplay.Col1Right + Leading; for s in FSList do begin s2:=SplitCommaText(s, s1); col1:=FDualDisplay.Col1Right - Leading - Canvas.TextWidth(s1); dt:=TDisplayType(PtrUInt(FSList.Objects[i])); case dt of dtNone: begin s1:=''; s2:=''; end; dtBlack: begin if (Canvas.Font.Color <> clBlack) then Canvas.Font.Color:=clBlack; if (Canvas.Font.Style <> []) then Canvas.Font.Style:=[]; end; dtBlackBold: begin if (Canvas.Font.Color <> clBlack) then Canvas.Font.Color:=clBlack; if (Canvas.Font.Style <> [fsBold]) then Canvas.Font.Style:=[fsBold]; end; dtGreyed: begin if (Canvas.Font.Color <> clGrayText) then Canvas.Font.Color:=clGrayText; if (Canvas.Font.Style <> []) then Canvas.Font.Style:=[]; end; dtGreyedBold: begin if (Canvas.Font.Color <> clGrayText) then Canvas.Font.Color:=clGrayText; if (Canvas.Font.Style <> [fsBold]) then Canvas.Font.Style:=[fsBold]; end; end; Canvas.TextOut(col1, i*VDim + VTextOffset, s1); Canvas.TextOut(col2, i*VDim + VTextOffset, s2); Inc(i); end; end; procedure TContents.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseDown(Button, Shift, X, Y); DoContentsClick(Y div VDim); end; procedure TContents.AddToList(const aLine: string; aDisplayType: TDisplayType); var h, w, cw, ch: integer; second, first: string; begin Assert(Parent<>nil,'TContents.AddToList: Parent is nil'); Assert(aDisplayType<>dtNone,'TContents.AddToList: TDisplayType=dtNone'); FSList.AddObject(aLine, TObject(PtrUInt(aDisplayType))); second:=SplitCommaText(aLine, first); w:=FDualDisplay.TextWidth(second); if (w > FCol2MaxTextWidth) then FCol2MaxTextWidth:=w; w:=FDualDisplay.TextWidth(first); if (w > FCol1MaxTextWidth) then FCol1MaxTextWidth:=w; w:=FCol1MaxTextWidth + FCol2MaxTextWidth + Treble_Leading; if (w < Parent.Width) then w:=Parent.Width; h:=FSList.Count*VDim; ch:=ClientHeight; cw:=ClientWidth; if (h > ch) or (w > cw) then SetBounds(0, 0, w, h); end; { THeader } procedure THeader.DoHeaderClick(anIndex: integer); begin if Assigned(FOnHeaderClick) then FOnHeaderClick(Self, anIndex); end; procedure THeader.Paint; begin Canvas.Brush.Color:=Header_Color; Canvas.FillRect(ClientRect); case FDisplayType of dtNone: begin FCol1Header:=''; FCol2Header:=''; end; dtBlack: begin if (Canvas.Font.Color <> clBlack) then Canvas.Font.Color:=clBlack; if (Canvas.Font.Style <> []) then Canvas.Font.Style:=[]; end; dtBlackBold: begin if (Canvas.Font.Color <> clBlack) then Canvas.Font.Color:=clBlack; if (Canvas.Font.Style <> [fsBold]) then Canvas.Font.Style:=[fsBold]; end; dtGreyed: begin if (Canvas.Font.Color <> clGrayText) then Canvas.Font.Color:=clGrayText; if (Canvas.Font.Style <> []) then Canvas.Font.Style:=[]; end; dtGreyedBold: begin if (Canvas.Font.Color <> clGrayText) then Canvas.Font.Color:=clGrayText; if (Canvas.Font.Style <> [fsBold]) then Canvas.Font.Style:=[fsBold]; end; end; Canvas.TextOut(FDualDisplay.Col1Right - Leading - FColumn1TextWidth, VTextOffset, FCol1Header); Canvas.TextOut(FDualDisplay.Col1Right + Leading, VTextOffset, FCol2Header); end; procedure THeader.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var i: integer=0; begin inherited MouseDown(Button, Shift, X, Y); if (X > FDualDisplay.Col1Right) then i:=1; DoHeaderClick(i); end; constructor THeader.Create(AOwner: TComponent); begin inherited Create(AOwner); FDualDisplay:=AOwner as TDualDisplay; Align:=alTop; Height:=VDim; Canvas.Font.Style:=[fsBold]; end; procedure THeader.AddHeader(const aHeader: string; aDisplayType: TDisplayType); begin FCol2Header:=SplitCommaText(aHeader, FCol1Header); FDisplayType:=aDisplayType; FColumn1TextWidth:=FDualDisplay.TextWidth(FCol1Header); Repaint; end; procedure THeader.Clear; begin FColumn1TextWidth:=0; FDisplayType:=dtNone; Invalidate; end; { TDualDisplay } constructor TDualDisplay.Create(AOwner: TComponent); begin inherited Create(AOwner); Name:='DualDisplay'; Color:=clBtnFace; Canvas.Font.Style:=[fsBold]; with GetControlClassDefaultSize do SetInitialBounds(0, 0, cx, cy); FHeader:=THeader.Create(Self); with FHeader do begin Name:='Header'; OnHeaderClick:=@HeaderContentsClick; Parent:=Self; end; FSBox:=TScrollBox.Create(Self); with FSBox do begin Align:=alClient; BorderStyle:=bsNone; AutoScroll:=True; Parent:=Self; end; FContents:=TContents.Create(Self); with FContents do begin Name:='Contents'; SetInitialBounds(0, 0, FSBox.Width, FSBox.Height); OnContentsClick:=@HeaderContentsClick; Color:=clBtnFace; Parent:=FSBox; end; end; function TDualDisplay.GetContentsCount: integer; begin Result:=FContents.SList.Count; end; procedure TDualDisplay.HeaderContentsClick(Sender: TObject; index: integer); begin if Assigned(FOnDisplayClick) then begin Assert(Sender<>nil,'TDualDisplay.HeaderContentsClick: Sender is nil'); Assert(index>-1,'TDualDisplay.HeaderContentsClick: index is negative'); if (Sender is TContents) then begin Assert(index AValue) then begin FCol1Right:=AValue; FHeader.Invalidate; FContents.Invalidate; end; end; class function TDualDisplay.GetControlClassDefaultSize: TSize; begin Result.cx:=200; Result.cy:=120; end; function TDualDisplay.TextWidth(const aText: string): integer; begin Result:=Canvas.TextWidth(aText); end; procedure TDualDisplay.AddHeader(const aHeader: string; aDT: TDisplayType); var tmp: integer; begin FHeader.AddHeader(aHeader, aDT); tmp:=FCol1Right - Double_Leading; if (FHeader.Column1TextWidth > tmp) then SetCol1Right(FHeader.Column1TextWidth + Double_Leading); tmp:=TextWidth(aHeader) + Treble_Leading; if (tmp > Width) then begin Width:=tmp; FHeader.Width:=tmp; FContents.Width:=tmp; end; FHeader.Repaint; end; procedure TDualDisplay.AddLine(const aLine: string; aDT: TDisplayType); var tmp: integer; begin FContents.AddToList(aLine, aDT); tmp:=FCol1Right - Double_Leading; if (FContents.Col1MaxTextWidth > tmp) then SetCol1Right(FContents.Col1MaxTextWidth + Double_Leading); tmp:=FContents.Width; if (tmp > ClientWidth) then begin Width:=tmp; FHeader.Width:=tmp; end; end; procedure TDualDisplay.BeginUpdate; begin FUpdating:=True; end; procedure TDualDisplay.EndUpdate; begin FUpdating:=False; end; procedure TDualDisplay.ClearHeader; begin FHeader.Clear; end; procedure TDualDisplay.Clear; begin FHeader.Clear; FContents.Clear; end; procedure TDualDisplay.ClearContents; begin FContents.Clear; end; procedure TDualDisplay.InvalidateContents; begin FContents.Invalidate; end; { TMenuShortcuts } constructor TMenuShortcuts.Create; begin FShortcutList:=TSCList.Create; end; destructor TMenuShortcuts.Destroy; begin FShortcutList.Free; inherited Destroy; end; procedure TMenuShortcuts.Initialize; begin FShortcutList.ClearAllLists; FShortcutList.ScanContainerForShortcutsAndAccelerators; FShortcutConflictsCount:=FShortcutList.InitialDuplicates.Count; end; procedure TMenuShortcuts.UpdateShortcutList(includeAccelerators: boolean); begin if includeAccelerators then FShortcutList.ScanContainerForShortcutsAndAccelerators else FShortcutList.ScanContainerForShortcutsOnly; end; procedure TMenuShortcuts.ResetMenuItemsCount; begin FShortcutMenuItemsCount := -1; end; function TMenuShortcuts.Statistics(aShortcutCount: integer): string; begin if (FShortcutMenuItemsCount <> aShortcutCount) then begin FShortcutMenuItemsCount := aShortcutCount; Result := Format(lisMenuEditorShortcutItemsS, [IntToStr(FShortcutMenuItemsCount)]); end else Result := ''; end; end.