Interpreting Delphi IDE Keyboard Macros (Updated)

 Delphi, GExperts, Uncategorized  Comments Off on Interpreting Delphi IDE Keyboard Macros (Updated)
Mar 042017
 

Keyboard macros have been part of the Delphi IDE since basically forever (I remember using them in Delphi 5 but I wouldn’t rule it out that they already existed in Delphi 1 which I never used.)

GExperts also has had the Macro Library Expert since I know about it to overcome the Delphi IDE’s shortcoming of having only one keyboard macro.

This expert handles the macros as a black box, storing them in a binary stream which is used to get them from and pass them to the IDE and to save and load them from files.

So far I haven’t seen any documentation of the format these macros are stored in. That’s a real shame because it prevented the Macro Library Expert to become more useful, by allowing to edit existing macros.

I have had a look at data stored in its configuration files and tried to interpret them. Here is what I found:

A keyboard macro is a stream of bytes. It always seems to start with the four bytes 54 50 45 52 (ASCII “TPOR”, no idea what it means. TP looks suspiciously like Turbo Pascal). This seems to be a kind of signature. I have not tried to modify it to see what the IDE makes of it, I just want to understand the actual macros. This signature is followed by a single byte that is either 00 or 01. 00 seems to mark the end of the macro. 01 is followed by 4 bytes, which seem to belong together as two separate words, e.g. 61 00 00 00, and describe a key press. Each such 4 byte sequence is again followed by a single byte that is either 00 or 01. 00 again, marks the end of the macro, 01 means that another another key press follows etc.

54 50 54 52
01  61 00  00 00
00

would be a keyboard macro that simply types the lower case letter “a”.

54 50 54 52
01  61 00  00 00
01  62 00  00 00
00

would be “ab”.

Let’s have a closer look at the four bytes describing a key press:

The first word describes the main key, the second word describes modifier keys. The modifier word can have the following values:

  • 00 00 – meaning the main key is a normal character
  • 88 00 – meaning the main key is a special key like the arrow keys or insert / del / page up/down etc.
  • 10 00 added to it means that the Ctrl key is being pressed
  • 20 00 added to it means that the Shift key is being pressed
  • 40 00 added to it means that the Alt key is being pressed

I have not seen any other values.

Please keep in mind that I represent the bytes in the order they show up in the stream. When interpreting two consecutive bytes as words, you have to switch them around according to Intel convention, so the bytes 10 00 actually mean the the number $0010, and 88 00 is the number $0088.

If the modifier word is $0000, the main key is in its simplest form the ASCII code of a character, e.g. like you can see in GExperts ASCII Chart:

I have yet to check whether the value is actually a WideChar.
update: It definitely is a WideChar, so the code below won’t work correctly for input that uses the high byte of the character.

If the modifier value is <> $0000 it describes a special key, e.g.
$0020 is the Space key, $0021 is PgUp, $0022 is PgDn, $002D is Ins key, $002E is Del etc.

update: The above applies to all Delphi versions that I checked. That includes Delphi 6, 7, 2005, 2006, 2007 and 10.1. Note that Delphi 6 and 7 apparently do not support Unicode characters in keyboard macros. I tried to enter some Arabic characters (Unicode $30xx) and they were converted to characters in $00xx range.

Based on my observations I have so far written the following code to interpret a keyboard macro:

type
  TMenuKeyCap = (
    mkcBkSp, mkcTab, mkcEsc, mkcEnter, mkcSpace, mkcPgUp,
    mkcPgDn, mkcEnd, mkcHome, mkcLeft, mkcUp, mkcRight,
    mkcDown, mkcIns, mkcDel, mkcShift, mkcCtrl, mkcAlt);

var // these resource strings are declared in unit Consts
  MenuKeyCaps: array[TMenuKeyCap] of string = (
    SmkcBkSp, SmkcTab, SmkcEsc, SmkcEnter, SmkcSpace, SmkcPgUp,
    SmkcPgDn, SmkcEnd, SmkcHome, SmkcLeft, SmkcUp, SmkcRight,
    SmkcDown, SmkcIns, SmkcDel, SmkcShift, SmkcCtrl, SmkcAlt);

function KeyCodeToText(Code: Word; Modifier: Word): string;
var
  LoByte: Byte;
