Fixing SelectDirectory function

 Delphi  Comments Off on Fixing SelectDirectory function
May 032016
 

The Delphi VCL comes with several overloaded SelectDirectory functions declared in FileCtrl, one of which uses the ShBrowseForFolder Windows API function. It passes a callback function which tries to do the following:

  1. Position the dialog on the monitor on which the application’s main form is shown.
  2. Center the dialog on that monitor
  3. Select the given directory in the tree view.

Unfortunately in Delphi 2007 it fails for 2.5 of these points and even in Delphi 10.1 Berlin only one bug has been fixed:

  1. Since it fails to take into account that a monitor left or on top of the primary monitor has negative coordinates, the dialog will appear on the primary monitor if the application’s main form is located on such a monitor. This has been fixed in Delphi 10.1 (possibly earlier, I didn’t check)
  2. Centering the dialog on the monitor fails, at least on both of my computers running Windows 8.1, but I seem to remember that the same problem occurs on several other computers running Windows 7 and XP. This is still the case with Delphi 10.1.
  3. Selecting the given directory works, kind of, but if the tree view contains many entries, the selected entry will not be visible. You’ll have to scroll down to see it. This is still the case with Delphi 10.1

So, why is that? The main problem is that Delphi tries to change the dialog before it is fully visible. Setting the position fails because of this as well as making the selected directory visible.

My bugfix isn’t pretty, I must admit. It just defers these changes until the dialog is fully visible. This has the disadvantage the the user will see it popping up at the wrong place first before its position is corrected and the selected directory becomes visible.

But here it comes anyway:

First, you need to copy the code of SelectDirectory from FileCtrl, the one with the following signature:

function SelectDirectory(const Caption: string; const Root: WideString;
  var Directory: string; Options: TSelectDirExtOpts; Parent: TWinControl): Boolean;

In addition you need the function SelectDirCB which is declared immediately above SelectDirectory.

Copy it to a separate unit (or, if you are brave, directly modify FileCtrl).

Above these functions, add the following code (which is based on the existing TSelectDirCallback class declared an implemented in FileCtrl but heavily modified):


uses
  Windows,
  SysUtils,
  FileCtrl,
  Controls,
  Consts,
  ShlObj,
  ActiveX,
  Dialogs,
  Forms,
  Classes,
  u_dzVclUtils;

// ....

type
  TSelectDirCallback = class(TObject)
  private
    FParent: TWinControl;
    FDirectory: string;
    FInitialized: Boolean;
    FPositioned: Boolean;
  protected
    function SelectDirCB(Wnd: HWND; uMsg: UINT; lParam, lpData: lParam): Integer;
  public
    constructor Create(const ADirectory: string; _Parent: TWinControl);
  end;

{ TSelectDirCallback }

constructor TSelectDirCallback.Create(const ADirectory: string; _Parent: TWinControl);
begin
  inherited Create;
  FParent := _Parent;
  FDirectory := ADirectory;
  FInitialized := False;
  FPositioned := False;
end;

function TSelectDirCallback.SelectDirCB(Wnd: HWND; uMsg: UINT; lParam, lpData: lParam): Integer;

  procedure SetDialogPosition;
  var
    Rect: TRect;
    Monitor: TMonitor;
    ltwh: TRectLTWH;
    RefLtwh: TRectLTWH;
    frm: TCustomForm;
  begin
    GetWindowRect(Wnd, Rect);
    if Assigned(FParent) then begin
      frm := GetParentForm(FParent);
      Monitor := Screen.MonitorFromWindow(frm.Handle);
      RefLtwh.Assign(frm.BoundsRect);
    end else begin
      if Assigned(Application.MainForm) then
        Monitor := Screen.MonitorFromWindow(Application.MainForm.Handle)
      else
        Monitor := Screen.MonitorFromWindow(0);
      RefLtwh.Assign(Monitor.BoundsRect);
    end;
    ltwh.Assign(Rect);
    ltwh.Left := RefLtwh.Left + RefLtwh.Width div 2 - ltwh.Width div 2;
    ltwh.Top := RefLtwh.Top + RefLtwh.Height div 2 - ltwh.Height div 2;
    TMonitor_MakeFullyVisible(Monitor, ltwh);
    SetWindowPos(Wnd, 0, ltwh.Left, ltwh.Top, ltwh.Width, ltwh.Height, SWP_NOZORDER);
  end;

  procedure SelectDirectory;
  begin
    if FDirectory <> '' then begin
      // we use PostMessage to asynchronously select the directory
      PostMessage(Wnd, BFFM_SETSELECTION, Windows.wParam(True), Windows.lParam(PChar(FDirectory)));
    end;
  end;

