doublecmd/src/foptions.pas
2023-02-27 22:29:33 +03:00

583 lines
18 KiB
ObjectPascal

{
Double Commander
-------------------------------------------------------------------------
Implementing of Options dialog
Copyright (C) 2006-2023 Alexander Koblov (alexx2000@mail.ru)
contributors:
Radek Cervinka <radek.cervinka@centrum.cz>
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, see <http://www.gnu.org/licenses/>.
}
unit fOptions;
{$mode objfpc}{$H+}
interface
uses
ActnList, SysUtils, Classes, Controls, Forms, Dialogs, ExtCtrls, ComCtrls,
Buttons, StdCtrls, TreeFilterEdit, KASButtonPanel, fgl, uGlobs, fOptionsFrame,
uDCUtils, EditBtn;
type
{ TOptionsEditorView }
TOptionsEditorView = class
EditorClass: TOptionsEditorClass;
Instance: TOptionsEditor;
TreeNode: TTreeNode;
LegacyOrderIndex: integer;
end;
TOptionsEditorViews = specialize TFPGObjectList<TOptionsEditorView>;
{ TfrmOptions }
TfrmOptions = class(TForm, IOptionsDialog)
btnHelp: TBitBtn;
lblEmptyEditor: TLabel;
OptionsEditorsImageList: TImageList;
pnlButtons: TKASButtonPanel;
Panel3: TPanel;
pnlCaption: TPanel;
btnOK: TBitBtn;
btnApply: TBitBtn;
btnCancel: TBitBtn;
sboxOptionsEditor: TScrollBox;
TreeFilterEdit: TTreeFilterEdit;
tvTreeView: TTreeView;
splOptionsSplitter: TSplitter;
alOptionsActionList: TActionList;
actCloseWithEscape: TAction;
procedure btnCancelClick(Sender: TObject);
procedure btnHelpClick(Sender: TObject);
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
procedure FormCloseQuery(Sender: TObject; var CanClose: boolean);
procedure FormCreate(Sender: TObject);
procedure btnOKClick(Sender: TObject);
procedure btnApplyClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure splOptionsSplitterMoved(Sender: TObject);
function TreeFilterEditFilterItem(ItemData: Pointer;
out Done: Boolean): Boolean;
procedure tvTreeViewChange(Sender: TObject; Node: TTreeNode);
procedure actCloseWithEscapeExecute(Sender: TObject);
private
FOptionsEditorList: TOptionsEditorViews;
FOldEditor: TOptionsEditorView;
function CreateEditor(EditorClass: TOptionsEditorClass): TOptionsEditor;
procedure CreateOptionsEditorList;
function GetEditor(EditorClass: TOptionsEditorClass): TOptionsEditor;
procedure LoadSettings;
procedure SelectEditor(EditorClassName: String);
function CompareTwoNodeOfConfigurationOptionTree(Node1, Node2: TTreeNode): integer;
function CycleThroughOptionEditors(bForceSaving:boolean):boolean;
procedure MakeVisible(Data: PtrInt);
public
constructor Create(TheOwner: TComponent); override;
constructor Create(TheOwner: TComponent; EditorClass: TOptionsEditorClass); overload;
constructor Create(TheOwner: TComponent; EditorClassName: String); overload;
procedure LoadConfig;
procedure SaveConfig;
end;
function ShowOptions(EditorClass: TOptionsEditorClass = nil): IOptionsDialog;
function ShowOptions(EditorClassName: String): IOptionsDialog;
procedure SortConfigurationOptionsOnLeftTree; //If the var "frmOptions" would be in the interface section, we could have called directly "frmOptions.tvTreeView.CustomSort(@frmOptions.CompareTwoNodeOfConfigurationOptionTree);"
//But it's not the case... Let's create this routine and respect the wish of original authors to have it there. Maybe there is a raison why so let's play safe.
function GetOptionsForm: TfrmOptions;
implementation
{$R *.lfm}
uses
HelpIntfs, LCLProc, LCLVersion, LazUTF8, LResources, Menus, Translations, Graphics,
DCStrUtils, uTranslator, uLng, uGlobsPaths, fMain;
var
LastOpenedEditor: TOptionsEditorClass = nil;
OptionsSearchFile: TPOFile = nil;
OptionsSearchCache: TList = nil;
frmOptions: TfrmOptions = nil;
procedure CreateSearchCache;
var
POFile: TPOFile;
procedure FillCache(AList: TOptionsEditorClassList);
var
I, J: Integer;
AClassName: String;
PoFileItem: TPoFileItem;
AEditor: TOptionsEditorRec;
begin
for I:= 0 to AList.Count - 1 do
begin
AEditor:= AList[I];
AClassName:= LowerCase(AEditor.EditorClass.ClassName);
for J:= 0 to POFile.Count - 1 do
begin
PoFileItem:= POFile.PoItems[J];
if StrBegins(PoFileItem.IdentifierLow, AClassName) then
begin
OptionsSearchCache.Add(PoFileItem);
end;
end;
if AEditor.HasChildren then FillCache(AEditor.Children);
end;
end;
begin
OptionsSearchCache:= TList.Create;
try
if Assigned(LRSTranslator) then
POFile:= (LRSTranslator as TTranslator).POFile
else begin
POFile:= TPOFile.Create(gpLngDir + gPOFileName, True);
OptionsSearchFile:= POFile;
end;
FillCache(OptionsEditorClassList);
except
// Skip
end;
end;
{ GetOptionsForm }
// To get a point on the frmOptions.
// Could have been simple to place "frmOptions" in the "interface" section but not sure why original author hide it under. Let's play safe.
function GetOptionsForm: TfrmOptions;
begin
Result := frmOptions;
end;
function ShowOptions(EditorClass: TOptionsEditorClass): IOptionsDialog;
begin
Result := ShowOptions(EditorClass.ClassName);
end;
function ShowOptions(EditorClassName: String): IOptionsDialog;
begin
if (OptionsSearchCache = nil) then
begin
CreateSearchCache;
end;
if Assigned(frmOptions) then
begin
if frmOptions.WindowState = wsMinimized then
frmOptions.WindowState:= wsNormal
else
frmOptions.BringToFront;
frmOptions.SelectEditor(EditorClassName);
end
else
begin
if EditorClassName = '' then
frmOptions := TfrmOptions.Create(Application)
else
frmOptions := TfrmOptions.Create(Application, EditorClassName);
frmOptions.Show;
end;
Result := frmOptions;
end;
procedure SortConfigurationOptionsOnLeftTree;
begin
if frmOptions<>nil then frmOptions.tvTreeView.CustomSort(@frmOptions.CompareTwoNodeOfConfigurationOptionTree);
end;
{ TfrmOptions }
procedure TfrmOptions.FormCreate(Sender: TObject);
begin
// Initialize property storage
InitPropStorage(Self);
TreeFilterEdit.Visible:= (OptionsSearchCache.Count > 0);
end;
procedure TfrmOptions.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
CloseAction:= caFree;
frmOptions:= nil;
end;
procedure TfrmOptions.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
CanClose := (ModalResult in [mrOK, mrCancel]) or CycleThroughOptionEditors(False);
end;
procedure TfrmOptions.btnCancelClick(Sender: TObject);
begin
// close window
Close;
end;
procedure TfrmOptions.btnHelpClick(Sender: TObject);
begin
ShowHelpOrErrorForKeyword('', HelpKeyword);
end;
procedure TfrmOptions.btnOKClick(Sender: TObject);
begin
// save all configuration
SaveConfig;
// write to config file
SaveGlobs;
// close window
Close;
end;
procedure TfrmOptions.btnApplyClick(Sender: TObject);
begin
// save all configuration
SaveConfig;
// write to config file
SaveGlobs;
end;
procedure TfrmOptions.FormDestroy(Sender: TObject);
begin
FreeAndNil(FOptionsEditorList);
end;
procedure TfrmOptions.splOptionsSplitterMoved(Sender: TObject);
var
ARight, ADelta: Integer;
begin
ADelta:= ScaleX(8, DesignTimePPI) * 2 + btnHelp.Width;
ARight:= splOptionsSplitter.Left;
if (ARight > (btnOK.Left - ADelta)) then
begin
ARight:= btnOK.Left - ADelta;
end;
TreeFilterEdit.Width:= (ARight - TreeFilterEdit.Left);
end;
function TfrmOptions.TreeFilterEditFilterItem(ItemData: Pointer; out
Done: Boolean): Boolean;
var
Index: Integer;
AClassName: String;
AFind, AText: String;
POFileItem: TPOFileItem;
aOptionsEditorView: TOptionsEditorView absolute ItemData;
begin
Done:= True;
AFind:= TreeFilterEdit.Text;
if Length(AFind) = 0 then Exit(True);
AFind:= UTF8LowerCase(AFind);
AClassName:= LowerCase(aOptionsEditorView.EditorClass.ClassName);
for Index:= 0 to OptionsSearchCache.Count - 1 do
begin
POFileItem:= TPOFileItem(OptionsSearchCache[Index]);
if Length(POFileItem.Translation) = 0 then
AText:= POFileItem.Original
else begin
AText:= POFileItem.Translation;
end;
AText:= UTF8LowerCase(StripHotkey(AText));
if Pos(AFind, AText) > 0 then
begin
if StrBegins(POFileItem.IdentifierLow, AClassName) then
begin
// DebugLn(AClassName + ': ' + AText);
Exit(True);
end;
end;
end;
Result:= False;
end;
function TfrmOptions.CompareTwoNodeOfConfigurationOptionTree(Node1, Node2: TTreeNode): integer;
begin
case gSortOrderOfConfigurationOptionsTree of
scoClassicLegacy:
begin
if TOptionsEditorView(Node1.Data).LegacyOrderIndex < TOptionsEditorView(Node2.Data).LegacyOrderIndex then result:=-1 else result:=1;
end;
scoAlphabeticalButLanguage:
begin
if (TOptionsEditorView(Node1.Data).EditorClass.ClassName='TfrmOptionsLanguage') or (TOptionsEditorView(Node1.Data).EditorClass.ClassName='TfrmOptionsFilesViewsComplement') then
result:=-1
else
if (TOptionsEditorView(Node2.Data).EditorClass.ClassName='TfrmOptionsLanguage') or (TOptionsEditorView(Node1.Data).EditorClass.ClassName='TfrmOptionsFilesViewsComplement') then
result:=1
else
result:=CompareStrings(Node1.Text, Node2.Text, gSortNatural, gSortSpecial, gSortCaseSensitivity)
end;
end;
end;
procedure TfrmOptions.CreateOptionsEditorList;
procedure AddEditors(EditorClassList: TOptionsEditorClassList; RootNode: TTreeNode);
var
I: LongInt;
aOptionsEditorClass: TOptionsEditorClass;
aOptionsEditorView: TOptionsEditorView;
TreeNode: TTreeNode;
IconIndex: Integer;
begin
for I:= 0 to EditorClassList.Count - 1 do
begin
aOptionsEditorClass := EditorClassList[I].EditorClass;
aOptionsEditorView := TOptionsEditorView.Create;
aOptionsEditorView.EditorClass := aOptionsEditorClass;
aOptionsEditorView.Instance := nil;
aOptionsEditorView.LegacyOrderIndex:=I;
FOptionsEditorList.Add(aOptionsEditorView);
TreeNode := tvTreeView.Items.AddChild(RootNode,
{$IF lcl_fullversion >= 093100}
aOptionsEditorClass.GetTitle
{$ELSE}
StringReplace(aOptionsEditorClass.GetTitle, '&', '&&', [rfReplaceAll])
{$ENDIF}
);
if Assigned(TreeNode) then
begin
IconIndex := aOptionsEditorClass.GetIconIndex;
TreeNode.ImageIndex := IconIndex;
TreeNode.SelectedIndex := IconIndex;
TreeNode.StateIndex := IconIndex;
TreeNode.Data := aOptionsEditorView;
end;
aOptionsEditorView.TreeNode := TreeNode;
if EditorClassList[I].HasChildren then
AddEditors(EditorClassList[I].Children, TreeNode);
end;
//2014-08-12:Let's sort by alphabetical order this list.
tvTreeView.CustomSort(@CompareTwoNodeOfConfigurationOptionTree);
end;
begin
FOptionsEditorList:= TOptionsEditorViews.Create;
AddEditors(OptionsEditorClassList, nil);
case gCollapseConfigurationOptionsTree of
ctsFullExpand: ; //By legacy, it was doing automaticall the tvTreeView.FullExpand;
ctsFullCollapse: tvTreeView.FullCollapse;
end;
end;
function TfrmOptions.GetEditor(EditorClass: TOptionsEditorClass): TOptionsEditor;
var
I: Integer;
begin
for I := 0 to FOptionsEditorList.Count - 1 do
begin
if FOptionsEditorList[I].EditorClass = EditorClass then
begin
if not Assigned(FOptionsEditorList[I].Instance) then
FOptionsEditorList[I].Instance := CreateEditor(FOptionsEditorList[I].EditorClass);
Result := FOptionsEditorList[I].Instance;
Exit;
end;
end;
Result := nil;
end;
procedure TfrmOptions.LoadSettings;
begin
LoadConfig;
end;
procedure TfrmOptions.SelectEditor(EditorClassName: String);
var
I: Integer;
begin
for I := 0 to FOptionsEditorList.Count - 1 do
begin
if (FOptionsEditorList[I].EditorClass.ClassName = EditorClassName) then
if Assigned(FOptionsEditorList[I].TreeNode) then
begin
FOptionsEditorList[I].TreeNode.Selected := True;
Application.QueueAsyncCall(@MakeVisible, PtrInt(FOptionsEditorList[I].TreeNode));
Break;
end;
end;
end;
constructor TfrmOptions.Create(TheOwner: TComponent);
begin
if not Assigned(LastOpenedEditor) and (OptionsEditorClassList.Count > 0) then
LastOpenedEditor := OptionsEditorClassList[0].EditorClass; // Select first editor.
Create(TheOwner, LastOpenedEditor);
end;
constructor TfrmOptions.Create(TheOwner: TComponent; EditorClass: TOptionsEditorClass);
begin
if Assigned(EditorClass) then
Create(TheOwner, EditorClass.ClassName)
else
Create(TheOwner, '');
end;
constructor TfrmOptions.Create(TheOwner: TComponent; EditorClassName: String);
begin
if (EditorClassName = '') and Assigned(LastOpenedEditor) then
EditorClassName := LastOpenedEditor.ClassName;
FOldEditor := nil;
inherited Create(TheOwner);
CreateOptionsEditorList;
SelectEditor(EditorClassName);
end;
procedure TfrmOptions.tvTreeViewChange(Sender: TObject; Node: TTreeNode);
var
SelectedEditorView: TOptionsEditorView;
begin
if (Node = nil) then Exit;
SelectedEditorView := TOptionsEditorView(Node.Data);
if Assigned(SelectedEditorView) and (FOldEditor <> SelectedEditorView) then
begin
if Assigned(FOldEditor) and Assigned(FOldEditor.Instance) then
FOldEditor.Instance.Visible := False;
if not Assigned(SelectedEditorView.Instance) then
SelectedEditorView.Instance := CreateEditor(SelectedEditorView.EditorClass);
if Assigned(SelectedEditorView.Instance) then
SelectedEditorView.Instance.Visible := True;
lblEmptyEditor.Visible := not Assigned(SelectedEditorView.Instance);
FOldEditor := SelectedEditorView;
LastOpenedEditor := SelectedEditorView.EditorClass;
pnlCaption.Caption := SelectedEditorView.EditorClass.GetTitle;
if Assigned(SelectedEditorView.Instance) then
begin
HelpKeyword:= SelectedEditorView.Instance.HelpKeyword;
btnHelp.Visible := HelpKeyword <> '';
end
else
btnHelp.Visible:= False;
end;
end;
function TfrmOptions.CreateEditor(EditorClass: TOptionsEditorClass): TOptionsEditor;
begin
if Assigned(EditorClass) and not EditorClass.IsEmpty then
begin
Result := EditorClass.Create(Self);
Result.Align := alClient;
Result.Visible := False;
Result.Init(sboxOptionsEditor, Self, [oeifLoad]);
end
else
Result := nil;
end;
procedure TfrmOptions.LoadConfig;
var
I: LongInt;
begin
{ Load options to frames }
for I:= 0 to FOptionsEditorList.Count - 1 do
begin
if Assigned(FOptionsEditorList[I].Instance) then
FOptionsEditorList[I].Instance.LoadSettings;
end;
end;
procedure TfrmOptions.SaveConfig;
begin
CycleThroughOptionEditors(True);
end;
{ TfrmOptions.CycleThroughOptionEditors }
// -Will cycle through all option editors to either:
// >Prompt user to save change if any, discard change if any, etc.
// >Force saving eventual modification without asking.
// -In case we prompt user save changes or not, user may answer that he wants to
// CANCEL exit. If so, that's the only case where the function will return FALSE.
// -Could be call from a simple "APPLY" or "OK" from the main option window and
// if so, will save any modification.
// -Could be call from "CANCEL" or "Attempt to close with the 'x' of the window
// and if so, will prompt user to save modifications, discard modification or
// cancel exiting.
function TfrmOptions.CycleThroughOptionEditors(bForceSaving: boolean): boolean;
var
I: integer;
SaveFlags: TOptionsEditorSaveFlags = [];
bNeedsRestart: boolean = False;
begin
Result := True;
I := 0;
while (I < FOptionsEditorList.Count) and (Result) do
begin
if Assigned(FOptionsEditorList[I].Instance) then
begin
try
Result := FOptionsEditorList[I].Instance.CanWeClose(SaveFlags, bForceSaving);
if oesfNeedsRestart in SaveFlags then
bNeedsRestart := True;
except
on E: Exception do
MessageDlg(FOptionsEditorList[I].Instance.GetTitle, E.Message, mtError, [mbOK], 0);
end;
end;
Inc(I);
end;
if bNeedsRestart then
MessageDlg(rsMsgRestartForApplyChanges, mtInformation, [mbOK], 0);
frmMain.UpdateWindowView; // Let's refresh the views.
// In fact, may settings would not really require it since they don't have an immediate visual impact.
// But let's do it for two reasons:
// 1st) Previously with "SaveConfig" it was updating it no matter what.
// 2nd) The little delay and visual blink it gives to user is a good feedback to him confirming him he just saved settings.
end;
procedure TfrmOptions.MakeVisible(Data: PtrInt);
var
TreeNode: TTreeNode absolute Data;
begin
TreeNode.MakeVisible;
end;
{ TfrmOptions.actCloseWithEscapeExecute }
procedure TfrmOptions.actCloseWithEscapeExecute(Sender: TObject);
begin
// Closing with the "Escape" key this way won't set the modalresult to mrCancel so this way, if an unsaved modification has been made, we'll be able to prompt confirmation from user who attempt to quit by hitting "Escape".
close;
end;
finalization
FreeAndNil(OptionsSearchCache);
FreeAndNil(OptionsSearchFile);
end.