begin
  Result := Format('unkown (%.4x)', [Code]);
  if (Modifier and $88) <> 0 then begin
    // special keys
    Result := '';
    if (Modifier and $10) <> 0 then
      Result := Result + MenuKeyCaps[mkcCtrl];
    if (Modifier and $40) <> 0 then
      Result := Result + MenuKeyCaps[mkcShift];
    if (Modifier and $20) <> 0 then
      Result := Result + MenuKeyCaps[mkcAlt];
    LoByte := (Code and $FF);
    case LoByte of
    // $00..$07
      $08, $09: // backspace / tab
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := Result + MenuKeyCaps[mkcEnter];
      $1B: Result := Result + MenuKeyCaps[mkcEsc];
    // $1B..$1F ?
      $20..$28: // space and various special characters
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcSpace) + LoByte - $20)];
    // $29..$2C ?
      $2D..$2E: // Ins, Del
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcIns) + LoByte - $2D)];
    // $2F ?
      $30..$39: // 0..9
        Result := Result + Chr(LoByte - $30 + Ord('0'));
      $41..$5A: // A..Z
        Result := Result + Chr(LoByte - $41 + Ord('A'));
      $60..$69: Result := Result + Chr(LoByte - $60 + Ord('0'));
      $70..$87: Result := Result + 'F' + IntToStr(LoByte - $6F);
    end;
  end else begin
    LoByte := (Code and $FF);
    case LoByte of
      // $00..$07
      $08, $09: // backspace / tab
        Result := MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := MenuKeyCaps[mkcEnter];
      $1B: Result := MenuKeyCaps[mkcEsc];
      // $1B..$1F ?
      $20..$7E: Result := Chr(LoByte);
      $7F: Result := MenuKeyCaps[mkcDel];
      $80..$FF: Result := Chr(LoByte);
    end;
  end;
end;

function TMacroInfo.TryDecode(AStrings: TStrings): Boolean;

  function Read(var Buffer; Count: Longint): Boolean;
  begin
    Result := (FStream.Read(Buffer, Count) = Count);
  end;

var
  Magic: Longword;
  Flag: Byte;
  HiWord: Word;
  LoWord: Word;
  s: string;
begin
  Result := False;
  FStream.Position := 0;
  if not Read(Magic, SizeOf(Magic)) or (Magic <> $524F5054) then
    Exit;
  if not Read(Flag, SizeOf(Flag)) then
    Exit;
  while Flag = $01 do begin
    if not Read(LoWord, SizeOf(LoWord)) then
      Exit;
    if not Read(HiWord, SizeOf(HiWord)) then
      Exit;
      s := KeyCodeToText(LoWord, HiWord);
    AStrings.Add(s);
    if not Read(Flag, SizeOf(Flag)) then
      Exit;
  end;
  Result := True;
end;

It’s not finished and might never be. It’s also far from perfect. I am posting this here in the hope that it might be useful to somebody. I might use it to extend the Macro Library Expert in GExperts to allow limited editing of keyboard macros.

Fixed HideNavBar functionality in GExperts

 Delphi, GExperts, Uncategorized  Comments Off on Fixed HideNavBar functionality in GExperts
Feb 262017
 

When Embarcadero added the Navigation Toolbar to the Delphi code editor in Delphi 10 there were a few people who didn’t like it because it took up some more of the vertical screen space. But there was no option to disable it.

Achim Kalwa wrote an expert to hide this toolbar and contributed the code, which I integrated into GExperts. Unfortunately it didn’t work reliably. The Navigation Toolbar came back whenever one opened a new edit window and went away again when switching between tabs, creating an annoying flicker.

Today I investigated the issue and found that apparently the IDE sets the control’s visible property to true whenever it opens a new editor tab. The fix was actually quite simple: Create a new panel, set its Parent to the toolbar’s original parent control and set the toolbar’s parent to this new panel. So it ends up between the toolbar and its parent. Then set this panel to be invisible instead of the toolbar. Since the IDE doesn’t know about this new panel, it does not change its visibility. Voila, problem solved.