begin
  Result := 0;
  if uMsg = BFFM_INITIALIZED then begin
    FInitialized := True;
    // It's too early to set the dialog position.
    // That only works once the dialog is visible.
    // But we must select the current directory, once here and once again
    SelectDirectory;
  end else if uMsg = BFFM_VALIDATEFAILED then begin
    MessageDlg(Format(SInvalidPath, [PChar(lParam)]), mtError, [mbOK], 0);
    Result := 1;
  end else if uMsg = BFFM_SELCHANGED then begin
    if FInitialized and not FPositioned then begin
      FPositioned := True;
      SetDialogPosition;
      // The first call to SelectDirectory only selects it but does not scrol the dialog
      // to make it visible. That's what this second call is for.
      SelectDirectory;
    end;
  end;
end;

Then modify SelectDirectory to pass the additional parameter Parent to the constructor:

// Initialization of the BrowseInfo record is done above
      SelectDirCallback := TSelectDirCallback.Create(Directory, Parent);
      try
        BrowseInfo.lParam := Integer(SelectDirCallback);

The TRectLTWH record as well as the TMonitor_MakeFullyVisible procedure is declared in u_dzVclUtils.

Ok, so, what does this do?

First of all, it does not try to position the dialog when receiving the BFFM_INITIALIZED message. In that state, the dialog isn’t yet visible and apparently cannot be positioned correctly. But it’s still necessary to set the selected directory at this time. My code also sets the FInitialized flag so it knows which of the multiple BFFM_SELCHANGED message to use to do the rest. And that’s actually all there is to it: When handling the first BFFM_SELCHANGED message after FInitialized was set, it sets the dialog position and selects the directory again.

Some details:

