Search code examples
delphicustom-componentdesign-timepropertyeditor

How to invoke property editor upon double-clicking a TCollectionItem?


I have a custom control. It has a published property of type TOwnedCollection, which of course contains several instances of TCollectionItem (inherited to my own implementation). Let's call them TMyComponent, TMyCollection, and TMyCollectionItem.

Within TMyCollectionItem, I have a TPersistent property (let's call it MyProp: TMyProp), which also has a corresponding design-time editor installed into the IDE. All this works just fine, so when you're editing the collection, select a collection item, you can click the little button in the object inspector to invoke the property editor. All of this works fine.

What I would like to add is the ability to double-click the item within the collection editor to also invoke that very same property editor. However, I cannot find decent information how to do this. ChatGPT gave me code that simply didn't compile, probably because it was for much older versions of Delphi.

How do I implement the ability to double-click a TMyCollectionItem to invoke the property editor for TMyCollectionItem.MyProp?

NOTE: Looking into the source of ColnEdit.pas (for this collection editor), I see the TListView implements OnClick, but not OnDblClick, and so I'm questioning whether it's even possible to do what I want without rolling out my own collection editor.


Solution

  • The IDE's default Collection Editor does not natively support the feature you are looking for. As you noted:

    NOTE: Looking into the source of ColnEdit.pas (for this collection editor), I see the TListView implements OnClick, but not OnDblClick, and so I'm questioning whether it's even possible to do what I want without rolling out my own collection editor.

    The default Collection Editor simply does not implement any logic when a TListView item is double-clicked on.

    (BTW, it is not the TListVew.OnClick event that updates the Object Inspector, it is the TListVew.OnChange event that does. That is the event that fires when ListView items are selected.)

    However, all is not lost! There IS a way you can access the TListView of the default Collection Editor and assign your own OnDblClick event handler to it:

    • Derive a new class from ColnEdit.TCollectionEditor, and have it assign an OnDblClick handler to the inherited TListView.

    • Derive a new class from ColnEdit.TCollectionProperty and override its virtual GetEditorClass() method to return your TCollectionEditor-derived class type.

    • Register your TCollectionProperty-derived class with DesignIntf.RegisterPropertyEditor() to override the default collection editor for your TMyCollection class.

    Your derived Collection Editor can then invoke your TMyProp property editor when needed by using the DesignEditors.GetComponentProperties() function:

    • Use DesignIntf.CreateSelectionList() to create a list and Add() the appropriate TMyCollectionItem object to it.

    • Pass that list to GetComponentProperties(), along with a callback that will receive an IPropertyEditor for each property of the TMyCollectionItem object.

    • When the callback sees your TMyProp property editor, it can call the editor's Edit() method.

    For example:

    uses
      ..., Vcl.ComCtrls, DesignIntf, DesignEditors, System.TypInfo, ColnEdit;
    
    type
      ...
    
      TMyCollectionEditor = class(TCollectionEditor)
      private
        procedure ListViewDblClick(Sender: TObject);
        procedure InvokeMyPropEditor(const Prop: IProperty);
      public
        constructor Create(AOwner: TComponent); override;
      end;
    
      TMyCollectionProperty = class(TCollectionProperty)
      public
        function GetEditorClass: TCollectionEditorClass; override;
      end;
    
    procedure Register;
    
    ...
    
    constructor TMyCollectionEditor.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      ListView1.OnDblClick := ListViewDblClick;
    end;
    
    procedure TMyCollectionEditor.ListViewDblClick(Sender: TObject);
    var
      Item: TListItem;
      Components: IDesignerSelections;
    begin
      Item := ListView1.Selected;
      if Item = nil then Exit;
      Components := CreateSelectionList();
      Components.Add(Collection.Items[Item.Index]);
      GetComponentProperties(Components, [tkClass], Designer, InvokeMyPropEditor);
    end;
    
    procedure TMyCollectionEditor.InvokeMyPropEditor(const Prop: IProperty);
    begin
      if Prop.GetPropType = typeinfo(TMyProp) then
        Prop.Edit;
    end;
    
    function TMyCollectionProperty.GetEditorClass: TCollectionEditorClass;
    begin
      Result := TMyCollectionEditor;
    end;
    
    procedure Register;
    begin
      RegisterComponents('...', [TMyComponent]);
      RegisterPropertyEditor(typeinfo(TMyCollection), TMyComponent, '', TMyCollectionProperty);
      ...
    end;
    

    That being said, I did test this approach and while it DOES work functionally, there is a slight visual side effect - in 10.2.1 and later, the IDE's theming feature will no longer apply to your Collection Editor window when it is shown. So, for instance, if you use a Dark theme in the IDE, the Collection Editor will not appear Dark anymore.

    To fix this, you need to use the IOTAIDEThemingServices interface in the OpenTools API to register your TCollectionEditor-derived class so the IDE will theme it:

    Using IDE Styles in Third-Party Plugins

    David Hoyle has also written an article on this topic:

    Theming OTA Forms

    For example:

    uses
      ..., ToolsAPI;
    
    type
      ...
    
      TMyCollectionEditor = class(TCollectionEditor)
      private
        FThemingService: IOTAIDEThemingServices;
        FThemingNotifier: Integer;
        ...
        procedure ThemeChanged;
      public
        constructor Create(AOwner: TComponent); override;
        destructor Destroy; override;
      end;
    
      TThemeChangedProc = procedure of object;
    
      TMyThemingNotifier = class(TInterfacedObject, IOTANotifier, INTAIDEThemingServicesNotifier)
      public
        OnThemeChanged: TThemeChangedProc;
        constructor Create(AProc: TThemeChangedProc);
        procedure AfterSave;
        procedure BeforeSave;
        procedure Destroyed;
        procedure Modified;
        procedure ChangingTheme;
        procedure ChangedTheme;
      end;
    
    ...
    
    constructor TMyCollectionEditor.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      ...
    
      if Supports(BorlandIDEServices, IOTAIDEThemingServices, FThemingService) then
      begin
        ThemeChanged;
        FThemingNotifier := FThemingService.AddNotifier(TMyThemingNotifier.Create(ThemeChanged) as INTAIDEThemingServicesNotifier);
      end;
    end;
    
    destructor TMyCollectionEditor.Destroy;
    begin
      if Assigned(FThemingService) then
        FThemingService.RemoveNotifier(FThemingNotifier);
      inherited Destroy;
    end;
    
    procedure TMyCollectionEditor.ThemeChanged;
    begin
      if FThemingService.IDEThemingEnabled then
      begin
        FThemingService.RegisterFormClass(TMyCollectionEditor);
        FThemingService.ApplyTheme(Self);
      end;
    end;
    
    constructor TMyThemingNotifier.Create(AProc: TThemeChangedProc);
    begin
      inherited Create;
      OnThemeChanged := AProc;
    end;
    
    procedure TMyThemingNotifier.AfterSave;
    begin
    end;
    
    procedure TMyThemingNotifier.BeforeSave;
    begin
    end;
    
    procedure TMyThemingNotifier.Destroyed;
    begin
    end;
    
    procedure TMyThemingNotifier.Modified;
    begin
    end;
    
    procedure TMyThemingNotifier.ChangingTheme;
    begin
    end;
    
    procedure TMyThemingNotifier.ChangedTheme;
    begin
      if Assigned(OnThemeChanged) then
        OnThemeChanged();
    end;
    

    Despite Embarcadero's documentation saying the following (emphasis is mine):

    In your plugin initialization or your form’s constructor, call IOTAIDEThemingServices.RegisterFormClass with the class of your form, that is, the form type. You only need to do this once to apply the style hooks to your form. You do not need to unregister it.

    I find that if RegisterFormClass() is not called again before ApplyStyle() after a Theme change, then ApplyStyle() DOES NOT apply the new theme correctly! As-if the IDE lost the previous Form class registration during the Theme change (IDE bug? I have reported it: RSP-43972). Calling RegisterFormClass() before each call to ApplyStyle() works.

    I also noticed that the default TCollectionEditor does not react to IDE Theme changes at all (definitely an IDE bug, which I have reported: RSP-43971).