Archive

Archive for October 30, 2017

Custom dialog and loading data from JSON in Smart Pascal

October 30, 2017 1 comment

Right now we are putting the finishing touches on our next update, which contains our new theme engine. As mentioned earlier (especially if you follow us on Facebook) the new system builds on the older – but we have separated border and background from the basic element styling.

When working with the new theme system, I needed an application that could demonstrate and show all the different border and background types, most of our visual controls – but also information about what Smart Mobile Studio is, what it’s features are and where you can buy it.

smarties_01

So it started as a personal application just to get a good overview of the CSS themes I was working on; but it has become an example in it’s own right.

Dont hardcode, just dont

If you look at the picture above, there is a MenuList with the options: “Introduction”, “Features” and “Where to buy”. When you click these I naturally want to inform the user about these things by displaying information.

I could have hardcoded the information text into the application; in many ways that would have been simpler (considering the data requirements here is practically insignificant). but all that text within the source? I hate mess like that.

Secondly, how exactly was I going to show this information? Would I use the modal framework already in place, or code something more lightweight?

As always I ended up making a new and more lightweight system. A reader style dialog appears and allows you to scroll vertically. The header contains the title of the information and a close button.

smarties_02

Typical “reader” style dialog with scrolling

I also used a block-box to prevent the user from reaching the UI until they click the close-button. You notice that the form, toolbar and header in the back is darkened. This is actually a control that is semi-transparent that does one thing: prevent anyone from clicking or interacting with the UI while the dialog is active.

The JSON file structure

json_structureThe structure I needed was very simple: our records would have a unique ID that we use to fetch and recognize content; It would also have a Title and Text property. It really doesnt have to be more difficult than that.

To work with the JSON i used the online JSON editor JSonEditorOnline, which is actually really good! It allows you to write your JSON and then format it so that special characters (like CR+LF) is properly encoded.

Putting it all together

Having coded the dialog thing first, I now sat down and finished a sort of “Turbo Pascal” record database system for this particular file format. It’s not very big nor extremely advanced – but that’s the entire point! Throwing in SQLite or MongoDB for something as simple as a few records of data – especially when the data is so simple, is just a complete waste of time and effort.

Right, let’s have a peek at the code shall we!

unit infodialog;

interface

uses
  System.Types,
  System.Types.Convert,
  System.Types.Graphics,
  System.Colors,
  System.JSON,

  SmartCL.Theme,
  SmartCL.FileUtils,
  SmartCL.System,
  SmartCL.Effects,
  SmartCL.Components,
  SmartCL.Scroll,
  SmartCL.Controls.Panel,
  SmartCL.Controls.Scrollbox,
  SmartCL.Controls.Header;

type

  TInfoDialog = class(TW3Panel)
  private
    FHead:    TW3HeaderControl;
    FBox:     TW3Scrollbox;
  protected
    procedure InitializeObject; override;
    procedure FinalizeObject; override;
    procedure Resize; override;
  public
    property  Header: TW3HeaderControl read FHead;
    property  Content: TW3Scrollbox read FBox;

    class function ShowDialog(Title, Content: string): TInfoDialog;
  end;

  TAppInfoRecord = record
    iiId:     string;
    iiTitle:  string;
    iiText:   string;
    procedure Clear;
    class function Create(const Id, Title, Text: string): TAppInfoRecord;
  end;

  TAppInfoDB = class(TObject)
  private
    FStack:     array of TStdCallback;
    FItems:     array of TAppInfoRecord;

    procedure   Parse(DBText: string);

    procedure   HandleDataLoaded(const FromUrl: string;
                const TextData: string; const Success: boolean);
  public
    property    Empty: boolean read ( (FItems.Count < 1) );
    property    Count: integer read (FItems.Count);
    property    Items[index: integer]: TAppInfoRecord
                read  (FItems[index])
                write (FItems[index] := Value);

    function    GetRecById(Id: string; var Info: TAppInfoRecord): boolean;

    procedure   LoadFrom(Url: string; const CB: TStdCallback);
    procedure   Clear;

    destructor  Destroy; override;
  end;


implementation

uses SmartCL.Application;

//#############################################################################
// TAppInfoRecord
//#############################################################################

class function TAppInfoRecord.Create(const Id, Title, Text: string): TAppInfoRecord;
begin
  result.iiId := id.trim();
  result.iiTitle := Title.trim();
  result.iiText := Text;
end;

procedure TAppInfoRecord.Clear;
begin
  iiId := '';
  iiTitle := '';
  iiText := '';
end;

//#############################################################################
// TAppInfoDB
//#############################################################################

destructor TAppInfoDB.Destroy;
begin
  if FItems.Count > 0 then
    Clear();
  inherited;
end;

procedure TAppInfoDB.Clear;
begin
  FItems.Clear();
end;

function TAppInfoDB.GetRecById(Id: string; var Info: TAppInfoRecord): boolean;
begin
  Info.Clear();
  if not Empty then
  begin
    Id := Id.trim().ToLower();
    if id.length > 0 then
    begin
      for var x := 0 to Count-1 do
      begin
        result := Items[x].iiId.ToLower() = Id;
        if result then
        begin
          Info := Items[x];
          break;
        end;
      end;
    end;
  end;
