I don’t know whether its an issue with my Delphi XE7 (I’ll find out when Seattle 10 arrives) but it doesn’t seem to have any Win32/64 help in the system anymore. I’m assuming that this is related to the space in the help required by the new FireMonkey and cross platform support. Since most of my development is in the Win32/64 world I find this frustrating so I decided to do something about it on Saturday.
What trigger this was while finalizing the OTA Book I came across an interface IOTAHelpServices
and wondered whether I could intercept calls to the help system and if they are not handled display some search information from say Google.
If you have followed my previous posts you will have created a wizard to create wizards. So rather than go through all the building of a new expert/wizard I’ve used my own tools to get most of the framework in place.
Eating your own dog food
…At least I think that the expression for using your own software.
If you haven’t worked through the previous chapters then you can download the complete wizard from Chapter 15: IDE Main Menus or use the code with Chapter 4: Key Bindings and Debugging Tools as a template.
Once you have create the template and saved the files there are a few things you will need to do to get the project to compile properly.
Enable Packages
When I did this I selected a DLL project from the wizard so to get the project to compile (and find ToolsAPI.pas
) you need to enabled packages and use at least the following packages:
- RTL;
- VCL;
- DesignIDE.
Your project should now compile BUT you will get some warnings.
Updating ConditionalDefinitions.inc
Depending upon your version of RAD Studio / Delphi the conditional compilation include file created by the wizard will not handle your version of the compiler (it only went up to XE2 not 11 – sorry couldn’t resist). You need to extend the conditions far enough to cover your compiler (the pattern should be obvious and has been explain in Conditional Compilation of Open Tools API Experts).
The OTA Code
OTAHelpServices Interface
Below is the definition of the IOTAHelpServices
from the XE7 ToolsAPI.pas
file.
IOTAHelpServices = interface(IDispatch) ['{25F4CC12-EA93-4AEC-BC4A-DFDF427053B0}'] procedure ShowKeywordHelp(const Keyword: WideString); safecall; function UnderstandsKeyword(const Keyword: WideString): WordBool; safecall; procedure ShowContextHelp(ContextID: Integer); safecall; procedure ShowTopicHelp(const Topic: WideString); safecall; function GetFileHelpTrait(const FileName: WideString): IOTAHelpTrait; safecall; function GetPersonalityHelpTrait(const Personality: WideString): IOTAPersonalityHelpTrait; safecall; end;
Below are my understanding of their function as there are no comments in the file (note, I only use 1 of these methods for this expert):
ShowKeywordHelp
I believe that this method will display the appropriate help page for a given keyword.
UnderstandsKeyword
This function returns true if the given Keyword
is understood by the IDE, i.e. there is a help topic for the Keyword else the function will return false. This method raises an exception if an empty string is passed as the Keyword. This is the function I use to determine whether the IDE can handle the Identifier under the cursor.
ShowContextHelp
This method will show the context help for the given context ID (integer). This is similar to WinHelp and HTMLHelp however I’m not sure where you would find the context numbers unless you can integrate your help with the IDE.
ShowTopicHelp
This method I believe allows you to open a help topic based on its description rather than just a help topic based on a Keyword.
GetFileHelpTrait
Not really sure about this one other than it would seem to return a IOTAHelpTrait
for a given filename but there is no further references to what a IOTAHelpTrait
is other than the definition below:
IOTAHelpTrait = interface(IDispatch) ['{DEE36173-1597-498A-A85A-C90BFCAE9B74}'] end;
You could perhaps surmise that this will allow you to get a string which represents the personality the file belongs to in the IDE.
GetPersonalityHelpTrait
This method allows you to get access to personality specific help by specifying the IDE personality (one of the predefined strings in ToolsAPI.pas
) which then provides access to personality specific implementations of ShowKeywordHelp
and UnderstandsKeyword
as described above (see definition of the IOTAPersonalityHelpTrait
below).
IOTAPersonalityHelpTrait = interface(IDispatch) ['{914E82DB-4123-4AA8-91D9-DB105E1FEC64}'] procedure ShowKeywordHelp(const Keyword: WideString); safecall; function UnderstandsKeyword(const Keyword: WideString): WordBool; safecall; end;
Getting the Identifier at the Cursor
In order to be able to first ask the IDE if it understand a Keyword/Identifier and then secondly search for that on the internet I need to get the word underneath the cursor in the IDE’s editor. To do this (see below code) I use one of the utility functions (see Chapter 5: Useful Open Tools Utility Functions) EditorAsString
which returns all the text of the current editor as a string. I then put that into a string list to get it into individual lines and then see if there is a word under the column position of the cursor. If so I trace the start and end of the word and then return that word from this function. If there is no word under the cursor I return a null string.
Function TKeybindingTemplate.GetWordAtCursor: String; Const strIdentChars = ['a'..'z', 'A'..'Z', '_', '0'..'9']; Var SE: IOTASourceEditor; EP: TOTAEditPos; iPosition: Integer; sl: TStringList; Begin Result := ''; SE := ActiveSourceEditor; EP := SE.EditViews[0].CursorPos; sl := TStringList.Create; Try sl.Text := EditorAsString(SE); Result := sl[Pred(EP.Line)]; iPosition := EP.Col; If (iPosition > 0) And (Length(Result) >= iPosition) And CharInSet(Result[iPosition], strIdentChars) Then Begin While (iPosition > 1) And (CharInSet(Result[Pred(iPosition)], strIdentChars)) Do Dec(iPosition); Delete(Result, 1, Pred(iPosition)); iPosition := 1; While CharInSet(Result[iPosition], strIdentChars) Do Inc(iPosition); Delete(Result, iPosition, Length(Result) - iPosition + 1); If CharInSet(Result[1], ['0'..'9']) Then Result := ''; End Else Result := ''; Finally sl.Free; End; End;
Determining if the IDE’s Help can handle the Identifier
When I first implemented this expert I passed the search URL to ShellExecute
to bring the search up in the default browser but like most thing I do it grew legs and now implements a dockable form with a TWebBrowser embedded in it. I’m not going to go through this implementation especially the web browser portion as this is all about OTA but I’m not going to go through the dockable form either. Why? Well that’s in the book… coming to a web site near you soon.
In essence the code gets the Keyword at the cursor as described above and if its a valid word asks the IDE if it has any help for the word. If not then the word is embedded in a search URL and passed to the dockable browser (or you could simply use ShellExecute
).
Procedure TKeybindingTemplate.ProcessKeyBinding(Const Context: IOTAKeyContext; KeyCode: TShortcut; Var BindingResult: TKeyBindingResult); Const strMsg = 'Your search URLs are misconfigured. Ensure there is a Search URL and that ' + 'it is checked in the list in the configuration dialogue.'; Var strWordAtCursor : String; boolHandled: Boolean; Begin strWordAtCursor := GetWordAtCursor; If strWordAtCursor <> '' Then Begin boolHandled := (BorlandIDEServices As IOTAHelpServices).UnderstandsKeyword(strWordAtCursor); If boolHandled Then BindingResult := krUnhandled Else Begin BindingResult := krHandled; If (AppOptions.SearchURLIndex <= AppOptions.SearchURLs.Count - 1) And (AppOptions.SearchURLIndex >= 0) Then TfrmDockableBrowser.Execute( Format(AppOptions.SearchURLs[AppOptions.SearchURLIndex], [strWordAtCursor])) Else MessageDlg(strMsg, mtError, [mbOK], 0); End; End Else BindingResult := krUnhandled; End;
Code
The RAD Studio XE7 code for this article can be downloaded from the page IDE Help Helper or directly from this link.
Hope this proves helpful to all.
regards
Dave.