Search code examples
delphidelphi-5

Why does the DOF remove lines?


I'm trying to write a plugin for Delphi 5 that will store the paths to our testing exes in the DOF so that there is a direct association between the project and the tests for that project. When I go to add my own module to the DOF file, something like

[DUint Plugin]
IntegrationTestExe=Somepath
UnitTestExeList=Some;Path;

Anytime I go to add this either manually or through code, when I save the project, the lines I add are removed. I chalked this up to maybe the IDE just doesn't allow custom modules in the DOF.

However, we use a third party plugin called EurekaLog. EurekaLog injects its own vars into the DOF and when you save, those vars are not removed. I copied much of the code over so I could test if the EurekaLog code would work properly (through some magic) but their code just wrote their module to the DOF and did nothing else special.

Does anyone know how this is accomplished in EurekaLog? Do I need to register my module somewhere so that the IDE knows not to remove it?


Solution

  • Update After a bit of experimenting, it seems that saving settings to the DOF is actually noticeably more reliable than saving them to the DSK file.

    Add another TEdit to the form and create LoadDOFSettings and SaveDOFSettings analogous to the existing LoadSettings and SaveSettings and call them on receipt of the DesktopLoad and DesktoSave notifications. The SaveDOFSettings doesn't need to be called via the Timer1 event because the renaming doesn't seem to happen to the DOF.

    Original answer I suggest that before reading this answer, you do a File | Close All in the IDE, create a new package, add the unit below into it and install it in the IDE.

    The purpose of the package is two-fold, firstly to show how to save custom settings in the DSK file and secondly to give you an idea of what event information about project files you can get from the IDE via the services in the ToolsAPI unit..

    Once you've installed the package, keep an eye on its form, which shows you file notifications in the upper memo as you open, work on and close a project. There are several things to notice:

    • When you open a project, the last notification you receive is about its DSK file having been opened.

    • Not every file type is the subject of a notification. In particular, you don't receive any notifications specifically about the DOF file, so if you want to write to it and later read from it, you have to make assumptions about when it's safe (or not) to do so, and this is possibly why you have run into the problem you are asking about.

    • When you do a Close All on the project, the last file change you get notified about is the DSK being written. The catch is that it's initially written to a file of the same name but with the extension .$$$. Very soon afterwards, but asaics you can't tell exactly when, this .$$$ file is renamed to .DSK.

    The form created by the code below has an edit box, edMyValue' which can be used to set a value in a section of the DSK file calledMySettingsand which is reloaded the next time the project is opened. The writing of theMySettings` section of the DSK file is triggered aby a TTimer with a 2-second delay to give the IDE time to write and rename the DSK file as I've described. This obviously provides the opportunity for a race condition.

    You might like to refer to

    http://www.gexperts.org/open-tools-api-faq/#dsk

    (GExperts is the IDE add-in tool that's been around since very early days of Delphi)

    The section of the article is talking about the current project's .DSK file. Like the DOF, this is in INI file format, with sections like

    [Closed Files]
    [Modules]
    [EditWindow0]
    [View0]
    

    As you'll see it says

    check for the ofnProjectDesktopLoad and ofnProjectDesktopSave NotifyCode values. When you see one of those, you can save/load values from the file indicated by the FileName parameter using a class such as TIniFile.

    Perhaps it's a bit trickier than the article suggests, because of the renaming business.

    Have fun!

    unit IDEEventsu;
    interface
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, StdCtrls, ExtCtrls, ToolsAPI, Grids, IniFiles;
    
    type
      TFileEventsForm = class(TForm)
        Panel1: TPanel;
        Memo1: TMemo;
        edMyValue: TEdit;
        btnClear: TButton;
        Timer1: TTimer;
        Memo2: TMemo;
        procedure btnClearClick(Sender: TObject);
        procedure Timer1Timer(Sender: TObject);
      private
        function GetCurrentProject: IOTAProject;
      public
        //  The following are using interfaces accessible via the ToolsAPI
        Services: IOTAServices;
        ProjectGroup : IOTAProjectGroup;
        Project: IOTAProject;
        Options : IOTAProjectOptions;
        ModServices: IOTAModuleServices;
        Module: IOTAModule;
    
        NotifierIndex: Integer;  // This is used to disconnect our notifier from the IDE
        IsSetUp : Boolean;
        SetUpCount : Integer;
        DskFileName : String;
        procedure SetUp;
        procedure SaveSettings;
        procedure LoadSettings;
      end;
    
    var
      FileEventsForm: TFileEventsForm;
    
    procedure Register;
    
    [...]
    uses
     typinfo;
    
    type
      TIdeNotifier = class(TNotifierObject, IOTANotifier, IOTAIDENotifier)
      //  This is the class we use to receive file notication events from the IDE via the
      //  interfaces in ToolsAPI.Pas
      //
      //  It needs to implement the IOTANotifier and IOTAIDENotifier interfaces and,
      //  once registered with the IDE, the IDE calls its methods as a kind of call-back
      //  mechanism so that it gets notified of file events
      //
      //  Note that this file also provides a form for displaying the received event
      //  notifications and that the IOTANotifier and IOTAIDENotifier interfaces could
      //  just as easily be implemented by the form itself
      protected
        procedure AfterCompile(Succeeded: Boolean);
        procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean);
        procedure FileNotification(NotifyCode: TOTAFileNotification;
          const FileName: string; var Cancel: Boolean);
      end;
    
    procedure Register;
    //  This is necessary to register the package in the IDE
    var
      Notifier : TIdeNotifier;
    begin
      FileEventsForm:= TFileEventsForm.Create(Nil);
      FileEventsForm.Services := BorlandIDEServices as IOTAServices;
      Notifier := TIdeNotifier.Create;
      Notifier.Form := FileEventsForm;
      FileEventsForm.NotifierIndex := FileEventsForm.Services.AddNotifier(TIdeNotifier.Create);
    end;
    
    procedure CloseDown;
    begin
      FileEventsForm.Services.RemoveNotifier(FileEventsForm.NotifierIndex);
      FileEventsForm.Close;
      FileEventsForm.Free;
    end;
    
    function NotifyCodeString(NotifyCode : TOTAFileNotification) : String;
    begin
      Result := Copy(GetEnumName(TypeInfo(TOTAFileNotification), Ord(NotifyCode)), 4, MaxInt);
    end;
    
    procedure TIdeNotifier.AfterCompile(Succeeded: Boolean);
    begin
    end;
    
    procedure TIdeNotifier.BeforeCompile(const Project: IOTAProject; var Cancel: Boolean);
    begin
    end;
    
    procedure TIdeNotifier.FileNotification(NotifyCode: TOTAFileNotification;
      const FileName: string; var Cancel: Boolean);
    begin
      if True {NotifyCode in [ofnProjectDesktopLoad, ofnActiveProjectChanged]}  then begin
        FileEventsForm.Show;
        FileEventsForm.Memo1.Lines.Add(Format('%s file: %s', [NotifyCodeString(NotifyCode), FileName]));
        case NotifyCode of
          ofnProjectDesktopLoad,
          ofnDefaultDesktopLoad : begin
            FileEventsForm.DskFileName := FileName;
            FileEventsForm.LoadSettings;
          end;
          ofnProjectDesktopSave,
          ofnDefaultDesktopSave : begin
            if True{CompareText(ExtractFileExt(FileName), '.DSK') = 0} then begin
              FileEventsForm.Caption := FileName;
              FileEventsForm.Timer1.Enabled := True;  //  causes DSK file to be updated after Timer1.Interval (=2000ms)
            end;
          end;
        end; { case }
      end;
    end;
    
    procedure TFileEventsForm.btnClearClick(Sender: TObject);
    begin
      Memo1.Lines.Clear;
    end;
    
    function TFileEventsForm.GetCurrentProject: IOTAProject;
    var
      i: Integer;
    begin
      Result := nil;
      ModServices := BorlandIDEServices as IOTAModuleServices;
      for i := 0 to ModServices.ModuleCount - 1 do
      begin
        Module := ModServices.Modules[i];
        if Supports(Module, IOTAProjectGroup, ProjectGroup) then begin
          Result := ProjectGroup.ActiveProject;
          Options := Result.ProjectOptions;
          Exit;
        end
        else if Supports(Module, IOTAProject, Project) then
        begin // In the case of unbound packages, return the 1st
          if Result = nil then begin
            Result := Project;
            Options := Result.ProjectOptions;
          end;
        end;
      end;
    end;
    
    procedure TFileEventsForm.SetUp;
    begin
      Project := GetCurrentProject;
      Inc(SetUpCount);
      Caption := 'Setup done ' + IntToStr(SetUpCount);
      IsSetUp := True;
    end;
    
    procedure TFileEventsForm.LoadSettings;
    var
      Ini : TMemIniFile;
      S : String;
    begin
      Ini := TMemIniFile.Create(DSKFileName);
      try
        S := Ini.ReadString('MySettings', 'Name', 'no value');
        edMyValue.Text := S;
      finally
        Ini.Free;
      end;
    end;
    
    procedure TFileEventsForm.SaveSettings;
    var
      Ini : TMemIniFile;
      S : String;
    begin
      S := DSKFileName;
      Caption := 'Saving: ' + S;
      Ini := TMemIniFile.Create(S);
      try
        Ini.WriteString('MySettings', 'Name', edMyValue.Text);
        Ini.UpdateFile;
        Ini.ReadSections(Memo2.Lines);
        Memo2.Lines.Add('This file : ' + DSKFileName);
        edMyValue.Text := '?';
      finally
        Ini.Free;
      end;
    end;
    
    procedure TFileEventsForm.Timer1Timer(Sender: TObject);
    begin
      Timer1.Enabled := False;
      SaveSettings;
    end;
    
    initialization
    
    finalization
      CloseDown;
    end.