Documents

Archives

Search

Blogical Thoughts

RAD Lazarus Part 3 - Lots of Code

by Michael Burton
http://blogicalthoughs.com/
19 April, 2015

Adding More Editor Features

I can't seem to stop with adding new features to the example text editor written using Lazarus. The following additions are all code-intensive, so keep that in mind.

This is what we will be doing this time:

Adding Highlighters

At the start of this series, we added and hooked up a single text highlighting component that will only work correctly if the editor is editing Pascal source code. Now we will add other highlighters to cover other kinds of text files. Specifically, we will add the following:

To add these highlighters to the project, click on the SynEdit tab of the component palette. Hover over each icon, so the popup tip shows what the component is. When you find one from the above list, click on it, then click on the editor form. It will be dropped into the project. Keep doing this until you have added all the components in the list.

We now have to hook up the highlighters. We will create a new procedure called SelectHighlighter that will figure out which one to use and connect it properly.

IMPORTANT NOTE: The code shown in bold type are the lines to be added. All the other lines are shown to provide context for the added lines.

Go to the top of the source code file, then scroll down until you find the private declarations section. Add the line shown below.

      private
        { private declarations }
        procedure SelectHighlighter(sExt: string);

While your text cursor is still on the line you just entered, hit Shift+Ctl+C on your keyboard. This will create an empty SelectHighlighter procedure. We will come back to it later.

There are two places in the code where we will use this new procedure - when you Open a file or do a file Save As. You will also have to modify the create a New file procedure. The procedures that will be modified are FileOpen1Accept, FileSaveAs1Accept and FileNew1Execute. Add the new lines shown below.

    procedure TfrmMain.FileOpen1Accept(Sender: TObject);
    begin
        sFileName := FileOpen1.Dialog.FileName;
        sedtText.Lines.LoadFromFile(sFileName);
        SelectHighlighter(LowerCase(ExtractFileExt(sFileName)));
        StatusBar1.Panels[0].Text := ExtractFileName(sFileName);
    end; 
    procedure TfrmMain.FileSaveAs1Accept(Sender: TObject);
    begin
        sFileName := FileSaveAs1.Dialog.FileName;
        SelectHighlighter(LowerCase(ExtractFileExt(sFileName)));
        sedtText.Lines.SaveToFile(sFileName);
        StatusBar1.Panels[0].Text := ExtractFileName(SFileName);
    end;
    procedure TfrmMain.FileNew1Execute(Sender: TObject);
    begin
        sedtText.Lines.Clear;
        sFileName := '';
        sedtText.Modified := False;
        sedtText.Highlighter := SynAnySyn1;
        StatusBar1.Panels[0].Text := 'unnamed';
    end;

We will now fill in the empty SelectHighlighter procedure. IMPORTANT NOTE: The case...of statement we are using in the procedure will not work in Delphi. case...of statements in Delphi only work on ordinals, like 1, 2, w, n, etc. They do not work on strings. You would have to use a series of iff...then statements in Delphi, e.g., if (Pos('.tex', sedtText) > 0) then

    procedure TfrmMain.SelectHighlighter(sExt: string);
    begin
        case sExt of
          '.pas','.dpr','.dpk','.dfm':  // Delphi highlighter
              sedtText.Highlighter := SynPasSyn1;
          '.lpr','.lpk','.lps','.lpi': // Free Pascal highlighter
              sedtText.Highlighter := SynFreePascalSyn1;
          '.js':  // JavaScript highlighter
              sedttext.Highlighter := SynjScriptSyn1;
          '.java': // java highlighter
              sedtText.Highlighter := SynJavaSyn1;
          '.pl','pm','cgi': // perl highlighter
              sedtText.Highlighter := SynPerlSyn1;
          '.lfm': // Lazarus forms highlighter
              sedtText.Highlighter := SynLFMSyn1;
          '.sh': // UNIX shell script highlighter
              sedtText.Highlighter := SynUNIXShellScriptSyn1;
          '.css': // Cascading Stylesheets highlighter
              sedtText.Highlighter := SynCssSyn1;
          '.php','.php3','.phtml': // PHP highlighter
              sedtText.Highlighter := SynPHPSyn1;
          '.html','.htm': // HMTL highlighter
              sedtText.Highlighter := SynHTMLSyn1;
          '.tex': // TEX highlighter
              sedtText.Highlighter := SynTexSyn1;
          '.sql': // SQL highlighter
              sedttext.Highlighter := SynSQLSyn1;
          '.py': // Python highlighter
              sedtText.Highlighter := SynPythonSyn1;
          '.bas': // Visual Basic highlighter
              sedtText.Highlighter := SynVBSyn1;
          '.cpp','.hpp','.hh','.c','.h': // C++ highlighter
              sedtText.Highlighter := SynCppSyn1;
        else
            sedtText.Highlighter := SynAnySyn1; // no highlighter
        end;
    end;

You can now save your work and compile the program. It will be especially evident that the highlighters are working if you open an HTML web page in the text editor.

Checking Editor Controls

After using the editor a bit, I came to the conclusion that you shouldn't be able to use a particular function if it is not useable at the time you click on it. A case in point is you can't Save a file if it hasn't got a filename yet.

