{ ***************************************************************************** See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** Author: Mattias Gaertner Abstract: Methods and types for CHM help using chm viewer "lhelp". } unit LazHelpCHM; {$mode objfpc}{$H+} {$IFDEF VerboseLCLHelp} {$DEFINE VerboseChmHelp} {$ENDIF} interface uses Classes, SysUtils, LazHelpIntf, LazConfigStorage, HelpIntfs, Dialogs, Forms, LazLoggerBase, FileUtil, LazFileUtils, LHelpControl, LResources; const CHMMimeType = 'application/chm'; CHMPathParam = 'path'; type { TCHMHelpDatabase KeywordPrefix: if set, then the database will handle all Keywords beginning with this value. And when the path is created by replacing the prefix with the BaseURL. For example: Create a chm. For example build and run chmmaker in lazarus/tools/chmmaker to create the example.chm (lazarus/tools/chmmaker/example.chm). Put a TCHMHelpDatabase on a form. Set AutoRegister to true. Set KeywordPrefix to 'example' Set CHM file to '../../../tools/chmmaker/example.chm' Put a TLHelpRemoteViewer on the form. Set AutoRegister to true. Set LHelpPath to the path of lhelp. E.g. '../../lhelp/lhelp' Put a TEdit on a form. Set HelpType to htKeyword Set HelpKeyword to 'example/MainPage.html' Run the program. Focus the edit field and press F1. The page '/MainPage.html' will be shown. Note: lhelp requires the leading slash. } TCHMHelpDatabase = class(THelpDatabase) private FFilename: string; FHelpNode: THelpNode; FKeywordPrefix: string; procedure SetFilename(AValue: string); procedure SetKeywordPrefix(AValue: string); public constructor Create(TheOwner: TComponent); override; destructor Destroy; override; function ShowHelp({%H-}Query: THelpQuery; {%H-}BaseNode, NewNode: THelpNode; {%H-}QueryItem: THelpQueryItem; var ErrMsg: string): TShowHelpResult; override; function ShowURL(const URL, Title: string; var ErrMsg: string): TShowHelpResult; virtual; function GetNodesForKeyword(const HelpKeyword: string; var ListOfNodes: THelpNodeQueryList; var ErrMsg: string): TShowHelpResult; override; procedure Load(Storage: TConfigStorage); override; procedure Save(Storage: TConfigStorage); override; published property AutoRegister; property Filename: string read FFilename write SetFilename; property KeywordPrefix: string read FKeywordPrefix write SetKeywordPrefix; end; type TOnFindLHelp = procedure(var Path: string) of object; { TLHelpConnector } TLHelpConnector = class(THelpViewer) private FConnection: TLHelpConnection; FLHelpPath: string; FOnFindLHelp: TOnFindLHelp; procedure SetLHelpPath(AValue: string); public constructor Create(TheOwner: TComponent); override; destructor Destroy; override; function ShowNode(Node: THelpNode; var ErrMsg: string): TShowHelpResult; override; procedure Assign(Source: TPersistent); override; procedure Load(Storage: TConfigStorage); override; procedure Save(Storage: TConfigStorage); override; function GetLocalizedName: string; override; property OnFindLHelp: TOnFindLHelp read FOnFindLHelp write FOnFindLHelp; property Connection: TLHelpConnection read FConnection; published property LHelpPath: string read FLHelpPath write SetLHelpPath; property AutoRegister; end; procedure Register; implementation {$R lazhelpchm.res} procedure Register; begin RegisterComponents('System',[TCHMHelpDatabase,TLHelpConnector]); end; { TLHelpConnector } procedure TLHelpConnector.SetLHelpPath(AValue: string); begin if FLHelpPath=AValue then Exit; FLHelpPath:=AValue; end; constructor TLHelpConnector.Create(TheOwner: TComponent); begin inherited Create(TheOwner); AddSupportedMimeType(CHMMimeType); end; destructor TLHelpConnector.Destroy; begin FConnection.Free; inherited; end; function TLHelpConnector.ShowNode(Node: THelpNode; var ErrMsg: string ): TShowHelpResult; var Path: String; IPCFile: String; URLScheme: string; URLPath: string; URLParams: string; CHMFilename: String; SubPath: String; Response: TLHelpResponse; s: String; begin {$IFDEF VerboseChmHelp} debugln(['TLHelpConnector.ShowNode START URL="',Node.URL,'"']); {$ENDIF} Result:=shrViewerError; ErrMsg:=''; if (not Node.URLValid) then begin ErrMsg:='TLHelpConnector.ShowNode Node.URLValid=false'; exit; end; if (Node.URL='') then begin ErrMsg:='TLHelpConnector.ShowNode Node.URL empty'; exit; end; SplitURL(Node.URL,URLScheme,URLPath,URLParams); CHMFilename:=CleanAndExpandFilename(URLPath); if not FileExistsUTF8(CHMFilename) then begin ErrMsg:='chm file "'+CHMFilename+'" not found'; exit; end; if DirPathExists(CHMFilename) then begin ErrMsg:='invalid chm file "'+CHMFilename+'"'; exit; end; SubPath:=''; if (URLParams<>'') and (URLParams[1]='?') then Delete(URLParams,1,1); if LeftStr(URLParams,length(CHMPathParam)+1)=CHMPathParam+'=' then begin SubPath:=URLParams; Delete(SubPath,1,length(CHMPathParam)+1); end; if Connection=nil then begin // create a connection to lhelp: FConnection := TLHelpConnection.Create; Connection.ProcessWhileWaiting := @Application.ProcessMessages; end; if Connection.ServerRunning = false then begin // Use '_lhlpctl_' in case application developer uses SimpleIPC // and also uses the exe name followed by the process ID. // See help protocol specs defined in // http://wiki.lazarus.freepascal.org/Help_protocol // Use process id in order to avoid conflicts when multiple entries are running IPCFile:=LowerCase(ExtractFileName(Application.ExeName))+ '_lhlpctl_'+ copy(inttostr(GetProcessID)+'00000',1,5); {$IFDEF Unix} if FileExistsUTF8('/tmp/'+IPCFile) then DeleteFileUTF8('/tmp/'+IPCFile); {$ENDIF} // get lhelp path Path:=LHelpPath; if Assigned(OnFindLHelp) then OnFindLHelp(Path); // append exe extension if (ExtractFileExt(Path)='') and (GetExeExt<>'') then Path:=Path+GetExeExt; // search in Path if (Path<>'') and (ExtractFilePath(Path)='') then begin s:=FindDefaultExecutablePath(Path); if s<>'' then Path:=s; end; if not FileExistsUTF8(Path) then begin ErrMsg:='The chm viewer program lhelp was not found at "'+Path+'"'; exit; end; Connection.StartHelpServer(IPCFile,Path); end; {$IFDEF VerboseChmHelp} debugln(['TLHelpConnector.ShowNode CHMFilename="',CHMFilename,'" SubPath="',SubPath,'"']); {$ENDIF} Response:=Connection.OpenURL(CHMFilename,SubPath); case Response of srSuccess: exit(shrSuccess); srNoAnswer: ErrMsg:='lhelp does not respond'; srInvalidFile: ErrMsg:='lhelp can not open the file "'+CHMFilename+'"'; srInvalidURL,srInvalidContext: ErrMsg:='lhelp can not find the help entry "'+SubPath+'"'; else ErrMsg:='Something is wrong with lhelp'; end; debugln(['TLHelpConnector.ShowNode error: ',ErrMsg]); end; procedure TLHelpConnector.Assign(Source: TPersistent); var Src: TLHelpConnector; begin if Source is TLHelpConnector then begin Src:=TLHelpConnector(Source); LHelpPath:=Src.LHelpPath; end; inherited Assign(Source); end; procedure TLHelpConnector.Load(Storage: TConfigStorage); begin inherited Load(Storage); LHelpPath:=Storage.GetValue('LHelp/Path',''); end; procedure TLHelpConnector.Save(Storage: TConfigStorage); begin inherited Save(Storage); Storage.SetDeleteValue('LHelp/Path',LHelpPath,''); end; function TLHelpConnector.GetLocalizedName: string; begin Result:='LHelp Connector'; end; { TCHMHelpDatabase } procedure TCHMHelpDatabase.SetFilename(AValue: string); begin if FFilename=AValue then Exit; FFilename:=AValue; end; procedure TCHMHelpDatabase.SetKeywordPrefix(AValue: string); begin if FKeywordPrefix=AValue then Exit; FKeywordPrefix:=AValue; end; constructor TCHMHelpDatabase.Create(TheOwner: TComponent); begin inherited Create(TheOwner); AddSupportedMimeType(CHMMimeType); end; destructor TCHMHelpDatabase.Destroy; begin FreeAndNil(FHelpNode); inherited Destroy; end; function TCHMHelpDatabase.ShowHelp(Query: THelpQuery; BaseNode, NewNode: THelpNode; QueryItem: THelpQueryItem; var ErrMsg: string ): TShowHelpResult; begin ErrMsg:=''; Result:=shrContextNotFound; if NewNode.URLValid then begin Result:=ShowURL(NewNode.URL,NewNode.Title,ErrMsg); end else begin Result:=shrContextNotFound; ErrMsg:='TCHMHelpDatabase.ShowHelp Node.URLValid=false Node.URL="'+NewNode.URL+'"'; end; end; function TCHMHelpDatabase.ShowURL(const URL, Title: string; var ErrMsg: string ): TShowHelpResult; var Viewer: THelpViewer; Node: THelpNode; begin //DebugLn('TCHMHelpDatabase.ShowURL A URL="',URL,'" Title="',Title,'"'); if not FileExistsUTF8(Filename) then begin ErrMsg:='chm help file "'+Filename+'" not found'; exit(shrDatabaseNotFound); end; // find HTML viewer Result:=FindViewer(CHMMimeType,ErrMsg,Viewer); if Result<>shrSuccess then exit; // call viewer Node:=nil; try Node:=THelpNode.CreateURL(Self,Title,URL); Result:=Viewer.ShowNode(Node,ErrMsg); finally Node.Free; end; end; function TCHMHelpDatabase.GetNodesForKeyword(const HelpKeyword: string; var ListOfNodes: THelpNodeQueryList; var ErrMsg: string): TShowHelpResult; var Path: String; begin Result:=inherited GetNodesForKeyword(HelpKeyword, ListOfNodes, ErrMsg); if Result<>shrSuccess then exit; if not (csDesigning in ComponentState) and (KeywordPrefix<>'') and (LeftStr(HelpKeyword,length(KeywordPrefix))=KeywordPrefix) then begin // HelpKeyword starts with KeywordPrefix -> add default node if FHelpNode=nil then FHelpNode:=THelpNode.CreateURL(Self,'',''); Path:=copy(HelpKeyword,length(KeywordPrefix)+1,length(HelpKeyword)); FHelpNode.Title:='Show page '+Path+' of '+ExtractFileName(Filename); FHelpNode.URL:='chmfile://'+FilenameToURLPath(Filename)+'?'+CHMPathParam+'='+Path; CreateNodeQueryListAndAdd(FHelpNode,nil,ListOfNodes,true); end; end; procedure TCHMHelpDatabase.Load(Storage: TConfigStorage); begin inherited Load(Storage); KeywordPrefix:=Storage.GetValue('KeywordPrefix',''); Filename:=Storage.GetValue('Filename',''); end; procedure TCHMHelpDatabase.Save(Storage: TConfigStorage); begin inherited Save(Storage); Storage.SetDeleteValue('KeywordPrefix',KeywordPrefix,''); Storage.SetDeleteValue('Filename',Filename, ''); end; end.