end;

procedure TAppInfoDB.Parse(DBText: string);
var
  vId:    variant;
  vTitle: variant;
  vText:  variant;
begin
  Clear();

  DbText := DbText.trim();
  if DbText.length > 0 then
  begin
    var FDb := TJSONObject.Create;
    FDb.FromJSON(DbText);

    if FDb.Exists('infotext') then
    begin
      // get the infotext-> [] array of JS objects
      var Root: TJSInstanceArray := TJSInstanceArray( FDb.Values['infotext'] );

      for var x := 0 to Root.Count-1 do
      begin
        var node := TJSONObject.Create(Root[x]);
        if node <> nil then
        begin
          Node
            .Read('id', vid)
            .Read('title', vtitle)
            .Read('text', vtext);

          FItems.add( TAppInfoRecord.Create(vId, vTitle, vText) );
        end;
      end;
    end;

  end;
end;

procedure TAppInfoDB.LoadFrom(Url: string; const CB: TStdCallback);
begin
  if assigned(CB) then
    FStack.push(CB);
  TW3Storage.LoadFile(Url, @HandleDataLoaded);
end;

procedure TAppInfoDB.HandleDataLoaded(const FromUrl: string;
          const TextData: string; const Success: boolean);
begin
  try
    // Parse if data ready
    if Success then
      Parse(TextData);
  finally
    // Perform callbacks
    while FStack.Count>0 do
    begin
      var CB := FStack.pop();
      if assigned(CB) then
        CB(Success);
    end;
  end;
end;

//#############################################################################
// TInfoDialog
//#############################################################################

procedure TInfoDialog.InitializeObject;
begin
  inherited;
  FHead := TW3HeaderControl.Create(self);
  FHead.BackButton.Visible := false;
  FHead.NextButton.Caption := 'Close';

  // By default the header text is centered within the space allocated for it,
  // which by default is 2/4. This can look a bit off when we never show
  // the left-button. So we force text-align to the left [normal].
  FHead.Title.Handle.style['text-align'] := 'left';

  FBox := TW3Scrollbox.Create(self);
  FBox.Background.FromColor(clWhite);
  FBox.ScrollBars := sbIndicator;
end;

procedure TInfoDialog.FinalizeObject;
begin
  FHead.free;
  FBox.free;
  inherited;
end;

procedure TInfoDialog.Resize;
begin
  inherited;
  var LBounds := ClientRect;
  var dy := LBounds.top;

  if FHead <> nil then
  begin
    FHead.SetBounds(LBounds.left, LBounds.top, LBounds.width, 32);
    inc(dy, FHead.Height +1);
  end;

  if FBox <> nil then
    FBox.SetBounds(LBounds.left, dy, LBounds.width, LBounds.height - dy);
end;

class function TInfoDialog.ShowDialog(Title, Content: string): TInfoDialog;
begin
  var Host := Application.Display;
  var Shade := TW3BlockBox.Create(Host);
  Shade.SizeToParent();

  var wd := Host.Width * 90 div 100;
  var hd := Host.Height * 80 div 100;
  var dx := (Host.Width div 2) - (wd div 2);
  var dy := (Host.Height div 2) - (hd div 2);

  var Dialog := TInfoDialog.Create(Shade);
  Dialog.Header.Title.Caption := Title;
  Dialog.SetBounds(dx, dy, wd, hd);
  Dialog.fxZoomIn(0.3, procedure ()
  begin
    Dialog.Content.Content.InnerHTML := Content;
    Dialog.Content.UpdateContent();
    Dialog.SetFocus();
  end);

  Dialog.Header.NextButton.OnClick := procedure (Sender: TObject)
  begin
    Dialog.fxFadeOut(0.2, procedure ()
    begin
      TW3Dispatch.Execute( procedure ()
      begin
        Dialog.free;
        Shade.free;
      end, 100);
    end);
  end;

  result := Dialog;
end;

end.

Using the code

The first thing you want to do is to create an instance of TAppInfoDb when your application starts. Remember to add your JSON file and that it’s formatted property, and then use the LoadFrom() method to load in the data:

  // Create our info database and load in the
  // introduction, features etc. JSON datafile
  FInfoDb := TAppInfoDB.Create;
  FInfoDb.LoadFrom('res/JSON1', nil);

The final parameter in the LoadFrom() method is a callback. So if you want to be notified when the file has loaded, just put an anonymous procedure there if you need it.

Showing a dialog with the information is then reduced to looking up the text you need by it’s ID, and firing up the reader dialog for it:

  W3Button1.OnClick := procedure (Sender: TObject)
  begin
    var LInfo: TAppInfoRecord;
    if FInfoDb.GetRecById('introduction', LInfo) then
      TInfoDialog.ShowDialog(LInfo.iiTitle, LInfo.iiText);
  end;

And that’s it! Simple, effective and ready to be dropped into any application. Enjoy!