Fixing the first bug (not taking the Monitor.Left / .Top coordinates into account is done here:

    ltwh.Left := RefLtwh.Left + RefLtwh.Width div 2 - ltwh.Width div 2;
    ltwh.Top := RefLtwh.Top + RefLtwh.Height div 2 - ltwh.Height div 2;

Note that RefLtwh is initialized with the Monitor’s Left, Top, Width and Height properties.

Fixing the second bug is also part of that code and it works because the dialog is already visible.

Fixing the third bug involves using

PostMessage(Wnd, BFFM_SETSELECTION, Windows.wParam(True), Windows.lParam(PChar(FDirectory)));

rather than SendMessage and posting this messages twice. Once for selecting the directory and once again to let the dialog scroll so it becomes visible.

In addition to fixing these bugs, this code adds a feature: It tries to centre the dialog on the form passed as parent. No idea why Borland/Codegear/Embarcadero didn’t do that.

You can find this bugfix in my dzlib svn repository on SourceForge

Why blank lines matter

 Delphi, GExperts  Comments Off on Why blank lines matter
May 012016
 

This had me puzzled for a minute:

program CodeLibrarian;

{$R *.res}

uses
  GX_VerDepConst in '..\..\Framework\GX_VerDepConst.pas';

procedure ShowCodeLib; external GExpertsDll;
begin
  ShowCodeLib;
end.

This is all the code there is to the stand alone Code Librarian tool that comes with GExperts. Then it dawned me. It’s much easier to understand if you add a single line feed:

program CodeLibrarian;

{$R *.res}

uses
  GX_VerDepConst in '..\..\Framework\GX_VerDepConst.pas';

procedure ShowCodeLib; external GExpertsDll;

begin
  ShowCodeLib;
end.

The procedure ShowCodeLib is an external declaration that does not have a body. The begin / end are actually the main program that calls ShowCodeLib.

Enhancement for the Sort editor expert

 Delphi, GExperts  Comments Off on Enhancement for the Sort editor expert
Apr 232016
 

One of the editor experts in GExperts that come in handy once in a while is the Sort Expert. All it does is take the selected lines and sort them alphabetically.

Now, it can sort ascending, as before, descending (not sure when you might want to do that, but since I was at it, why not implement it?) and it can reverse the current line order. The latter is useful, if you want to undo changes in reverse order.

In addition, a new option allows to sort procedures / functions by name, ignoring the procedure / function prefix of the line, so

function c: integer;
procedure b;
function a: integer;

can be sorted like this:

function a: integer;
procedure b;
function c: integer;

rather than this:

function a: integer;
function c: integer;
procedure b;

Sort-Selected-Lines-Expert

FixInsight vs. GExperts

 Delphi, GExperts  Comments Off on FixInsight vs. GExperts
Apr 232016
 

Roman Yankovsky has been so kind to donate a FixInsight license to my open source projects, in particular to GExperts.

And since he just blogged about running FixInsight against the latest FMX I did the same with GExperts.

The result is not too bad actually. All included there are 235 warnings, optimization and convention messages. Most messages are about

  • Fields, that are not prefixed with F
  • Local Variables that hide class fields/methods/properties
  • const missing for unmodified string parameters
  • empty then blocks and empty procedures
  • for variables that are not used in the loop (all of them legit)

There are some warnings about variables assigned twice successively, but these are actually assignments to properties with side effects (e.g. TTimer.Enable being set to false and then to true to restart it).

The overall code quality of GExperts is very good compared to other source code I have seen.

I’ll go through all those messages and clean them up if possible.

Enabling a form while another form is being shown modally

 Delphi  Comments Off on Enabling a form while another form is being shown modally
Apr 092016
 

There was one shortcoming in my Delphi IDE explorer that has irked me since the beginning: It was disabled while a modal dialog was shown in the IDE, so there was no way to inspect the controls on the current dialog. The option to follow the focus ameliorated this a bit because it was now possible to click on the various controls in the dialog and see their properties, but still: There was no way to inspect non-clickable controls and how they were related to other controls on the form or to switch the different pages showing the properties, events, hierarchy or parents of the current control.

Delphi-Ide-Explorer-with-modal-form

(Click on the picture to see an animation how it works.)

Today, I remembered that I had asked a question on StackOverflow a few days ago: Why do modal Delphi forms not receive WM_SYSCOMMAND when the user clicks the task bar button?

In his answer David Heffernan mentioned the Windows API function EnableWindow and that it is being used by the VCL to disable all windows of an application when one of them is shown modally (and re-enable them afterwards). He explicitly warned me to meddle with that “The consequences are somewhat dire.”.

But being who I am, could of course not resist trying it anyway.

When I asked that question it was about a program at work that was showing a modal progress form which I wanted to allow to minimize the application and restore it when the user clicks on the task bar button. It did not work because the WM_SYSCOMMAND message never reached the program while the modal form was active. I worked around this by re-enabling the main window when the application was minimized (so it would receive the WM_SYSCOMMAND message again), and disabling it again once it got restored. It worked, and so far I have not seen any problems with that approach (but that might still be in store for me).

Today, I used a similar approach to enable the IDE explorer window while a modal form is active in the IDE. My first try was a TTimer that every second called EnabledWindow(Self.Handle, True). While this worked, I thought it might be a bit excessive to call that API every second, so I looked for a better place to put this code. How can a plugin be notified when a modal dialog is being shown? It turned out to be quite easy: Hook Screen.OnActiveFormChange, something I have done before.

And lo and behold: It works and again I have not yet experienced any problems with that approach.

EDIT:
As Ondrej Kelle pointed out in his comment on my Google+ post:

Watching for WM_ENABLE with wParam = 0 in your explorer form, checking if Application.ModalLevel > 0 (meaning a ShowModal call is currently executing), and re-enabling yourself might work, too, and you could avoid the Screen.OnActiveFormChange event hook. (Just an idea.)

He even provided an implementation for this:

/// [...]
interface
/// [...]
type
  TForm1 = class(TForm)
    /// [...]
  private
   procedure WMEnable(var _Msg: TWMEnable); message WM_ENABLE;
   /// [...]
  end;
/// [...]
implementation
/// [...]
procedure TExplorerForm.WMEnable(var _Msg: TWMEnable);
begin
  inherited;
  if not _Msg.Enabled and (Application.ModalLevel > 0) then
    EnableWindow(Self.Handle, True);
end;

And yes, it works. One hook less is good. Thanks Ondrey!

Do not enumerate on TTreeNode.Item

 Delphi  Comments Off on Do not enumerate on TTreeNode.Item
Apr 092016
 

Note to self: Do not enumerate on TTreeNode.Item, it’s highly inefficient.

Consider this code:

procedure SelectFocusedControl(_ActCtrl: TWinControl; _Parent: TTreeNode);
var
  j: Integer;
  CtrlItem: TTreeNode;
begin
  for j := 0 to _Parent.Count - 1 do begin
    CtrlItem := _Parent.Item[j];
    if CtrlItem.Data = _ActCtrl then begin
      CtrlItem.Selected := true;
      break;
    end else
      SelectFocusedControl(_ActCtrl, CtrlItem);
  end;
end;

This code is from my Delphi IDE Explorer expert (a bit simplified for readability).
It does something very simple: It goes through all child nodes of the given Parent TTreeNode and looks for one whose Data property matches the given ActCrl parameter. This node then gets selected.

I always wondered why this takes so long until I had a look at the implementation of TTreeNode.GetItem today:

function TTreeNode.GetItem(Index: Integer): TTreeNode;
begin
  Result := GetFirstChild;
  while (Result <> nil) and (Index > 0) do
  begin
    Result := GetNextChild(Result);
    Dec(Index);
  end;
  if Result = nil then TreeViewError(Format(SListIndexError, [Index]));
end;

This is the current implementation in Delphi 10 Seattle.

As you can see, in order to get the Index’ed item, it enumerates through all child items counting them until reaching the given Index. And since my code calls it for each item, this is highly inefficient.

Now, consider this alternative code:

procedure SelectFocusedControl(_ActCtrl: TWinControl; _Parent: TTreeNode);
var
  CtrlItem: TTreeNode;
begin
  CtrlItem := _Parent.getFirstChild;
  while Assigned(CtrlItem) do begin
    if CtrlItem.Data = _ActCtrl then begin
      CtrlItem.Selected := true;
      break;
    end else
      SelectFocusedControl(_ActCtrl, CtrlItem);
    CtrlItem := CtrlItem.getNextSibling;
  end;
end;

This uses basically the same code as TTreeNode.GetItem, so it only loops through the items once. Much faster.

EDIT:
I deleted the rest of this post regarding TTreeNodesEnumerator. It was clearly wrong.

More enhancements for the search path dialog

 Delphi, GExperts  Comments Off on More enhancements for the search path dialog
Apr 022016
 

After I found and fixed the problem with the Grep results dialog I returned to enhancing the search path dialog. Last time I added an option to replace the ListBox on that dialog with a Memo. Many people liked that change but of course, people being people, they started to complain about missing features. In particular the option to easily spot invalid paths and remove them with a single button click was sorely missed. I even found myself missing that ListBox sometimes so I thought: Why not have both, the ListBox and the Memo on the form? It turned out to be both, easier and more complex than I thought, but here it is, the new enhanced search path dialog:

GExperts-PageControl-in-SearchPathEdit-Small
(Click on the image to get an animated introduction to the new features.)

So, what’s new?

  • There is now a tab below the list that switches between the ListBox and the Memo.
  • When switching between them, the current line is preserved.
  • When the ListBox is active, all buttons work as they used to.
  • The Up and Down buttons now have a keyboard shortcut (Ctrl+Up / Ctrl+Down) and work in both, the ListBox and the Memo.
  • Dropping directories now works on all three, the ListBox, the Memo and the Edit control. It’s also now possible to drop multiple directories
  • The OK Button now has a hotkey (Something that irked my since forever, but I always forgot to add it.)

There isn’t a new release yet, so in order to get these goodies, you’ll have to check out the sources and compile GExperts yourself.

Safe event hooking for Delphi IDE plugins revisited

 Delphi, GExperts  Comments Off on Safe event hooking for Delphi IDE plugins revisited
Mar 282016
 

A while ago I blogged about Safe event hooking for Delphi IDE plugins. I have used this method in GExperts as well as my Delphi IDE Explorer and it worked fine for both plugins.

Today I looked at the code again and didn’t like it very much. Yes, it works fine and there aren’t any apparent bugs, but it blatantly violates the DRY (don’t repeat yourself) principle. Back, when I wrote it, my goal was to get something working fast so I could use it for my main goal: Make GExperts and the IDE explorer coexist peacefully within the same Delphi instance, even though they both hook Screen.OnFormChange and Screen.OnControlChange. Today, my goal changed. It’s now: Improve the code.

So, what’s the problem?

First, there must be a Hook-class for each event type. There isn’t much that can be done about this since the method signature of each event is different and we don’t want to resort to assembly language. Also, we can’t use generics because I want the code to work with ancient Delphi versions back to Delphi 6 that just didn’t have generics. So, unfortunately I can’t think of any way to improve on that.

Secondly, there are the simple Hook function and the rather complex Unhook procedure. These are also duplicated, not only for each event type but also for each actual event we want to hook. That’s a really bad code smell. What can be be done to improve on that? First I thought I could just abstract from the actual event by passing a pointer to the memory that stores the event. Unfortunately this doesn’t work because an event might have setter and getter methods. So, what about passing procedure pointers for getting and setting that event? Yes, that would work but it would also be so 1980ies an C-ish. Improve on that, pass an object with getter and setter methods? Yes, but I don’t want to create such an object and free it later. Use an extended record? Not available before Delphi 2006. Use an interface? Too much boiler plate code to be worth it. Then it struck me: Why not use class methods? So the first idea was a class like this:

type
  TScreenOnFormChangeAccess = class
  public
    class function GetEvent: TMethod;
    class procedure SetEvent(_Value: TMethod);
  end;

// [...]

class function TScreenOnFormChangeAccess.GetEvent: TMethod;
begin
  Result := TMethod(Screen.OnFormChange);
end;

class procedure TScreenOnFormChangeAccess.SetEvent(_Value: TMethod);
begin
  Screen.OnFormChange := TNotifyEvent(_Value);
end;

I could pass this class to the Hook/Unhook functions to abstract the code for getting/setting the event from the actual program logic.

Great! Got rid of one copy of the Hook/Unhook code: Un-/HookScreenOnFormChange and Un-/HookScreenOnControlChange became simple functions that called a Un-/HookNotifyEvent function, passing it appropriate classes. And since everything was class methods, no class instance had to be created.

function HookScreenActiveFormChange(_HookEvent: TNotifyEvent): TNotifyEventHook;
begin
  Result := HookNotifyEvent(TScreenOnFormChangeAccess, TMethod(_HookEvent));
end;

procedure UnhookScreenActiveFormChange(_Hook: TNotifyEventHook);
begin
  UnhookNotifyEvent(TScreenOnFormChangeAccess, _Hook);
end;

Once I had figured out this solution, there was another thought: Since I already have a class, why not make Hook/Unhook also methods of this class and make Get/SetEvent virtual class methods that are called by Hook/Unhook? At first, I was not sure whether Delphi 6 already had virtual class methods, but it turned out, it had. So, we get to a more object oriented implementation:

First, there is a pure abstract ancestor class TSecureEventHook:

type
  ///<summary>
  /// This class provides the abstract declarations of two class methods:
  /// GetEvent returns the original event handler, typecasted to TMethod
  /// SetEvent sets the replacement event handler, typecated from TMethod
  /// Both methods must be overriden to access an actual event. </summary>
  TSecureEventHook = class
  protected
    class function GetEvent: TMethod; virtual; abstract;
    class procedure SetEvent(_Value: TMethod); virtual; abstract;
  end;

All it does, is declaring two virtual abstract class methods GetEvent and SetEvent. Since these events are treated as TMethod (that is: A record with a Data and a Code pointer which is what an event pointer really is.) this can be agnostic of the actual event type.

Now, for each event type, we need a specialized class, e.g. for a TNotifyEvent:

type
  ///<summary>
  /// Provides the Methods Install and Remove to hook/unhook an event of type TNotifyEvent
  /// It uses the inherited virtual methods GetEvent and SetEvent to access the actual
  /// event. Since GetEvent/SetEvent are still abstract, a descendant is required
  /// that implements them. </summary>
  TSecureNotifyEventHook = class(TSecureEventHook)
  public
    class function Install(_HookEvent: TNotifyEvent): TNotifyEventHook;
    class procedure Remove(_Hook: TNotifyEventHook);
  end;

This class introduces two class methods: Install and Remove. They have the same signature as the old HookScreenActiveControlChange function and UnhookScreenActiveControlChange procedure. But since they call the inherited GetEvent/SetEvent methods, they don’t need to know which event they are hooking. I’ll come back to the implementation later.

The last building block is a class that actually implements GetEvent/SetEvent for a particular event, let’s say Screen.OnActiveFormChange. We already had that above, but this time we must derive from TSecureNotifyEventHook and override the methods inherited from TSecureEventHook:

type
  ///<summary>
  /// Implements the GetEvent/SetEvent methods to access Screen.ActiveFormChange </summary>
  TScreenActiveFormChangeHook = class(TSecureNotifyEventHook)
  protected
    class function GetEvent: TMethod; override;
    class procedure SetEvent(_Value: TMethod); override;
  end;

So, in order to hook / unhook the Screen.OnActiveFormChange event, we call:

  FHook := TScreenActiveFormChangeHook.Install(MyHookMethod);
  // [...]
  TScreenActiveFormChangeHook.Remove(FHook);

The neat part is, that since we now have an object oriented implementation, adding support to hook another TNotifyEvent simply requires adding another descendant from TSecureNotifyEventHook, e.g. for Screen.OnActiveControlChange:

type
  ///<summary>
  /// Implements the GetEvent/SetEvent methods to access Screen.ActiveControlChange </summary>
  TScreenActiveControlChangeHook = class(TSecureNotifyEventHook)
  protected
    class function GetEvent: TMethod; override;
    class procedure SetEvent(_Value: TMethod); override;
  end;

// [...]

class function TScreenActiveControlChangeHook.GetEvent: TMethod;
begin
  Result := TMethod(Screen.OnActiveControlChange);
end;

class procedure TScreenActiveControlChangeHook.SetEvent(_Value: TMethod);
begin
  Screen.OnActiveControlChange := TNotifyEvent(_Value);
end;

What’s missing? The actual Hook/Unhook code, now in the Install/Remove class methods:

{ TSecureNotifyEventHook }

class function TSecureNotifyEventHook.Install(_HookEvent: TNotifyEvent): TNotifyEventHook;
var
  evt: TNotifyEvent;
begin
  // Here we use the inherited virtual GetEvent method which
  // will be overridden by descendants to access an actual event.
  Result := TNotifyEventHook.Create(GetEvent, TMethod(_HookEvent));
  evt := Result.HandleEvent;
  // Here we use the inherited virtual SetEvent method which
  // will be overridden by descendants to access an actual event.
  SetEvent(TMethod(evt));
end;

class procedure TSecureNotifyEventHook.Remove(_Hook: TNotifyEventHook);
var
  Ptr: TMethod;
begin
  if not Assigned(_Hook) then begin
   // Just in case somebody did not check whether HookScreenActiveFormChange actually returned
   // a valid object or simply didn't call it.
    Exit;
  end;

  // Here we use the inherited virtual GetEvent method which
  // will be overridden by descendants to access an actual event.
  Ptr := GetEvent;
  if not Assigned(Ptr.Data) and not Assigned(Ptr.Code) then begin
    // Somebody has assigned NIL to the event.
    // It's probably safe to assume that there will be no reference to our hook left, so we just
    // free the object and be done.
    _Hook.Free;
    Exit;
  end;

  while TObject(Ptr.Data).ClassNameIs('TNotifyEventHook') do begin
    // Somebody who knows about this standard has hooked the event.
    // (Remember: Do not change the class name or the class structure. Otherwise this
    //  check will fail!)
    // Let's check whether we can find our own hook in the chain.
    if Ptr.Data = _Hook then begin
      // We are lucky, nobody has tampered with the event, we can just assign the original event,
      // free the hook object and be done with it.
      // Here we use the inherited virtual SetEvent method which
      // will be overridden by descendants to access an actual event.
      SetEvent(_Hook.OrigEvent);
      _Hook.Free;
      Exit;
    end;
    // check the next event in the chain
    Ptr := TMethod(TNotifyEventHook(Ptr.Data).OrigEvent);
  end;

  // If we get here, somebody who does not adhere to this standard has changed the event.
  // The best thing we can do, is Assign NIL to the HookEvent so it no longer gets called.
  // We cannot free the hook because somebody might still have reference
  // to _Hook.HandleEvent.
  _Hook.HookEvent.Code := nil;
  _Hook.HookEvent.Data := nil;
end;

Note that the code to actually get and set the event has been abstracted to call the virtual GetEvent/SetEvent methods.

There still remains one copy of the Hook/Unhook code per event type. I think I can get rid of that as well, but that’s for another blog post. Also, I am not yet sure whether it is possible to get rid of the separate TNotifyEventHook class by just merging it with the TSecureNotifyEventHook class. I’ll have to check if/how adding virtual class methods changes the class’ VMT and thus, the memory structure. I want it to stay backwards compatible to the original proposal, even though I haven’t heard back from anybody who is using it (which probably means nobody but myself is using it.)

If your OnePlus One won’t turn on but only vibrates

 Android  Comments Off on If your OnePlus One won’t turn on but only vibrates
Mar 192016
 

Note to self: If your OnePlus One smartphone won’t turn on but only vibrates for a short time when you press the power button, that probably means the battery is dead. If it has already been on the charger for some time, make sure that you didn’t plug the charging cable upside down into the charger (it fits both ways but only one way works). If that isn’t the problem, try a different cable and/or charger.

Calculating Offsets into the Delphi editor buffer

 Delphi, GExperts  Comments Off on Calculating Offsets into the Delphi editor buffer
Mar 122016
 

I have already mentioned the AutoTodo wizard for Delphi when I was trying to contact the Author Peter Laman. He hasn’t responded and nobody could give me any contact information. (Peter, if you ever read this, please contact me using my Google+ profile.)

The animated GIF in that post shows how the new AutoTodo expert in GExperts works. Unfortunately it turned out later that, again, I had overlooked a Unicode issue. If the editor buffer contains any Unicode characters, the offsets for inserting the todos where off by one for each of these characters, so the todos were inserted in the middle of the source code rather than at the empty blocks.

unit bla;
// ä <--- beware, there's a Unicode character here!
interface

implementation

procedure blub;
begi  //TODO 5 -otwm -cEmpty Structure : blub (begin/end in procedure)n
end;

end.

The reason, of course is that starting with Delphi 8 the IDE uses UTF-8 for its editor buffers, so any offsets into these buffers have to take characters into account that take up more than one byte.

The easiest way to do that is not using offsets at all but Line/CharIndex positions as stored in the TOTACharPos record. IOTAEditView provides two methods for converting a TOTACharPos to a buffer offset and vice versa:

type
  IOTAEditView40 = interface(IInterface)
    // ...
    { Converts a linear buffer offset position to a CharPos }
    function PosToCharPos(Pos: Longint): TOTACharPos;
    { Convert a CharPos to a linear buffer offset }
    function CharPosToPos(CharPos: TOTACharPos): Longint;
    // ...
  end;

So, if you want to store any positions, never use the buffer offset but use the CharPos instead.

But what if your algorithm only works with offsets? And if these offsets are not into UTF-8 strings but into Unicode strings? You need a way to calculate the CharPos from your offset and then use CharPosToPos to calculate the buffer offset.

In this case, the algorithm of the AutoTodo wizard which I used as a base for the GExperts AutoTodo Expert generated a TStringList with text to insert into the source code where the Objects[] property stored the character index into the source code string:

  // get the source code from the current editor window into the
  // string Source and pass it to the AutoTodo handler:
  Patches := TStringList.Create;
  Handler.Execute(Source, Patches);
  // and now what?

I am a lazy basterd™, so the first thing I looked for was some existing source code for converting the character index to a line index / character position. I found nothing in the TStringList interface and nothing in the Delphi RTL. A Google search didn’t give me any useful results (I might have used the wrong search terms.). Even the Google+ Delphi Developer community refused to support my lazyness by pointing me to a ready made algorithm. So I had to roll my own.

This class takes a StringList (TGXUnicodeStringList is just a regular StringList for most purposes) with a multi line string and calculates the offsets for the first characters of all lines. These offsets are stored in the FOffsets array. After this is done, it can easily and reasonably efficient calculate the line index and the character position in that line from the character position in the multi line string stored in the StringList.

type
  TOffsetToCursorPos = class
  private
    FOffsets: array of integer;
  public
    constructor Create(_sl: TGXUnicodeStringList);
    function CalcCursorPos(_Offset: integer): TPoint;
  end;


{ TOffsetToCursorPos }

constructor TOffsetToCursorPos.Create(_sl: TGXUnicodeStringList);
var
  cnt: integer;
  i: Integer;
  CrLfLen: integer;
  Ofs: Integer;
begin
  inherited Create;
{$IFDEF GX_VER190_up}
  CrLfLen := Length(_sl.LineBreak);
{$ELSE}
  // Delphi < 2007 does not have the LineBreak property
  CrLfLen := 2;
{$ENDIF}
  cnt := _sl.Count;
  SetLength(FOffsets, cnt);
  Ofs := 1;
  for i := 0 to _sl.Count - 1 do begin
    FOffsets[i] := Ofs;
    Inc(Ofs, Length(_sl[i]) + CrLfLen);
  end;
end;

function TOffsetToCursorPos.CalcCursorPos(_Offset: integer): TPoint;
var
  i: integer;
begin
  i := 0;
  while (i < Length(FOffsets)) and (_Offset >= FOffsets[i]) do begin
    Inc(i);
  end;
  Result.Y := i - 1;
  Result.X := _Offset - FOffsets[Result.Y] + 1;
end;

Not too complicated, but let my tell you: It took me quite a while to get it right and make it compile with all affected Delphi versions.

The while loop in CalcCursorPos could probably be replaced with a binary search because the FOffsets array by definition is sorted.

Now, all I had to do was passing the offsets from the patch array to CalcCursorPos and then use CharPosToPos to calculate the buffer offset.

Easy, isn’t it?