1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE examples.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    The code included in this file is provided under the terms of the ISC license
8    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
9    To use, copy, modify, and/or distribute this software for any purpose with or
10    without fee is hereby granted provided that the above copyright notice and
11    this permission notice appear in all copies.
12 
13    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
14    WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
15    PURPOSE, ARE DISCLAIMED.
16 
17   ==============================================================================
18 */
19 
20 /*******************************************************************************
21  The block below describes the properties of this PIP. A PIP is a short snippet
22  of code that can be read by the Projucer and used to generate a JUCE project.
23 
24  BEGIN_JUCE_PIP_METADATA
25 
26  name:             XMLandJSONDemo
27  version:          1.0.0
28  vendor:           JUCE
29  website:          http://juce.com
30  description:      Reads XML and JSON files.
31 
32  dependencies:     juce_core, juce_data_structures, juce_events, juce_graphics,
33                    juce_gui_basics, juce_gui_extra
34  exporters:        xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone
35 
36  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
37 
38  type:             Component
39  mainClass:        XMLandJSONDemo
40 
41  useLocalCopy:     1
42 
43  END_JUCE_PIP_METADATA
44 
45 *******************************************************************************/
46 
47 #pragma once
48 
49 #include "../Assets/DemoUtilities.h"
50 
51 //==============================================================================
52 class XmlTreeItem  : public TreeViewItem
53 {
54 public:
XmlTreeItem(XmlElement & x)55     XmlTreeItem (XmlElement& x)  : xml (x)    {}
56 
getUniqueName()57     String getUniqueName() const override
58     {
59         if (xml.getTagName().isEmpty())
60             return "unknown";
61 
62         return xml.getTagName();
63     }
64 
mightContainSubItems()65     bool mightContainSubItems() override
66     {
67         return xml.getFirstChildElement() != nullptr;
68     }
69 
paintItem(Graphics & g,int width,int height)70     void paintItem (Graphics& g, int width, int height) override
71     {
72         // if this item is selected, fill it with a background colour..
73         if (isSelected())
74             g.fillAll (Colours::blue.withAlpha (0.3f));
75 
76         // use a "colour" attribute in the xml tag for this node to set the text colour..
77         g.setColour (Colour::fromString (xml.getStringAttribute ("colour", "ff000000")));
78         g.setFont ((float) height * 0.7f);
79 
80         // draw the xml element's tag name..
81         g.drawText (xml.getTagName(),
82                     4, 0, width - 4, height,
83                     Justification::centredLeft, true);
84     }
85 
itemOpennessChanged(bool isNowOpen)86     void itemOpennessChanged (bool isNowOpen) override
87     {
88         if (isNowOpen)
89         {
90             // if we've not already done so, we'll now add the tree's sub-items. You could
91             // also choose to delete the existing ones and refresh them if that's more suitable
92             // in your app.
93             if (getNumSubItems() == 0)
94             {
95                 // create and add sub-items to this node of the tree, corresponding to
96                 // each sub-element in the XML..
97 
98                 for (auto* child : xml.getChildIterator())
99                 {
100                     jassert (child != nullptr);
101                     addSubItem (new XmlTreeItem (*child));
102                 }
103             }
104         }
105         else
106         {
107             // in this case, we'll leave any sub-items in the tree when the node gets closed,
108             // though you could choose to delete them if that's more appropriate for
109             // your application.
110         }
111     }
112 
113 private:
114     XmlElement& xml;
115 
116     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XmlTreeItem)
117 };
118 
119 //==============================================================================
120 class JsonTreeItem  : public TreeViewItem
121 {
122 public:
JsonTreeItem(Identifier i,var value)123     JsonTreeItem (Identifier i, var value)
124         : identifier (i),
125           json (value)
126     {}
127 
getUniqueName()128     String getUniqueName() const override
129     {
130         return identifier.toString() + "_id";
131     }
132 
mightContainSubItems()133     bool mightContainSubItems() override
134     {
135         if (auto* obj = json.getDynamicObject())
136             return obj->getProperties().size() > 0;
137 
138         return json.isArray();
139     }
140 
paintItem(Graphics & g,int width,int height)141     void paintItem (Graphics& g, int width, int height) override
142     {
143         // if this item is selected, fill it with a background colour..
144         if (isSelected())
145             g.fillAll (Colours::blue.withAlpha (0.3f));
146 
147         g.setColour (Colours::black);
148         g.setFont ((float) height * 0.7f);
149 
150         // draw the element's tag name..
151         g.drawText (getText(),
152                     4, 0, width - 4, height,
153                     Justification::centredLeft, true);
154     }
155 
itemOpennessChanged(bool isNowOpen)156     void itemOpennessChanged (bool isNowOpen) override
157     {
158         if (isNowOpen)
159         {
160             // if we've not already done so, we'll now add the tree's sub-items. You could
161             // also choose to delete the existing ones and refresh them if that's more suitable
162             // in your app.
163             if (getNumSubItems() == 0)
164             {
165                 // create and add sub-items to this node of the tree, corresponding to
166                 // the type of object this var represents
167 
168                 if (json.isArray())
169                 {
170                     for (int i = 0; i < json.size(); ++i)
171                     {
172                         auto& child = json[i];
173                         jassert (! child.isVoid());
174                         addSubItem (new JsonTreeItem ({}, child));
175                     }
176                 }
177                 else if (auto* obj = json.getDynamicObject())
178                 {
179                     auto& props = obj->getProperties();
180 
181                     for (int i = 0; i < props.size(); ++i)
182                     {
183                         auto id = props.getName (i);
184 
185                         auto child = props[id];
186                         jassert (! child.isVoid());
187 
188                         addSubItem (new JsonTreeItem (id, child));
189                     }
190                 }
191             }
192         }
193         else
194         {
195             // in this case, we'll leave any sub-items in the tree when the node gets closed,
196             // though you could choose to delete them if that's more appropriate for
197             // your application.
198         }
199     }
200 
201 private:
202     Identifier identifier;
203     var json;
204 
205     /** Returns the text to display in the tree.
206         This is a little more complex for JSON than XML as nodes can be strings, objects or arrays.
207      */
getText()208     String getText() const
209     {
210         String text;
211 
212         if (identifier.isValid())
213             text << identifier.toString();
214 
215         if (! json.isVoid())
216         {
217             if (text.isNotEmpty() && (! json.isArray()))
218                 text << ": ";
219 
220             if (json.isObject() && (! identifier.isValid()))
221                 text << "[Array]";
222             else if (! json.isArray())
223                 text << json.toString();
224         }
225 
226         return text;
227     }
228 
229     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JsonTreeItem)
230 };
231 
232 //==============================================================================
233 class XMLandJSONDemo   : public Component,
234                          private CodeDocument::Listener
235 {
236 public:
237     /** The type of database to parse. */
238     enum Type
239     {
240         xml,
241         json
242     };
243 
XMLandJSONDemo()244     XMLandJSONDemo()
245     {
246         setOpaque (true);
247 
248         addAndMakeVisible (typeBox);
249         typeBox.addItem ("XML",  1);
250         typeBox.addItem ("JSON", 2);
251 
252         typeBox.onChange = [this]
253         {
254             if (typeBox.getSelectedId() == 1)
255                 reset (xml);
256             else
257                 reset (json);
258         };
259 
260         comboBoxLabel.attachToComponent (&typeBox, true);
261 
262         addAndMakeVisible (codeDocumentComponent);
263         codeDocument.addListener (this);
264 
265         addAndMakeVisible (resultsTree);
266         resultsTree.setColour (TreeView::backgroundColourId, Colours::white);
267         resultsTree.setDefaultOpenness (true);
268 
269         addAndMakeVisible (errorMessage);
270         errorMessage.setReadOnly (true);
271         errorMessage.setMultiLine (true);
272         errorMessage.setCaretVisible (false);
273         errorMessage.setColour (TextEditor::outlineColourId, Colours::transparentWhite);
274         errorMessage.setColour (TextEditor::shadowColourId,  Colours::transparentWhite);
275 
276         typeBox.setSelectedId (1);
277 
278         setSize (500, 500);
279     }
280 
~XMLandJSONDemo()281     ~XMLandJSONDemo() override
282     {
283         resultsTree.setRootItem (nullptr);
284     }
285 
paint(Graphics & g)286     void paint (Graphics& g) override
287     {
288         g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
289     }
290 
resized()291     void resized() override
292     {
293         auto area = getLocalBounds();
294 
295         typeBox.setBounds (area.removeFromTop (36).removeFromRight (150).reduced (8));
296         codeDocumentComponent.setBounds (area.removeFromTop(area.getHeight() / 2).reduced (8));
297         resultsTree          .setBounds (area.reduced (8));
298         errorMessage         .setBounds (resultsTree.getBounds());
299     }
300 
301 private:
302     ComboBox typeBox;
303     Label comboBoxLabel { {}, "Database Type:" };
304     CodeDocument codeDocument;
305     CodeEditorComponent codeDocumentComponent  { codeDocument, nullptr };
306     TreeView resultsTree;
307 
308     std::unique_ptr<TreeViewItem> rootItem;
309     std::unique_ptr<XmlElement> parsedXml;
310     TextEditor errorMessage;
311 
rebuildTree()312     void rebuildTree()
313     {
314         std::unique_ptr<XmlElement> openness;
315 
316         if (rootItem.get() != nullptr)
317             openness = rootItem->getOpennessState();
318 
319         createNewRootNode();
320 
321         if (openness.get() != nullptr && rootItem.get() != nullptr)
322             rootItem->restoreOpennessState (*openness);
323     }
324 
createNewRootNode()325     void createNewRootNode()
326     {
327         // clear the current tree
328         resultsTree.setRootItem (nullptr);
329         rootItem.reset();
330 
331         // try and parse the editor's contents
332         switch (typeBox.getSelectedItemIndex())
333         {
334             case xml:           rootItem.reset (rebuildXml());        break;
335             case json:          rootItem.reset (rebuildJson());       break;
336             default:            rootItem.reset();                     break;
337         }
338 
339         // if we have a valid TreeViewItem hide any old error messages and set our TreeView to use it
340         if (rootItem.get() != nullptr)
341             errorMessage.clear();
342 
343         errorMessage.setVisible (! errorMessage.isEmpty());
344 
345         resultsTree.setRootItem (rootItem.get());
346     }
347 
348     /** Parses the editor's contents as XML. */
rebuildXml()349     TreeViewItem* rebuildXml()
350     {
351         parsedXml.reset();
352 
353         XmlDocument doc (codeDocument.getAllContent());
354         parsedXml = doc.getDocumentElement();
355 
356         if (parsedXml.get() == nullptr)
357         {
358             auto error = doc.getLastParseError();
359 
360             if (error.isEmpty())
361                 error = "Unknown error";
362 
363             errorMessage.setText ("Error parsing XML: " + error, dontSendNotification);
364 
365             return nullptr;
366         }
367 
368         return new XmlTreeItem (*parsedXml);
369     }
370 
371     /** Parses the editor's contents as JSON. */
rebuildJson()372     TreeViewItem* rebuildJson()
373     {
374         var parsedJson;
375         auto result = JSON::parse (codeDocument.getAllContent(), parsedJson);
376 
377         if (! result.wasOk())
378         {
379             errorMessage.setText ("Error parsing JSON: " + result.getErrorMessage());
380             return nullptr;
381         }
382 
383         return new JsonTreeItem (Identifier(), parsedJson);
384     }
385 
386     /** Clears the editor and loads some default text. */
reset(Type type)387     void reset (Type type)
388     {
389         switch (type)
390         {
391             case xml:   codeDocument.replaceAllContent (loadEntireAssetIntoString ("treedemo.xml")); break;
392             case json:  codeDocument.replaceAllContent (loadEntireAssetIntoString ("juce_module_info")); break;
393             default:    codeDocument.replaceAllContent ({}); break;
394         }
395     }
396 
codeDocumentTextInserted(const String &,int)397     void codeDocumentTextInserted (const String&, int) override     { rebuildTree(); }
codeDocumentTextDeleted(int,int)398     void codeDocumentTextDeleted (int, int) override                { rebuildTree(); }
399 
400     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (XMLandJSONDemo)
401 };
402