To prevent this kind of problem, we will add some lines to our idle handler. The lines are designed to enable/disable some functions based on whether you can actually use them. The code for this is shown below.

    procedure TfrmMain.ApplicationProperties1Idle(Sender: TObject; var Done: Boolean);
    begin
        if Pos(ExtractFilename(sFileName), sFileName) <> 0 then
            StatusBar1.Panels[0].Text := ExtractFilename(sFileName); // set filename
        if (sedtText.Modified) and (Pos('*', StatusBar1.Panels[0].Text) = 0) then
            StatusBar1.Panels[0].Text := '*' + StatusBar1.Panels[0].Text; // set changed
        if sedtText.InsertMode then
            StatusBar1.Panels[1].Text := 'INS' // in insert mode
        else
            StatusBar1.Panels[1].Text := 'OVR'; // in overwrite mode
        StatusBar1.Panels[3].Text := IntToStr(sedtText.CaretY);
        StatusBar1.Panels[5].Text := IntToStr(sedtText.CaretX);

        // Edit controls
        tbtnCut.Enabled := Length(sedtText.SelText) > 0;
        mnuCut.Enabled := Length(sedtText.SelText) > 0;
        tbtnCopy.Enabled := Length(sedtText.SelText) > 0;
        mnuCopy.Enabled := Length(sedtText.SelText) > 0;
        tbtnPaste.Enabled := sedttext.CanPaste;
        mnuPaste.Enabled := sedtText.CanPaste;

        // File controls
        tbtnSave.Enabled := Length(sFileName) > 0;
        mnuSave.Enabled := Length(sFileName) > 0;
    end;    

Basically, each added line will test something and the test will return a True or a False, which will enable or disable the item.

Adding Text Search

In order to implement a search capability in the editor, we will need some new menu items. Right click on the mnuMain component and click on Menu Editor. Right click on the Edit word in the menu, then click on Insert new item (after). A new item will be added to the menu line. Change the new item's caption to &Search. Right click again on the new item, then click on Create Submenu. This will create a submenu with one item. Right click on that item and add another item.

Highlight the first new item and change its properties to this:

Find Menu Item
Property Value
Caption &Find...
Name mnuFind
ShortCut Ctrl+F

Now highlight the second new item and change its properties to this:

Find Next Menu Item
Property Value
Caption Find Next
Name mnuFindNext
ShortCut F3

Close the Menu Editor and save your work. The menu should now look like this.


Search Menu

In order to do a find text search, we need a Find Dialog component. Click on the Dialogs tab of the component palette, then click on the component in the middle with the binoculars on it (TFindDialog). Click on the form to drop the dialog into the program. Rename the component to 'dlgFind'.


Find Dialog Component

The Find Dialog has a lot of options in its Options property. We should set the following options to True:

In the private declarations section of the code, add the following lines:

    private
        { private declarations }
        iSelPos: integer;
        sFindString: string;
        procedure SelectHighlighter(sExt: string);

In the form, click on the Search menu item, then click on Find. This will create an OnClick event for that item. Add the following lines to it:

    procedure TfrmMain.mnuFindClick(Sender: TObject);
    begin
        iSelPos := 0;
        sFindString := '';
        dlgFind.Execute;
    end;

This causes the Find dialog box to be displayed when you click on the 'Find...' menu item, or press Ctrl+F.

Now we need to write the code that does the actual searching through the currently displayed text file. This code is activated when you click on the Find button in the Find dialog, or when you click on the Find Next menu item. Clicking the Find button generates an OnFind event, and that is what does all the work.

Click on the dlgFind component in the form. Go to the Object Inspector and click on the Events tab. Double-click on the empty space next to the OnFind line. Add the following code to that event:

    procedure TfrmMain.dlgFindFind(Sender: TObject);
    var
        str : string;
        iStartPos : integer;
    begin
        if iSelPos = 0 then // where do we start the search?
        begin // Find first. Start at the start.
            sFindString := dlgFind.FindText;
            dlgFind.CloseDialog; // Remove the Find dialog.
            str := LowerCase(sedtText.Lines.Text);
            iStartPos := 1;
        end else
        begin // Find Next. Start after last found
            iStartPos := iSelPos + Length(sFindString);
            str := Copy(LowerCase(sedtText.Lines.Text), iStartPos, MaxInt);
        end;
        iSelPos := Pos(LowerCase(sFindString), str); // search, case-insensitive
        if iSelPos > 0 then
        begin // found the string, highlight it.
            iSelPos := iSelPos + iStartPos;
            sedtText.SelStart := iSelPos -1;
            sedtText.SelEnd := iSelPos + Length(sFindString) - 1;
            sedtText.SetFocus;
        end else
        begin // could not find the string
            str := Concat('Could not find "', sFindString, '".');
            MessageDlg(str, mtError, [mbOK], 0);
        end;
    end;

Finally, we must hook up the Find Next menu item. This is a very easy thing to do. Click on the Find Next menu item in the form and you will get an empty OnClick event for it. Add one line to the event.

    procedure TfrmMain.mnuFindNextClick(Sender: TObject);
    begin
        dlgFindFind(Sender);
    end;

Save your work and compile it. You now have a working search capability in the editor.

We have uploaded the updated source for the editor. Click on MyEditor_3.tar.gz to download it.

end;