Of course it was a bit more involved because I had to size the panel correctly and also make sure that I don’t insert a new panel every time I check for the visibility. But once I had the general principle it was just a matter of fine tuning the solution.

  if TryFindComponentByName(Ctrl, 'TEditorNavigationToolbar', C) then begin
    Ctrl := TWinControl(C);
    ParentCtrl := Ctrl.Parent;
    if Assigned(ParentCtrl) and (ParentCtrl is TPanel) and (ParentCtrl.Name = GX_HideNavbarPanel) then
      pnl := TPanel(ParentCtrl)
    else begin
      pnl := TPanel.Create(ParentCtrl);
      pnl.Parent := ParentCtrl;
      pnl.Name := GX_HideNavbarPanel;
      pnl.Align := alTop;
      pnl.BevelOuter := bvNone;
      pnl.Height := ctrl.Height;
      Ctrl.Parent := pnl;
    end;
    pnl.Visible := FIsNavbarVisible;
    pnl.Enabled := FIsNavbarVisible;
    if FIsNavbarVisible then
      Ctrl.Visible := True;
    Result := True;
  end;

I also found, that in Delphi 10.1 Berlin there is already an option to show or hide the Navigation Toolbar (Tools -> Options -> Editor Options -> Display), so I removed the functionality from GExperts for Delphi 10.1 again.

GExperts formatter branch is dead, long live the trunk

 Delphi, GExperts, Uncategorized  Comments Off on GExperts formatter branch is dead, long live the trunk
Feb 252017
 

As of today, I have stopped developing GExperts in the formatter branch and switched to the trunk of the repository.

If you want to get the current sources, take them from

svn co https://svn.code.sf.net/p/gexperts/code/trunk GExperts

I have updated the Compiling GExperts article accordingly.

Debugging helper for TDataSet

 Delphi, Uncategorized  Comments Off on Debugging helper for TDataSet
Feb 152017
 

I like programming in Delphi, but I don’t particularly like writing applications that work with database backends, but sometimes I just can’t avoid it (have been working on one for several weeks now, and it wasn’t too bad really). But one thing that can drive me nuts is when you have got an error, want to investigate the cause and it’s a bloody nuisance to try and get the value of the current record in a dataset. So, finally I wrote this little helper unit which I want to share with you.

All it does is export one public function TDataset_Dump which is meant to be called from the debugger’s “Evaluate and Modify” window:

TDataset_Dump(SomeDataset)

If called like this, it will magically open a text file in the associated application and display the content of the current record of the dataset you passed to it.

Alternatively you can let it dump the whole dataset like this:

TDataset_Dump(SomeDataset, -1)

Or all remaining records in the dataset (until EOF) like this:

TDataset_Dump(SomeDataset, 0)

Or you can pass it a number of records it should dump:

TDataset_Dump(SomeDataset, 5)

For this to work, you need to add the unit u_dzDatasetDump to the unit with the dataset you want to debug, otherwise the debugger won’t know about the function.

The unit is part of my dzlib library. It needs a few units from it (I’m a lazy bastard ™, so since I use the library in most of my programs I didn’t see the point of trying to avoid dependencies, but it should’t be too difficult to remove them.).

OK, so here goes the unit:

///<summary>
/// Add this unit only for debug purposes.
/// It exports TDataset_Dump for dumping a dataset from the evaluate and modify dialog
/// of the Delphi debugger. </summary>
unit u_dzDatasetDump;

interface

uses
  SysUtils,
  Classes,
  DB;

///<summary>
/// @param Count:  1 -> Dump the current record only (default)
///               -1 -> Dump the whole dataset
///                0 -> Dump from the current record until EOF
///               >1 -> Dump a maximum of n records starting from the current
/// @returns the file name the data was written to </summary>
function TDataset_Dump(_ds: TDataset; _Count: Integer = 1): string;

implementation

uses
  u_dzLineBuilder,
  u_dzVariantUtils,
  u_dzFileUtils,
  u_dzExecutor,
  u_dzOsUtils;

const
  DUMP_ALL = -1;

procedure TDataset_DumpHeaders(_ds: TDataset; _sl: TStrings);
var
  lb: TLineBuilder;
  i: Integer;
  fld: TField;
begin
  lb := TLineBuilder.Create;
  try
    for i := 0 to _ds.FieldCount - 1 do begin
      fld := _ds.Fields[i];
      lb.Add(fld.FieldName);
    end;
    _sl.Add(lb.Content)
  finally
    FreeAndNil(lb);
  end;
end;

procedure TDataset_DumpCurrent(_ds: TDataset; _sl: TStrings);
var
  lb: TLineBuilder;
  i: Integer;
  fld: TField;
begin
  lb := TLineBuilder.Create;
  try
    for i := 0 to _ds.FieldCount - 1 do begin
      fld := _ds.Fields[i];
      lb.Add(Var2Str(fld.Value));
    end;
    _sl.Add(lb.Content)
  finally
    FreeAndNil(lb);
  end;
