904 lines
28 KiB
ObjectPascal
904 lines
28 KiB
ObjectPascal
/// JSON tree view bases on Synopse JSON parser
|
|
// - based on JSON TreeView VCL Component from pawel.glowacki@embarcadero.com
|
|
// - this unit is a part of the freeware Synopse mORMot framework,
|
|
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
|
|
unit SynJSONTreeView;
|
|
|
|
{
|
|
This file is part of Synopse framework.
|
|
|
|
Synopse framework. Copyright (C) 2022 Arnaud Bouchez
|
|
Synopse Informatique - https://synopse.info
|
|
|
|
*** BEGIN LICENSE BLOCK *****
|
|
Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
The contents of this file are subject to the Mozilla Public License Version
|
|
1.1 (the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
http://www.mozilla.org/MPL
|
|
|
|
Software distributed under the License is distributed on an "AS IS" basis,
|
|
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
for the specific language governing rights and limitations under the License.
|
|
|
|
The Original Code is Synopse mORMot framework.
|
|
|
|
The Initial Developer of the Original Code is Arnaud Bouchez.
|
|
|
|
Portions created by the Initial Developer are Copyright (C) 2022
|
|
the Initial Developer. All Rights Reserved.
|
|
|
|
Contributor(s):
|
|
- Esteban Martin (EMartin)
|
|
|
|
Alternatively, the contents of this file may be used under the terms of
|
|
either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
in which case the provisions of the GPL or the LGPL are applicable instead
|
|
of those above. If you wish to allow use of your version of this file only
|
|
under the terms of either the GPL or the LGPL, and not to allow others to
|
|
use your version of this file under the terms of the MPL, indicate your
|
|
decision by deleting the provisions above and replace them with the notice
|
|
and other provisions required by the GPL or the LGPL. If you do not delete
|
|
the provisions above, a recipient may use your version of this file under
|
|
the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
|
|
Version 1.18
|
|
- first public release, corresponding to Synopse mORMot Framework 1.18
|
|
|
|
TODO:
|
|
- FPC support (I just use Windows)
|
|
- Developed with Delphi 7, not tested with other Delphi versions
|
|
}
|
|
|
|
{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER
|
|
|
|
interface
|
|
|
|
uses
|
|
SysUtils,
|
|
Classes,
|
|
ComCtrls,
|
|
Controls,
|
|
SynCommons,
|
|
SynTable;
|
|
|
|
type
|
|
ESynJSONTreeView = class(ESynException);
|
|
TSynJSONTreeNode = class;
|
|
|
|
/// Tree node types
|
|
// - jtnObject: JSON object, i.e: {"fieldNumber":123,"fieldText":"text","fieldNull":null}
|
|
// - jtnObjectContent: conten of jtnObject, i.e: "fieldNumber":123
|
|
// - jtnArray: JSON array, i.e: [123,"text",{"fieldNumber":123,"fieldText":"text","fieldNull":null},null]
|
|
// - jtnArrayContent: content of jtnArray, i.e: 123 or {"fieldNumber":123,"fieldText":"text","fieldNull":null}
|
|
TSynJSONTreeNodeType = (jtnObject, jtnObjectContent, jtnArray, jtnArrayContent);
|
|
|
|
/// Event type for fire event when editing
|
|
TSynJSONTreeViewCustomInputEvent = procedure(Sender: TObject; Node: TSynJSONTreeNode;
|
|
var Prompt: RawUTF8; var Value: Variant;
|
|
var Handled: Boolean) of object;
|
|
/// Node extension for handling JSON value in the treeview
|
|
TSynJSONTreeNode = class(TTreeNode)
|
|
protected
|
|
fName: RawUTF8;
|
|
fJSONType: TSynJSONTreeNodeType;
|
|
fValue: Variant;
|
|
public
|
|
/// Clone the node
|
|
function Clone: TSynJSONTreeNode;
|
|
/// JSON type in the node
|
|
property JSONType: TSynJSONTreeNodeType read fJSONType write fJSONType;
|
|
/// Name of the JSON value
|
|
property Name: RawUTF8 read FName write fName;
|
|
/// JSON value
|
|
property Value: Variant read fValue write fValue;
|
|
end;
|
|
|
|
/// Allowed edition states of treeview
|
|
// - estEdit: allow edit a node
|
|
// - estDelete: allow delete a node
|
|
// - estInsert: allow insert a node
|
|
TSynJSONTreeViewEditionStateType = (estEdit , estDelete, estInsert);
|
|
TSynJSONTreeViewEditionStateSet = set of TSynJSONTreeViewEditionStateType;
|
|
|
|
/// TSynJSONTreeView is a treeview supporting mORMOn JSON document (TDocVariantData), the allowed tasks are:
|
|
// - edit a node
|
|
// - delete a node
|
|
// - add a node
|
|
// - save to file
|
|
// - load from file
|
|
// - generate JSON to string variable
|
|
// - implement a custom input for edit/insert node
|
|
TSynJSONTreeView = class(TTreeView)
|
|
private
|
|
fAllowedEditionStates: TSynJSONTreeViewEditionStateSet;
|
|
protected
|
|
fFirsttime: Boolean;
|
|
fJSONDocument: Variant;
|
|
fJSONText: RawUTF8;
|
|
fKey: Integer;
|
|
fOnDblClickBak: TNotifyEvent;
|
|
fOnEditingBak: TTVEditingEvent;
|
|
fOnKeyDownBak: TKeyEvent;
|
|
fOnCustomInput: TSynJSONTreeViewCustomInputEvent;
|
|
fVisibleChildrenCounts: boolean;
|
|
fVisibleByteSizes: boolean;
|
|
/// Add child node from JSON value
|
|
// - aNode: node from which add the child
|
|
// - aJSON: JSON value single or object/array
|
|
// - aNameOrIndex: if it is numeric, then is the index into the object/array, otherwise the name of the container node
|
|
function AddChildFromJSON(const aNode: TSynJSONTreeNode; const aJSON: Variant; const aNameOrIndex: Variant): TSynJSONTreeNode;
|
|
procedure BackupEvents;
|
|
procedure DeleteNode(Node: TSynJSONTreeNode; const aAsk: Boolean = True);
|
|
procedure DoOnCreateNodeClass(Sender: TCustomTreeView; var NodeClass: TTreeNodeClass);
|
|
procedure DoOnDblClick(Sender: TObject);
|
|
procedure DoOnEditing(Sender: TObject; Node: TTreeNode; var AllowEdit: Boolean);
|
|
procedure DoOnKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
|
|
procedure EditNode(Node: TSynJSONTreeNode);
|
|
procedure InsertNode(Node: TSynJSONTreeNode; const aJSON: Variant);
|
|
function IsActive: Boolean;
|
|
function IsSimpleJsonValue(v: Variant): boolean;{$ifdef HASINLINE}inline;{$endif}
|
|
procedure ProcessArray(const aCurrNode: TSynJSONTreeNode; const aValue: Variant; const aNameOrIndex: Variant);
|
|
procedure ProcessElement(currNode: TSynJSONTreeNode; arr: Variant; aIndex: integer);
|
|
function ProcessJSONText: boolean;
|
|
procedure ProcessObject(const aCurrNode: TSynJSONTreeNode; const aValue: Variant; const aNameOrIndex: Variant);
|
|
function ProcessPair(currNode: TSynJSONTreeNode; obj: Variant; aIndex: integer): TSynJSONTreeNode;
|
|
/// Assign and process the JSON text
|
|
// - if Value is null then clear the JSON text and the treeview content
|
|
procedure SetJSONText(const Value: RawUTF8);
|
|
/// Assign mORMot JSON Document
|
|
// - if Value is not equal to the previous value then load the JSON into the treeview
|
|
procedure SetSynJSONDocument(const Value: Variant);
|
|
/// Show how many children has a container node
|
|
procedure SetVisibleChildrenCounts(const Value: boolean);
|
|
/// Show byte size of a container node
|
|
procedure SetVisibleByteSizes(const Value: boolean);
|
|
/// Update info of a container node
|
|
procedure UpdateNodeInfo(aNode: TSynJSONTreeNode);
|
|
/// Get the original value from variant value
|
|
function VariantToRaw(const aVarData: Variant): Variant;
|
|
/// Assign source variant to dest variant but considering the dest data type
|
|
procedure VariantToVariant(aSource: Variant; var aDest: Variant);
|
|
/// Convert variant to UTF8 but checking the boolean type and convert it to true/false string
|
|
function VarToUTF8(const aVarData: Variant): RawUTF8;
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
|
|
/// Remove items and JSON text
|
|
procedure ClearAll;
|
|
/// Load from file
|
|
procedure LoadFromFile(const aFileName: TFileName);
|
|
/// Load the JSONText in the treeview, clear if empty
|
|
procedure LoadJSON;
|
|
/// Save to file
|
|
procedure SaveToFile(const aFileName: TFileName);
|
|
/// Generate JSON from the treeview
|
|
function ToJSON: RawUTF8;
|
|
|
|
/// JSON DocVariantData
|
|
property JSONDocument: Variant read FJSONDocument write SeTSynJSONDocument;
|
|
published
|
|
/// Allowed state editions
|
|
property AllowedEditionStates: TSynJSONTreeViewEditionStateSet read fAllowedEditionStates write fAllowedEditionStates;
|
|
/// Content the JSON text, when assigned parse the the JSON and load in the treeview
|
|
property JSONText: RawUTF8 read fJSONText write SetJSONText;
|
|
/// Display info over how many children has each node
|
|
property VisibleChildrenCounts: boolean read FVisibleChildrenCounts write SetVisibleChildrenCounts;
|
|
/// Display info over the size of node
|
|
property VisibleByteSizes: boolean read FVisibleByteSizes write SetVisibleByteSizes;
|
|
/// Event fired when editing for custom input
|
|
property OnCustomInput: TSynJSONTreeViewCustomInputEvent read fOnCustomInput write fOnCustomInput;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Windows,
|
|
Variants,
|
|
mORMotUILogin;
|
|
|
|
resourcestring
|
|
SInvalidJSONValue = 'Invalid JSON value';
|
|
SInvalidJSON = 'Invalid JSON: %';
|
|
SEnterValue = 'Enter Value';
|
|
SInsertNewItem = 'Insert new item';
|
|
SInsertNewItemPrompt = 'Enter JSON:\n\n object: <name>:<value>\n array: [<index>]:<value>\n\n ' +
|
|
'In array the brackets are required.';
|
|
SRemoveChildren = 'This node has children, are you sure to remove them';
|
|
SDeleteNode = 'Delete node\n\n% ?';
|
|
|
|
{ TSynJSONTreeView }
|
|
|
|
procedure TSynJSONTreeView.ClearAll;
|
|
begin
|
|
SetJSONText('');
|
|
Items.Clear;
|
|
end;
|
|
|
|
constructor TSynJSONTreeView.Create(AOwner: TComponent);
|
|
begin
|
|
inherited;
|
|
OnCreateNodeClass := DoOnCreateNodeClass;
|
|
fAllowedEditionStates := [estEdit, estDelete, estInsert];
|
|
fFirstTime := True;
|
|
FVisibleChildrenCounts := true;
|
|
FVisibleByteSizes := false;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.LoadJson;
|
|
var
|
|
v: Variant;
|
|
begin
|
|
BackupEvents;
|
|
Items.Clear;
|
|
|
|
if IsActive then
|
|
begin
|
|
v := fJSONDocument;
|
|
Items.Clear;
|
|
|
|
if (v._Kind = dvObject) then
|
|
ProcessObject(nil, v, Unassigned)
|
|
else if (v._Kind = dvArray) then
|
|
ProcessArray(nil, v, Unassigned)
|
|
else
|
|
raise ESynJSONTreeView.Create(SInvalidJSONValue);
|
|
|
|
FullExpand;
|
|
end;
|
|
end;
|
|
|
|
function TSynJSONTreeView.ProcessPair(currNode: TSynJSONTreeNode; obj: Variant; aIndex: integer): TSynJSONTreeNode;
|
|
var
|
|
p: Variant;
|
|
begin
|
|
Result := nil;
|
|
|
|
p := obj._(aIndex);
|
|
|
|
if IsSimpleJsonValue(p) then
|
|
begin
|
|
AddChildFromJSON(currNode, obj, aIndex);
|
|
Exit;
|
|
end;
|
|
|
|
if (p._Kind = dvObject) then
|
|
ProcessObject(currNode, _CopyFast(p), obj.Name(aIndex))
|
|
else if (p._Kind = dvArray) then
|
|
ProcessArray(currNode, _CopyFast(p), obj.Name(aIndex))
|
|
else
|
|
raise ESynJSONTreeView.Create(SInvalidJSONValue);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.ProcessElement(currNode: TSynJSONTreeNode; arr: Variant; aIndex: integer);
|
|
var
|
|
v: Variant;
|
|
begin
|
|
v := arr._(aIndex);
|
|
|
|
if IsSimpleJsonValue(v) then
|
|
begin
|
|
AddChildFromJSON(currNode, arr, aIndex);
|
|
Exit;
|
|
end;
|
|
|
|
if (v._Kind = dvObject) then
|
|
ProcessObject(currNode, _CopyFast(v), IntToStr(aIndex))
|
|
else if (v._Kind = dvArray) then
|
|
ProcessArray(currNode, _CopyFast(v), aIndex)
|
|
else
|
|
raise ESynJSONTreeView.Create(SInvalidJSONValue);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.SetSynJSONDocument(const Value: Variant);
|
|
begin
|
|
if fJSONDocument <> Value then
|
|
begin
|
|
fJSONDocument := Value;
|
|
ClearAll;
|
|
if IsActive then
|
|
LoadJson;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.SetVisibleByteSizes(const Value: boolean);
|
|
begin
|
|
if FVisibleByteSizes <> Value then
|
|
begin
|
|
FVisibleByteSizes := Value;
|
|
LoadJson;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.SetVisibleChildrenCounts(const Value: boolean);
|
|
begin
|
|
if FVisibleChildrenCounts <> Value then
|
|
begin
|
|
FVisibleChildrenCounts := Value;
|
|
LoadJson;
|
|
end;
|
|
end;
|
|
|
|
function TSynJSONTreeView.VarToUTF8(const aVarData: Variant): RawUTF8;
|
|
begin
|
|
if (TVarData(aVarData).VType = varBoolean) then
|
|
begin
|
|
if TVarData(aVarData).VBoolean then
|
|
Result := 'true'
|
|
else
|
|
Result := 'false';
|
|
end else
|
|
Result := VariantToUTF8(aVarData);
|
|
end;
|
|
|
|
function TSynJSONTreeView.ToJSON: RawUTF8;
|
|
var
|
|
lW: TTextWriter;
|
|
|
|
procedure ProcessNode(aNode: TSynJSONTreeNode);
|
|
|
|
procedure CheckLastComma(aW: TTextWriter);
|
|
begin
|
|
if (aW.TextLength > 0) and (not (aW.LastChar in [',','[','{'])) then
|
|
aW.Add(',');
|
|
end;
|
|
|
|
var
|
|
lNode: TSynJSONTreeNode;
|
|
begin
|
|
lNode := aNode;
|
|
repeat
|
|
if Assigned(lNode) then begin
|
|
case lNode.JSONType of
|
|
jtnObject:
|
|
begin
|
|
CheckLastComma(lW);
|
|
// numeric name is excluded
|
|
if (lNode.Name <> '') and (StrToIntDef(lNode.Name, MaxInt) = MaxInt) then
|
|
lW.AddFieldName(lNode.Name);
|
|
lW.Add('{');
|
|
end;
|
|
jtnArray:
|
|
begin
|
|
CheckLastComma(lW);
|
|
if (lNode.Name <> '') then
|
|
lW.AddFieldName(lNode.Name);
|
|
lW.Add('[');
|
|
end;
|
|
jtnObjectContent:
|
|
begin
|
|
CheckLastComma(lW);
|
|
lW.AddFieldName(lNode.Name);
|
|
lW.AddVariant(lNode.Value);
|
|
lW.Add(',');
|
|
end;
|
|
jtnArrayContent:
|
|
begin
|
|
CheckLastComma(lW);
|
|
lW.AddVariant(lNode.Value);
|
|
lW.Add(',');
|
|
end;
|
|
end;
|
|
end;
|
|
if lNode.HasChildren then begin
|
|
ProcessNode(TSynJSONTreeNode(lNode.GetFirstChild));
|
|
lW.CancelLastComma;
|
|
case lNode.JSONType of
|
|
jtnObject: lW.Add('}');
|
|
jtnArray: lW.Add(']');
|
|
end;
|
|
end;
|
|
lNode := TSynJSONTreeNode(lNode.GetNextSibling);
|
|
until not Assigned(lNode);
|
|
end;
|
|
|
|
begin
|
|
lW := TTextWriter.CreateOwnedStream;
|
|
try
|
|
ProcessNode(TSynJSONTreeNode(Items.GetFirstNode));
|
|
lW.CancelLastComma;
|
|
lW.SetText(Result);
|
|
finally
|
|
lW.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.DoOnCreateNodeClass(Sender: TCustomTreeView; var NodeClass: TTreeNodeClass);
|
|
begin
|
|
NodeClass := TSynJSONTreeNode;
|
|
end;
|
|
|
|
function TSynJSONTreeView.VariantToRaw(const aVarData: Variant): Variant;
|
|
var
|
|
lVar: TSQLVar;
|
|
lTmp: RawByteString;
|
|
begin
|
|
if VarIs(aVarData, [varBoolean]) then
|
|
begin
|
|
Result := aVarData;
|
|
Exit;
|
|
end;
|
|
VariantToSQLVar(aVarData, lTmp, lVar);
|
|
with lVar do
|
|
case VType of
|
|
ftNull: Result := null;
|
|
ftCurrency: Result := VInt64;
|
|
ftUTF8:
|
|
if Assigned(VText) then
|
|
Result := RawUTF8(PUTF8Char(VText))
|
|
else
|
|
Result := '';
|
|
ftInt64: Result := VInt64;
|
|
ftDouble: Result := VDouble;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.DoOnDblClick(Sender: TObject);
|
|
begin
|
|
if ReadOnly or (Selected.Count > 0) or (not (estEdit in fAllowedEditionStates)) then
|
|
Exit;
|
|
|
|
EditNode(TSynJSONTreeNode(Selected));
|
|
if Assigned(fOnDblClickBak) then
|
|
fOnDblClickBak(Sender);
|
|
end;
|
|
|
|
function TSynJSONTreeView.IsActive: Boolean;
|
|
begin
|
|
Result := (fJSONDocument <> Unassigned) and (not VarIsEmptyOrNull(fJSONDocument)) and (fJSONDocument._Kind <> dvUndefined);
|
|
end;
|
|
|
|
function TSynJSONTreeView.IsSimpleJsonValue(v: Variant): boolean;
|
|
begin
|
|
Result := VarIsEmptyOrNull(v) or VarIsNumeric(v) or VarIsStr(v) or VarIsStr(v);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.SetJSONText(const Value: RawUTF8);
|
|
begin
|
|
if fJSONText <> Value then
|
|
begin
|
|
fJSONText := Value;
|
|
if fJSONText <> '' then
|
|
ProcessJSONText
|
|
else
|
|
begin
|
|
TDocVariantData(fJSONDocument).Clear;
|
|
fJSONDocument := Unassigned;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TSynJSONTreeView.ProcessJSONText: boolean;
|
|
begin
|
|
if IsActive then
|
|
fJSONDocument.Clear;
|
|
TDocVariantData(fJSONDocument).InitJSON(fJSONText);
|
|
if (TDocVariantData(fJSONDocument).Kind = dvUndefined) then
|
|
raise ESynJSONTreeView.CreateUTF8(SInvalidJSON, [fJSONText]);
|
|
Result := IsActive;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.BackupEvents;
|
|
begin
|
|
if not fFirstTime then
|
|
Exit;
|
|
|
|
if not Assigned(fOnDblClickBak) and Assigned(OnDblClick) then
|
|
fOnDblClickBak := OnDblClick;
|
|
OnDblClick := DoOnDblClick;
|
|
|
|
if not Assigned(fOnEditingBak) and Assigned(OnEditing) then
|
|
fOnEditingBak := OnEditing;
|
|
OnEditing := DoOnEditing;
|
|
|
|
if not Assigned(fOnKeyDownBak) and Assigned(OnKeyDown) then
|
|
fOnKeyDownBak := OnKeyDown;
|
|
OnKeyDown := DoOnKeyDown;
|
|
|
|
fFirstTime := False;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.DoOnKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
|
|
begin
|
|
if ReadOnly or (not (Key in [VK_DELETE, VK_INSERT, VK_RETURN])) or
|
|
((Key = VK_DELETE) and (not (estDelete in fAllowedEditionStates))) or
|
|
((Key = VK_RETURN) and (not (estEdit in fAllowedEditionStates))) or
|
|
((Key = VK_INSERT) and (not (estInsert in fAllowedEditionStates))) then
|
|
Exit;
|
|
|
|
case Key of
|
|
VK_DELETE: DeleteNode(TSynJSONTreeNode(Selected));
|
|
VK_INSERT: InsertNode(TSynJSONTreeNode(Selected), Unassigned);
|
|
VK_RETURN: EditNode(TSynJSONTreeNode(Selected));
|
|
end;
|
|
|
|
if Assigned(fOnKeyDownBak) then
|
|
fOnKeyDownBak(Sender, Key, Shift);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.UpdateNodeInfo(aNode: TSynJSONTreeNode);
|
|
|
|
procedure UpdateNodeText(aNode: TSynJSONTreeNode; const aText: RawUTF8);
|
|
begin
|
|
if not VarIsNull(aNode.Value) then
|
|
aNode.Text := aText
|
|
else
|
|
aNode.Text := aNode.Name + ': null';
|
|
end;
|
|
|
|
begin
|
|
case aNode.JSONType of
|
|
jtnObject: begin
|
|
if (not Assigned(aNode.Parent)) or (TSynJSONTreeNode(aNode.Parent).JSONType <> jtnArray) then
|
|
UpdateNodeText(aNode, aNode.Name + ' {}')
|
|
else
|
|
UpdateNodeText(aNode, FormatUTF8('[%] {}', [aNode.Name]));
|
|
end;
|
|
jtnArray:
|
|
if not VarIsNull(aNode.Value) then
|
|
UpdateNodeText(aNode, aNode.Name + ' []');
|
|
end;
|
|
|
|
case aNode.JSONType of
|
|
jtnObject, jtnArray: begin
|
|
if VisibleChildrenCounts then
|
|
if not VarIsNull(aNode.Value) then
|
|
UpdateNodeText(aNode, aNode.Text + ' (' + IntToStr(aNode.Value._Count) + ')')
|
|
else
|
|
UpdateNodeText(aNode, '');
|
|
|
|
if VisibleByteSizes then
|
|
if not VarIsNull(aNode.Value) then
|
|
UpdateNodeText(aNode, aNode.Text + ' (size: ' + IntToStr(Length(aNode.Value._JSON)) + ' bytes)')
|
|
else
|
|
UpdateNodeText(aNode, '');
|
|
end;
|
|
jtnObjectContent, jtnArrayContent: begin
|
|
UpdateNodeText(aNode, '');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.DeleteNode(Node: TSynJSONTreeNode; const aAsk: Boolean);
|
|
|
|
procedure RecalculateChildNames(aNode: TSynJSONTreeNode);
|
|
var
|
|
lNode: TSynJSONTreeNode;
|
|
I: Integer;
|
|
begin
|
|
lNode := TSynJSONTreeNode(aNode.GetFirstChild);
|
|
if not Assigned(lNode) then
|
|
Exit;
|
|
I := 0;
|
|
case lNode.JSONType of
|
|
jtnObject: begin
|
|
repeat
|
|
if Assigned(lNode) then begin
|
|
if (StrToIntDef(lNode.Name, MaxInt) <> MaxInt) then
|
|
lNode.Name := IntToStr(I);
|
|
UpdateNodeInfo(lNode);
|
|
Inc(I);
|
|
end;
|
|
lNode := TSynJSONTreeNode(lNode.getNextSibling);
|
|
until not Assigned(lNode);
|
|
end;
|
|
jtnArrayContent: begin
|
|
repeat
|
|
if Assigned(lNode) then begin
|
|
lNode.Name := IntToStr(I);
|
|
lNode.Text := UTF8ToString(FormatUTF8('[%]: %', [lNode.Name, lNode.Value]));
|
|
Inc(I);
|
|
end;
|
|
lNode := TSynJSONTreeNode(lNode.getNextSibling);
|
|
until not Assigned(lNode);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure UpdateEmptyParent(aParent: TSynJSONTreeNode);
|
|
begin
|
|
if (aParent.Count = 0) then begin
|
|
aParent.Value := NULL;
|
|
case TSynJSONTreeNode(aParent.Parent).JSONType of
|
|
jtnObject: aParent.JSONType := jtnObjectContent;
|
|
jtnArray: aParent.JSONType := jtnArrayContent;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
lParent, lDeletedNode: TSynJSONTreeNode;
|
|
begin
|
|
if aAsk then
|
|
if (YesNo(UTF8ToString(FormatUTF8(SDeleteNode, [Node.Text])), '', False) <> mrYes) then
|
|
Exit;
|
|
lDeletedNode := Node.Clone;
|
|
lParent := TSynJSONTreeNode(Node.Parent);
|
|
Items.BeginUpdate;
|
|
try
|
|
Node.Delete;
|
|
Node := lDeletedNode;
|
|
// if node name is empty it means that the JSON object is empty
|
|
if (Node.Name = '') then begin
|
|
ClearAll;
|
|
Exit;
|
|
end;
|
|
case Node.JSONType of
|
|
jtnArray, jtnObject: begin
|
|
if (StrToIntDef(Node.Name, MaxInt) <> MaxInt) then
|
|
TDocVariantData(lParent.Value).Delete(StrToInt(Node.Name))
|
|
else
|
|
TDocVariantData(lParent.Value).Delete(Node.Name);
|
|
if (lParent.Count = 0) then
|
|
UpdateEmptyParent(lParent)
|
|
else
|
|
RecalculateChildNames(lParent);
|
|
UpdateNodeInfo(lParent);
|
|
end;
|
|
jtnArrayContent: begin
|
|
TDocVariantData(lParent.Value).Delete(StrToInt(Node.Name));
|
|
if (lParent.Count = 0) then
|
|
UpdateEmptyParent(lParent)
|
|
else
|
|
RecalculateChildNames(lParent);
|
|
UpdateNodeInfo(lParent);
|
|
end;
|
|
jtnObjectContent: begin
|
|
TDocVariantData(lParent.Value).Delete(Node.Name);
|
|
if (lParent.Count = 0) then
|
|
UpdateEmptyParent(lParent);
|
|
UpdateNodeInfo(lParent);
|
|
end;
|
|
end;
|
|
finally
|
|
Items.EndUpdate;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.InsertNode(Node: TSynJSONTreeNode; const aJSON: Variant);
|
|
var
|
|
lJSON: Variant;
|
|
lPrompt: RawUTF8;
|
|
lValue: Variant;
|
|
I: Integer;
|
|
lCount: Integer;
|
|
lHandled: Boolean;
|
|
begin
|
|
if (aJSON = Unassigned) then begin
|
|
lPrompt := SInsertNewItemPrompt;
|
|
|
|
if Assigned(fOnCustomInput) then
|
|
fOnCustomInput(Self, Node, lPrompt, lValue, lHandled);
|
|
|
|
if not lHandled then
|
|
lValue := InputBox(SInsertNewItem, lPrompt, '');
|
|
|
|
|
|
if (lValue = '') then
|
|
Exit;
|
|
|
|
if (lValue[1] <> '[') then
|
|
if (lValue[1] <> '{') then
|
|
lValue := '{' + lValue + '}';
|
|
TDocVariantData(lJSON).InitJSON(lValue);
|
|
if (TDocVariantData(lJSON).Kind = dvUndefined) then
|
|
raise ESynJSONTreeView.CreateUTF8(SInvalidJSON, [lValue]);
|
|
end else
|
|
lJSON := aJSON;
|
|
// always will be an object the inserted node
|
|
lCount := lJSON._Count;
|
|
TDocVariantData(TSynJSONTreeNode(Node).Value).AddOrUpdateObject(lJSON);
|
|
for I := 0 to lCount-1 do begin
|
|
lValue := lJSON._(I);
|
|
if IsSimpleJsonValue(lValue) then begin
|
|
AddChildFromJSON(Node, lJSON, I);
|
|
end else
|
|
ProcessObject(Node, _CopyFast(lValue), lJSON.Name(I));
|
|
end;
|
|
UpdateNodeInfo(Node);
|
|
end;
|
|
|
|
function TSynJSONTreeView.AddChildFromJSON(const aNode: TSynJSONTreeNode; const aJSON: Variant; const aNameOrIndex: Variant): TSynJSONTreeNode;
|
|
var
|
|
lValue: Variant;
|
|
begin
|
|
Result := TSynJSONTreeNode(Items.AddChild(aNode, ''));
|
|
if not VarIsEmpty(aNameOrIndex) and (VariantToIntegerDef(aNameOrIndex, -1) <> -1) then
|
|
lValue := VariantToRaw(aJSON._(aNameOrIndex))
|
|
else
|
|
lValue := aJSON;
|
|
|
|
if (TDocVariantData(lValue).Kind <> dvUndefined) then
|
|
Result.Value := _CopyFast(lValue)
|
|
else
|
|
Result.Value := lValue;
|
|
|
|
case TDocVariantData(aJSON).Kind of
|
|
dvObject:
|
|
if not VarIsEmpty(aNameOrIndex) then begin
|
|
// is name or index is numeric, the text property is assigned showing the name and value, otherwise is a
|
|
// container node
|
|
if (VariantToIntegerDef(aNameOrIndex, -1) <> -1) then begin
|
|
Result.Name := aJSON.Name(aNameOrIndex);
|
|
Result.JSONType := jtnObjectContent;
|
|
Result.Text := Result.Name + ': ' + VarToUTF8(lValue);
|
|
end else begin
|
|
Result.Name := aNameOrIndex;
|
|
Result.JSONType := jtnObject;
|
|
end;
|
|
end;
|
|
dvArray:
|
|
if not VarIsEmpty(aNameOrIndex) then begin
|
|
// is name or index is numeric, the text property is assigned showing the name and value, otherwise is a
|
|
// container node
|
|
if (VariantToIntegerDef(aNameOrIndex, -1) <> -1) then begin
|
|
Result.Name := aNameOrIndex;
|
|
Result.JSONType := jtnArrayContent;
|
|
Result.Text := IntToStr(aNameOrIndex) + ': ' + VarToUTF8(lValue);
|
|
end else begin
|
|
Result.Name := aNameOrIndex;
|
|
Result.JSONType := jtnArray;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.ProcessObject(const aCurrNode: TSynJSONTreeNode; const aValue: Variant; const aNameOrIndex: Variant);
|
|
var
|
|
lCount: Integer;
|
|
I: Integer;
|
|
lCurrNode: TSynJSONTreeNode;
|
|
begin
|
|
lCurrNode := AddChildFromJSON(aCurrNode, aValue, aNameOrIndex);
|
|
UpdateNodeInfo(lCurrNode);
|
|
lCount := aValue._Count;
|
|
for I := 0 to lCount - 1 do
|
|
ProcessPair(lCurrNode, _CopyFast(aValue), I);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.ProcessArray(const aCurrNode: TSynJSONTreeNode; const aValue: Variant; const aNameOrIndex: Variant);
|
|
var
|
|
lCount: Integer;
|
|
I: Integer;
|
|
lCurrNode: TSynJSONTreeNode;
|
|
begin
|
|
lCurrNode := AddChildFromJSON(aCurrNode, aValue, aNameOrIndex);
|
|
UpdateNodeInfo(lCurrNode);
|
|
lCount := aValue._Count;
|
|
for I := 0 to lCount - 1 do
|
|
ProcessElement(lCurrNode, aValue, I);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.EditNode(Node: TSynJSONTreeNode);
|
|
var
|
|
lPrompt: RawUTF8;
|
|
lJSON, lValue, lDestValue: Variant;
|
|
lHandled: Boolean;
|
|
begin
|
|
lPrompt := UTF8ToString(TSynJSONTreeNode(Selected).Name);
|
|
if (Selected.Text[1] = '[') then
|
|
lPrompt := FormatUTF8('[%]', [lPrompt]);
|
|
lValue := '';
|
|
if not VarIsNull(TSynJSONTreeNode(Selected).Value) then
|
|
lValue := TSynJSONTreeNode(Selected).Value;
|
|
if Assigned(fOnCustomInput) then
|
|
fOnCustomInput(Self, Node, lPrompt, lValue, lHandled);
|
|
if not lHandled then
|
|
lValue := InputBox(SEnterValue, lPrompt, lValue);
|
|
|
|
// if not modification, exit
|
|
if (lValue = TSynJSONTreeNode(Selected).Value) then
|
|
Exit;
|
|
|
|
// remove children if input data was null
|
|
if (TDocVariantData(lValue).Kind = dvUndefined) and
|
|
((lValue = '') or IdemPChar(pointer(VariantToUTF8(lValue)), 'NULL')) and
|
|
Selected.HasChildren then begin
|
|
if (YesNo(SRemoveChildren, '', False) = mrYes) then begin
|
|
Selected.DeleteChildren;
|
|
TSynJSONTreeNode(Selected).Value := NULL;
|
|
UpdateNodeInfo(TSynJSONTreeNode(Selected));
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
TDocVariantData(lJSON).InitJSON(lValue);
|
|
case TDocVariantData(lJSON).Kind of
|
|
dvUndefined: begin
|
|
Selected.Text := FormatUTF8('%: %', [lPrompt, lValue]);
|
|
lDestValue := TSynJSONTreeNode(Selected).Value;
|
|
VariantToVariant(lValue, lDestValue);
|
|
TSynJSONTreeNode(Selected).Value := lDestValue;
|
|
if Assigned(Selected.Parent) then begin
|
|
case TSynJSONTreeNode(Selected.Parent).JSONType of
|
|
jtnObject: TSynJSONTreeNode(Selected).JSONType := jtnObjectContent;
|
|
jtnArray: begin
|
|
TSynJSONTreeNode(Selected).JSONType := jtnArrayContent;
|
|
TDocVariantData(TSynJSONTreeNode(Selected.Parent).Value).Values[StrToInt(TSynJSONTreeNode(Selected).Name)] :=
|
|
TSynJSONTreeNode(Selected).Value;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
else
|
|
case TDocVariantData(lJSON).Kind of
|
|
dvObject: TSynJSONTreeNode(Selected).JSONType := jtnObject;
|
|
dvArray: TSynJSONTreeNode(Selected).JSONType := jtnArray;
|
|
end;
|
|
Node.Value := _CopyFast(lJSON);
|
|
if Node.HasChildren then
|
|
Node.DeleteChildren;
|
|
InsertNode(Node, lJSON);
|
|
UpdateNodeInfo(Node);
|
|
end;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.LoadFromFile(const aFileName: TFileName);
|
|
begin
|
|
if (aFileName = '') then
|
|
Exit;
|
|
SetJSONText(StringFromFile(aFileName));
|
|
LoadJSON;
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.SaveToFile(const aFileName: TFileName);
|
|
var
|
|
lJSON: RawUTF8;
|
|
begin
|
|
if (aFileName = '') then
|
|
Exit;
|
|
lJSON := ToJSON;
|
|
FileFromString(lJSON, aFileName);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.DoOnEditing(Sender: TObject; Node: TTreeNode; var AllowEdit: Boolean);
|
|
begin
|
|
AllowEdit := (estEdit in fAllowedEditionStates);
|
|
if AllowEdit and Assigned(fOnEditingBak) then
|
|
fOnEditingBak(Sender, Node, AllowEdit);
|
|
end;
|
|
|
|
procedure TSynJSONTreeView.VariantToVariant(aSource: Variant; var aDest: Variant);
|
|
var
|
|
lVar: TSQLVar;
|
|
lTmp: RawByteString;
|
|
begin
|
|
VariantToSQLVar(aDest, lTmp, lVar);
|
|
with lVar do
|
|
case VType of
|
|
ftNull: aDest := aSource;
|
|
ftCurrency: aDest := aSource.VInt64;
|
|
ftUTF8: aDest := VariantToUTF8(aSource);
|
|
ftInt64: begin
|
|
if (aSource <> '') then
|
|
if (UpperCaseU(aSource) <> 'TRUE') and (UpperCaseU(aSource) <> 'FALSE') then begin
|
|
aDest := StrToInt64Def(aSource, MaxInt);
|
|
if (aDest = MaxInt) then
|
|
raise ESynJSONTreeView.CreateUTF8('Invalid input data type: %', [aSource]);
|
|
end else
|
|
if (UpperCaseU(aSource) = 'TRUE') then
|
|
aDest := True
|
|
else
|
|
aDest := False;
|
|
end;
|
|
ftDouble: aDest := aSource.VDouble;
|
|
end;
|
|
end;
|
|
|
|
{ TSynJSONTreeNode }
|
|
|
|
function TSynJSONTreeNode.Clone: TSynJSONTreeNode;
|
|
begin
|
|
Result := TSynJSONTreeNode.Create(nil);
|
|
Result.Name := fName;
|
|
Result.JSONType := fJSONType;
|
|
Result.Value := fValue;
|
|
end;
|
|
|
|
end.
|
|
|