end;

procedure TDataset_DumpToEof(_ds: TDataset; _sl: TStrings);
begin
  while not _ds.Eof do begin
    TDataset_DumpCurrent(_ds, _sl);
    _ds.Next;
  end;
end;

procedure TDataset_DumpAll(_ds: TDataset; _sl: TStrings);
begin
  _ds.First;
  TDataset_DumpToEof(_ds, _sl);
end;

function TDataset_Dump(_ds: TDataset; _Count: Integer = 1): string;
var
  sl: TStringList;
  bm: Pointer;
begin
  Result := '';
  sl := TStringList.Create;
  try
    if not Assigned(_ds) then begin
      sl.Add('<dataset not assigned>');
      if _Count = -1 then begin
        // do nothing this is a dummy call to fool the linker
        Exit; //==>
      end;
    end else if not _ds.Active then begin
      sl.Add('<dataset not active>');
    end else if _ds.IsEmpty then begin
      sl.Add('<dataset is empty>');
    end else begin
      bm := nil;
      _ds.DisableControls;
      try
        bm := _ds.GetBookmark;
        TDataset_DumpHeaders(_ds, sl);
        if _Count = DUMP_ALL then begin
          TDataset_DumpAll(_ds, sl);
        end else begin
          if _ds.Eof then begin
            sl.Add('<dataset is at eof>');
          end else if _Count = 0 then begin
            TDataset_DumpToEof(_ds, sl);
          end else begin
            while (not _ds.Eof) and (_Count > 0) do begin
              TDataset_DumpCurrent(_ds, sl);
              _ds.Next;
              Dec(_Count);
            end;
          end;
        end;
      finally
        _ds.GotoBookmark(bm);
        _ds.EnableControls;
      end;
    end;
    if Assigned(_ds) then
      Result := _ds.Name;
    if Result = '' then
      Result := 'NONAME';
    Result := itpd(TFileSystem.GetTempPath) + 'dump_of_' + Result + '.txt';
    sl.SaveToFile(Result);
    OpenFileWithAssociatedApp(Result, True);
  finally
    FreeAndNil(sl);
  end;
end;

initialization
  // Make sure the linker doesn't eliminate the function
  TDataset_Dump(nil, -1);
end.

Some more form enhancements in GExperts

 Delphi, GExperts, Uncategorized  Comments Off on Some more form enhancements in GExperts
Feb 122017
 

Prompted by a post from +Attila Kovacs I have added the menu designer form (TMenuBuilder) to the list of forms which GExperts enhances. In this case, it only stores the size and optionally the position of the form.

And since I was at it, I also added several other forms:

  • TActionListDesigner
  • TFieldsEditor – used for TDataset and descendants
  • TDBGridColumnsEditor
  • TConnEditForm – the one for TAdoConnection, required to modify some controls. There is a form with the same name used for TSqlConnection, that is already sizeable.
  • TDriverSettingsForm – I don’t remember where it is used
  • TPakComponentsDlg – this is the one which shows the component list for a package, required to modify some controls

The last one pointed out a problem with the GExperts code: For whatever reason, the Delphi IDE does not destroy the form when it closes, but the form’s window handle changes every time. This messed up the code which was supposed to keep track of forms that were already managed because it compared the form’s ClassName and window handle. Since the handle changed it ended up setting the OnDestroy handler every time, saving the old handler for calling and restoring later. Unfortunately the “old handler” was GExperts’ own replacement handler for the second and all further instances, so it called itself. Boom instant stack overflow.

So I ended up completely rewriting the code. It no longer maintains a list of all managed forms. Instead, the TManagedForm class now descends from TComponent and is added to the form it enhances. This makes the code much cleaner. It also allowed me to have descendants for forms, that require special handling to become sizeable. These now have got a MakeComponentsResizable method that looks up the components on the form and changes their Anchor property so they move and size sensibly when the form gets resized.

Today I finally finished that code and found some more forms that could benefit from being sizeable:

  • TImageListEditor – Delphi 6 only, it is already sizeable in Delphi 7 and later
  • TPictureEditDlg – which is surprisingly not sizeable even in Delphi 10.1 Berlin

And last but not least, I fixed a bug in the Environment Options form of Delphi 6 and 7 where the Environment page was not correctly modified.

Anybody interested in adding stuff from JEDI Experts?

 Delphi, GExperts, Uncategorized  Comments Off on Anybody interested in adding stuff from JEDI Experts?
Feb 052017
 

A feature request for GExperts mentions a tool called JEDI Experts which is a project on SourceForge which has been inactive since Delphi 6 times. The description reads as follows:

JEDI Experts is set of experts/wizards to be used in Delphi IDE. While they can be used directly in Delphi IDE, the main task will be convert them, and merge into GExperts – another Delphi project on SourceForge: http://sourceforge.net/projects/gexperts

It also links to a more descriptive page which unfortunately still doesn’t say much about the actual functionality.

I got curious, downloaded the sources and tried to compile them. It was quite difficult because it requires the JCL/JVCL and many of the components it used are have been deprecated. I ended up converting the dfm files to text (Yes, it is that old.) and replacing these components in a text editor.

JEDI experts consists of a DLL and a package, the latter is apparently supposed to load the DLL into the IDE. I got the DLL to compile but eventually gave up on the package. It references the deprecated ToolsAPI units from the Delphi 5 days and after deleting/replacing lots of ifdefs to get it to even compile in Delphi 2007 I gave up.

But I didn’t want to let those hours of work I already put into it got to waste. So I have put the sources into the GExperts repository, adding a new branch for it:

https://svn.code.sf.net/p/gexperts/code/branches/JExperts/trunk

If you check out this directory, it will automatically also get the parts of the GExperts sources it requires.

The actual JEDI Experts sources are in the subdirectory JExperts. The DLL project is in “Library”, the Package project in “Package”.

There is also the ZIP file containing the original sources as downloaded from the JEDI Experts SourceForge project page.

So, if anybody is interested to pick up where I gave up, you are welcome. There might be some functionality there which could be integrated into GExperts. Much of it is duplicated though.

Enhanced Goto dialog enhancement

 Delphi, GExperts, Uncategorized  Comments Off on Enhanced Goto dialog enhancement
Feb 042017
 

The Goto dialog IDE enhancement of GExperts got itself an enhancement:

It now also displays “Package”, “Requires” and “Contains” in the list.

Also, all IDE dialog enhancement classes now derive from a new TIdeDialogEnhancer class which provides some basic functionality that previously was duplicated.

New IDE enhancement for the Application settings

 Delphi, GExperts, Uncategorized  Comments Off on New IDE enhancement for the Application settings
Feb 042017
 

GExperts now has got a very small enhancement for the Application tab in the project settings dialog of the Delphi IDE:

A new button that sets the lib suffix to the default value corresponding to the IDE version which is showing this dialog (example: Delphi 2007 -> 110, Delphi 10.1 -> 240).

This is only really useful for those who develop packages for multiple Delphi versions and like me always forget, which suffix to use for which Delphi version.

Of course it would be nice to have that set automatically for new package projects. Anybody who wants to contribute code for that?

WordPress update broke ssl

 blog, Uncategorized  Comments Off on WordPress update broke ssl
Feb 032017
 

My hoster has updated my WordPress installation to the latest version (and broke it for several days). What they also did was disable my option to set the site address and wordpress address to https rather than http. So, now even though the site is still available through https://blog.dummzeuch.de it now longer automatically forces https connections. Thanks a lot. πŸ™

What’s even worse: It reverses to plain http sometimes for no reason I can determine. e.g. I have been writing this post through http because I didn’t notice that until now. Thanks even more. :-((

GExperts Sort Selected Lines Expert now uses “natural” sort order

 Delphi, GExperts, Uncategorized  Comments Off on GExperts Sort Selected Lines Expert now uses “natural” sort order
Feb 032017
 

Achim Kalwa, who has been steadily contributing to GExperts, submitted a patch that changes the sort order used by the “Sort Selected Lines” Expert in GExperts to use the natural sort order, similar to the way Windows Explorer sorts files. E.g. Assume you want to sort the following:

  Label20
  Label1
  Label10
  Label2
  Label100
  Label3
  Label11

The old code sorted it like this:

  Label1
  Label10
  Label100
  Label11
  Label2
  Label20
  Label3

The new code does this:

  Label1
  Label2
  Label3
  Label10
  Label11
  Label20
  Label100

I have accepted this patch as is. Not sure whether there should be an option to switch between the new and the old sort order.

%d bloggers like this: