diff --git a/README.md b/README.md index 9fd7ada..2c4bf08 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,23 @@ # DynamicComponents-AI2 +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/c9fee4822c864505a2ade6d19731caa5)](https://app.codacy.com/manual/ysfchn/DynamicComponents-AI2?utm_source=github.com&utm_medium=referral&utm_content=ysfchn/DynamicComponents-AI2&utm_campaign=Badge_Grade_Dashboard) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fysfchn%2FDynamicComponents-AI2.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fysfchn%2FDynamicComponents-AI2?ref=badge_shield) + Fully supported Dynamic Components extension for MIT App Inventor 2. It is based on Java's reflection feature, so it creates the components by searching for a class by just typing its name. So it doesn't have a limited support for specific components, because it supports every component which is ever added to App Inventor platform! ## Blocks ![](blocks.png) -Source code is licensed under MIT license. +Source code is licensed under MIT license. You must include the license notice in all copies or substantial uses of the work. + +## Building + +You will need: + +* Java 1.8 (either OpenJDK or Oracle) +* Ant 1.10 or higher + +Then execute `ant extensions` in the root of the repository. + +## License +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fysfchn%2FDynamicComponents-AI2.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fysfchn%2FDynamicComponents-AI2?ref=badge_large) diff --git a/src/com/yusufcihan/DynamicComponents/DynamicComponents.java b/src/com/yusufcihan/DynamicComponents/DynamicComponents.java index 429db07..fbcf5fe 100644 --- a/src/com/yusufcihan/DynamicComponents/DynamicComponents.java +++ b/src/com/yusufcihan/DynamicComponents/DynamicComponents.java @@ -1,174 +1,355 @@ -package com.yusufcihan.DynamicComponents; +package com.yusufcihan.DynamicComponents; import com.google.appinventor.components.annotations.*; -import com.google.appinventor.components.runtime.*; import com.google.appinventor.components.common.*; +import com.google.appinventor.components.runtime.*; +import com.google.appinventor.components.runtime.errors.YailRuntimeError; import com.google.appinventor.components.runtime.util.YailList; -import com.google.appinventor.components.runtime.errors.YailRuntimeError; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; -import java.util.Hashtable; -import java.util.ArrayList; -import java.util.List; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; import java.util.Set; -import java.lang.Boolean; -@DesignerComponent(version = 3, - description = "Dynamic Components extension to create any type of dynamic component in any arrangement.

- by Yusuf Cihan", - category = ComponentCategory.EXTENSION, - nonVisible = true, - iconName = "https://yusufcihan.com/img/dynamiccomponents.png") +@DesignerComponent(version = 4, + description = "Dynamic Components extension to create any type of dynamic component in any arrangement.

- by Yusuf Cihan", + category = ComponentCategory.EXTENSION, + nonVisible = true, + iconName = "https://yusufcihan.com/img/dynamiccomponents.png") @SimpleObject(external = true) public class DynamicComponents extends AndroidNonvisibleComponent implements Component { - - // Variables + + // ------------------------ + // VARIABLES + // ------------------------ + + /* + ----------------------- + Hashtable COMPONENTS + + Contains the created components. Key is the ID of the components, and their values are the components + that created with Create block. + + ----------------------- + */ private Hashtable COMPONENTS = new Hashtable(); + + /* + ----------------------- + String BASE_PACKAGE + + Specifies the base package for creating the components. + + ----------------------- + */ private String BASE_PACKAGE = "com.google.appinventor.components.runtime"; + + /* + ----------------------- + String LAST_ID + + Stores the last ID that created with the Create block. + + ----------------------- + */ private String LAST_ID = ""; - + + /* + ----------------------- + JSONArray PROPERTIESARRAY + + Stores the component template. Needs to be cleared before rendering Schema operation. + + ----------------------- + */ + private JSONArray PROPERTIESARRAY = new JSONArray(); + public DynamicComponents(ComponentContainer container) { super(container.$form()); } - + /* private String BasePackage() { return BASE_PACKAGE; } - + private void BasePackage(String packageName) { BASE_PACKAGE = packageName; } */ + // ------------------------ + // EVENTS + // ------------------------ + + + /* + ----------------------- + SchemaCreated + + Raises after Schema has been created with Schema block. + + ----------------------- + */ + @SimpleEvent(description = "Raises after Schema has been created with Schema block.") + public void SchemaCreated() { + EventDispatcher.dispatchEvent(this, "SchemaCreated"); + } + // ------------------------ // MAIN METHODS // ------------------------ - /* + + /* + ----------------------- + Create + Creates a new dynamic component. It supports all component that added to your current AI2 builder. - In componentName, you can type the component's name like "Button", or you can pass a static component then it can create a new instance of it. + In componentName, you can type the component's name like "Button", + or you can pass a static component then it can create a new instance of it. + + + -- Parameters -- + AndroidViewComponent in : To specify where component will be created in. + Object componentName : Name of the component like "Button" or add a static component block. + String id : ID of the component to create + + ----------------------- */ - @SimpleFunction(description = - "Creates a new dynamic component. It supports all component that added to your current AI2 builder.\n" - + "In componentName, you can type the component's name like 'Button',\n" - + "or you can pass a static component then it can create a new instance of it.") + @SimpleFunction(description = + "Creates a new dynamic component. It supports all component that added to your current AI2 builder.\n" + + "In componentName, you can type the component's name like 'Button',\n" + + "or you can pass a static component then it can create a new instance of it.") public void Create(AndroidViewComponent in, Object componentName, String id) { Component component = null; LAST_ID = id; String error = null; // Check if id is used by another created dynamic component. - if (!COMPONENTS.containsKey(id)) - { - try - { + if (!COMPONENTS.containsKey(id)) { + try { // If input is a component name then create a instance of it. - if (componentName instanceof String) - { + if (componentName instanceof String) { // Return the component class by looking the its name. Class clasz = Class.forName(BASE_PACKAGE + "." + componentName.toString().replace(" ", "")); // Create constructor object for creating a new instance. - Constructor constructor = clasz.getConstructor(new Class[] { ComponentContainer.class }); + Constructor constructor = clasz.getConstructor(new Class[]{ComponentContainer.class}); // Create a new instance of specified component. - component = (Component)constructor.newInstance((ComponentContainer)in); - } - else - { - String packageName = componentName.getClass().getPackage().getName(); - if (packageName.equals(BASE_PACKAGE)) - { - Class clasz = Class.forName(componentName.getClass().getName()); - Constructor constructor = clasz.getConstructor(new Class[] { ComponentContainer.class }); - component = (Component)constructor.newInstance((ComponentContainer)in); - } - else - { - error = "Input is not a string or a valid component type."; - } - + component = (Component) constructor.newInstance((ComponentContainer) in); + // If input is a component's itself, then create a new component from itself. + } else if (componentName instanceof Component) { + Class clasz = Class.forName(componentName.getClass().getName()); + Constructor constructor = clasz.getConstructor(new Class[]{ComponentContainer.class}); + component = (Component) constructor.newInstance((ComponentContainer) in); + } else { + error = "Input is not a component block or a component name."; } - + } catch (Exception exception) { + error = "" + exception; } - catch (Exception e) - { - error = e.getMessage(); - } - } - else - { + } else { error = "This ID is already used for another component, please pick another. ID needs to be unique for all components!"; - } + } - if ((id.trim().length() == 0) || (id == null)) - { + if (id == null || id.trim().isEmpty()) { error = "ID is blank. Please enter a valid ID."; } - if (error != null) - { + if (error != null) { throw new YailRuntimeError(error, "DynamicComponents-AI2 Error"); - } - else { + } else { COMPONENTS.put(id, component); - } + } } - /* - Changes ID of one of created components to a new one. The old ID must be exist and new ID mustn't exist. + + /* + ----------------------- + Schema + + Imports a JSON string that is a template for creating the dynamic components + automatically with single block. Templates can also contain parameters that will be + replaced with the values which defined in the "parameters" list. + + + -- Parameters -- + AndroidViewComponent in : To specify where base component will be created in. + String template : Template source as plain JSON text + YailList parameters : Parameters that will be replaced in template text. + + ----------------------- + */ + @SimpleFunction(description = + "Imports a JSON string that is a template for creating the dynamic components\n" + + "automatically with single block. Templates can also contain parameters that will be\n" + + "replaced with the values which defined in the 'parameters' list.") + public void Schema(AndroidViewComponent in, String template, YailList parameters) { + try { + // Remove the contents of the array by creating a new JSONArray. + PROPERTIESARRAY = new JSONArray(); + // Create a JSONObject from template for checking. + JSONObject j = new JSONObject(template); + // Save the template string to a new variable for editing. + String modifiedTemplate = template; + // Check if JSON contains "keys". + if (j.has("keys")) { + // Throw a runtime error if parameter count is lower than required parameter count. + if (j.optJSONArray("keys").length() > parameters.length()) + { + throw new YailRuntimeError("Input parameter count is lower than the requirement!", "Error"); + } + else + { + // Replace the template keys with their values. + // For example; + // {0} --> "a value" + for (int i = 0; i < j.optJSONArray("keys").length(); i++) { + modifiedTemplate = modifiedTemplate.replace("{" + j.getJSONArray("keys").getString(i) + "}", parameters.getString(i).replace("\"", "")); + } + } + } + + // Check the metadata version for checking compatibility for next/previous versions of the extension. + // Will be used in the future releases. + if (j.optInt("metadata-version", 0) == 0) + throw new YailRuntimeError("Metadata version is not specified!", "Error"); + // Lastly parse the JSONObject. + Parse(new JSONObject(modifiedTemplate), ""); + // Delete the first element, because it contains metadata instead of components. + PROPERTIESARRAY.remove(0); + + // Start creating the extensions (finally). + for (int i = 0; i < PROPERTIESARRAY.length(); i++) { + // Check if component has an ID key. + if (!PROPERTIESARRAY.getJSONObject(i).has("id")) + { + throw new YailRuntimeError("One or more of the components has not an ID in template!", "Error"); + } + + // If a component JSONObject doesn't contain an "in" key then insert it in the main component + // that specified as this method's "in" parameter. + if (!PROPERTIESARRAY.getJSONObject(i).has("in")) + { + Create(in, PROPERTIESARRAY.getJSONObject(i).getString("type"), PROPERTIESARRAY.getJSONObject(i).getString("id")); + } + // Else, insert it in the another component that is specified with an ID. + else + { + Create((AndroidViewComponent)GetComponent(PROPERTIESARRAY.getJSONObject(i).getString("in")), PROPERTIESARRAY.getJSONObject(i).getString("type"), PROPERTIESARRAY.getJSONObject(i).getString("id")); + } + + // If JSONObject contains a "properties" section, then set its properties with + // SetProperty block. + if (PROPERTIESARRAY.getJSONObject(i).has("properties")) + { + JSONArray keys = PROPERTIESARRAY.getJSONObject(i).getJSONObject("properties").names(); + + for (int k = 0; k < keys.length(); k++) { + SetProperty( + (Component)GetComponent(PROPERTIESARRAY.getJSONObject(i).getString("id")), + keys.getString(k), + PROPERTIESARRAY.getJSONObject(i).getJSONObject("properties").get(keys.getString(k)) + ); + } + } + } + SchemaCreated(); + + } catch (Exception e) { + throw new YailRuntimeError(e.getMessage(), "Error"); + } + } + + + /* + ----------------------- + ChangeId + + Changes ID of one of created components to a new one. + The old ID must be exist and new ID mustn't exist. + + + -- Parameters -- + String id : The old ID that will be changed. + String newId : The new ID that old ID will be changed to. + + ----------------------- */ @SimpleFunction(description = "Changes ID of one of created components to a new one. The old ID must be exist and new ID mustn't exist.") public void ChangeId(String id, String newId) { - if (COMPONENTS.containsKey(id) && !COMPONENTS.containsKey(newId)) - { + if (COMPONENTS.containsKey(id) && !COMPONENTS.containsKey(newId)) { Component component = COMPONENTS.remove(id); COMPONENTS.put(newId, component); - } - else - { - throw new YailRuntimeError("Old ID must exist and new ID mustn't exist.","Error"); + } else { + throw new YailRuntimeError("Old ID must exist and new ID mustn't exist.", "Error"); } } - /* - Removes the component with specified ID from screen/layout and the component list. So you will able to use its ID again as it will be deleted. + + /* + ----------------------- + Remove + + Removes the component with specified ID from screen/layout and the component list. + So you will able to use its ID again as it will be deleted. + + + -- Parameters -- + String id : The old ID that will be changed. + String newId : The new ID that old ID will be changed to. + + ----------------------- */ - @SimpleFunction(description = "Removes the component with specified ID from screen/layout and the component list. So you will able to use its ID again as it will be deleted.") + @SimpleFunction(description = "Removes the component with specified ID from screen/layout and the component list.\n" + + "So you will able to use its ID again as it will be deleted.") public void Remove(String id) { // Don't do anything if id is not in the components list. - if (COMPONENTS.containsKey(id)) - { + if (COMPONENTS.containsKey(id)) { // Get the component. Object cmp = COMPONENTS.get(id); - try { - Method m = cmp.getClass().getMethod("Visible", boolean.class); - m.invoke(cmp, false); - } catch (NoSuchMethodException e) { - e.printStackTrace(); + if (cmp != null) { + Method method = cmp.getClass().getMethod("Visible", boolean.class); + method.invoke(cmp, false); + } } catch (Exception e) { e.printStackTrace(); } - // Remove its id from components list. COMPONENTS.remove(id); } } - /* + + /* + ----------------------- + LastUsedID + Returns last used ID by Create block. + + ----------------------- */ @SimpleFunction(description = "Returns last used ID by Create block.") public String LastUsedID() { return LAST_ID; } - /* + + /* + ----------------------- + UsedIDs + Returns all used IDs in the created components list. + + ----------------------- */ @SimpleFunction(description = "Returns all used IDs in the created components list.") public YailList UsedIDs() { @@ -176,120 +357,192 @@ public YailList UsedIDs() { return YailList.makeList(keys); } - /* - Returns the component's itself for setting properties. ID must be a valid ID which is added with Create block. + + /* + ----------------------- + GetComponent + + Returns the component's itself for setting properties. + ID must be a valid ID which is added with Create block. + ID --> Component + + + -- Parameters -- + String id : The ID of the component. + + ----------------------- */ @SimpleFunction(description = "Returns the component's itself for setting properties. ID must be a valid ID which is added with Create block.") public Object GetComponent(String id) { return COMPONENTS.get(id); } - /* - Returns the ID of component. Component needs to be created by Create block. Otherwise it will return blank string. + + /* + ----------------------- + GetID + + Returns the ID of component. Component needs to be created by Create block. + Otherwise it will return blank string. Also known as reverse of the GetComponent block. + Component --> ID + + + -- Parameters -- + Component component : The component that has an ID. + + ----------------------- */ - @SimpleFunction(description = "Returns the ID of component. Component needs to be created by Create block. Otherwise it will return blank string.") + @SimpleFunction(description = "Returns the ID of component. Component needs to be created by Create block.\n" + + "Otherwise it will return blank string.") public String GetId(Component component) { return getKeyFromValue(COMPONENTS, component); } - /* - Returns the component's name. + + /* + ----------------------- + GetName + + Returns the internal name of any component or object. + + + -- Parameters -- + Component component : The component that its name will be returned. + + ----------------------- */ - @SimpleFunction(description = "Returns the component's name.") - public String GetName(Component component) { + @SimpleFunction(description = "Returns the internal name of any component or object.") + public String GetName(Object component) { return component.getClass().getName().replace(BASE_PACKAGE + ".", ""); } - /* - Set a property of a component by typing its name. + + /* + ----------------------- + SetProperty + + Set a property of a component by typing its property name. It behaves like a Setter property block. + It can be also used to set properties that only exists in Designer. + Supported values are; "string", "boolean", "integer" and "float". For other values, you should use + Any Component blocks. + + + -- Parameters -- + Component component : The component that will be modified. + String name : Name of the property. + String value : Value of the property. + + ----------------------- */ - @SimpleFunction(description = "Set a property of a component by typing its property name.") + @SimpleFunction(description = "Set a property of a component by typing its property name. It behaves like a Setter property block.\n" + + "It can be also used to set properties that only exists in Designer. Supported values are;\n" + + "'string', 'boolean', 'integer' and 'float'. For other values, you should use Any Component blocks.") public void SetProperty(Component component, String name, Object value) { // The method will be invoked. - Method m = null; - try - { - m = FindMethod(component.getClass().getMethods(), name, 1); + try { + if (component == null) + throw new YailRuntimeError("Component is not specified.", "Error"); + + Method method = findMethod(component.getClass().getMethods(), name, 1); // Method m = component.getClass().getMethod(name, value.getClass()); - if (m == null) + if (method == null) throw new YailRuntimeError("Property can't found with that name.", "Error"); - - String outputName = m.getParameterTypes()[0].getName().toString().trim(); + + String outputName = method.getParameterTypes()[0].getName().toString().trim(); String inputName = value.getClass().getName().toString().trim(); String v = ""; // Parse the value and save it in a variable. - if (inputName.equals("gnu.math.IntNum")) - { - v = Integer.toString(((gnu.math.IntNum)value).intValue()); - } - else if (inputName.equals("gnu.math.DFloNum")) - { - v = Double.toString(((gnu.math.DFloNum)value).doubleValue()); - } - else { + if ("gnu.math.IntNum".equals(inputName)) { + v = Integer.toString(((gnu.math.IntNum) value).intValue()); + } else if ("gnu.math.DFloNum".equals(inputName)) { + v = Double.toString(((gnu.math.DFloNum) value).doubleValue()); + } else { v = value.toString(); } // Check for requested parameter type. - if (outputName.equals("int")) { - m.invoke(component, Integer.parseInt(v)); - } - else if (outputName.equals("double")) { - m.invoke(component, Double.parseDouble(v)); - } - else if (outputName.equals("float")) { - m.invoke(component, Float.parseFloat(v)); - } - else { - m.invoke(component, Class.forName(value.getClass().getName()).cast(value)); + switch (outputName) { + case "int": + method.invoke(component, Integer.parseInt(v)); + break; + case "double": + method.invoke(component, Double.parseDouble(v)); + break; + case "float": + method.invoke(component, Float.parseFloat(v)); + break; + default: + method.invoke(component, Class.forName(value.getClass().getName()).cast(value)); + break; } - } - catch (InvocationTargetException | IllegalAccessException | ClassNotFoundException eh) - { - throw new YailRuntimeError(eh.getMessage().toString(),"Error"); - } - catch (IllegalArgumentException eh) { - throw new YailRuntimeError("Looks like parameters are invalid. If you think everything is correct, please report.", "Error"); - } - catch (Exception e) - { - throw new YailRuntimeError(e.getClass().getName() + ": " + e.getMessage().toString(), "Error"); + } catch (Exception exception) { + throw new YailRuntimeError(exception.getMessage(), "Error"); } } - /* - Get property value of a component. + + /* + ----------------------- + GetProperty + + Get a property value of a component by typing its property name. It behaves like a Getter property block. + It can be also used to get properties that only exists in Designer. + + + -- Parameters -- + Component component : The component that property value will get from. + String name : Name of the property. + + ----------------------- */ - @SimpleFunction(description = "Get property value of a component.") + @SimpleFunction(description = "Get a property value of a component by typing its property name. It behaves like a Getter property block.\n" + + "It can be also used to get properties that only exists in Designer.") public Object GetProperty(Component component, String name) { // The method will be invoked. - Method m = null; - try - { - m = FindMethod(component.getClass().getMethods(), name, 0); + try { + if (component == null) + throw new YailRuntimeError("Component is not specified.", "Error"); + + Method method = findMethod(component.getClass().getMethods(), name, 0); + + if (method == null) + throw new YailRuntimeError("Property can't found with that name.", "Error"); // Invoke the saved method and return its return value. - return m.invoke(component); - } - catch (Exception eh) - { - // Throw an error when something goes wrong. - throw new YailRuntimeError(eh.getMessage().toString(),"Error"); + return method.invoke(component); + } catch (Exception exception) { + // Throw an error when something goes wrong. + throw new YailRuntimeError("" + exception, "Error"); } } - @SimpleFunction(description = "Get all available properties of a component which can be set from Designer as list along with types. Can be used to learn the properties of any component which is not static.") + + /* + ----------------------- + GetDesignerProperties + + Get all available properties of a component which can be set from Designer as list along with types. + Can be used to learn the properties of any component which is not static. + Property values and names are joined with --- separator. + + + -- Parameters -- + Component component : The component that property values will be fetched. + + ----------------------- + */ + @SimpleFunction(description = "Get all available properties of a component which can be set from Designer as list along with types.\n" + + "Can be used to learn the properties of any component which is not static.\n" + + "Property values and names are joined with --- separator.") public YailList GetDesignerProperties(Component component) { // A list which includes designer properties. - List properties = new ArrayList(); + List properties = new ArrayList<>(); // Get the component's class and return all methods from it. Method[] methods = component.getClass().getMethods(); - for (Method mtd : methods) - { + for (Method mtd : methods) { // Read for @DesignerProperty annotations. // So we can learn which method is used as property setter/getter. - if ((mtd.getDeclaredAnnotations().length == 2) && (mtd.isAnnotationPresent(DesignerProperty.class))) - { + if ((mtd.getDeclaredAnnotations().length == 2) && (mtd.isAnnotationPresent(DesignerProperty.class))) { // Get the DesignerProperty annotation. DesignerProperty n = mtd.getAnnotation(DesignerProperty.class); // Add editorType value and method name to the list. @@ -300,47 +553,71 @@ public YailList GetDesignerProperties(Component component) { return YailList.makeList(properties); } + + /* + ----------------------- + Version + + Returns the version of the extension. + + ----------------------- + */ + @SimpleProperty(description = "Returns the extension version.") + public int Version() { + return DynamicComponents.class.getAnnotation(DesignerComponent.class).version(); + } + + + + // ------------------------ + // PRIVATE METHODS + // ------------------------ + + // Get all available methods from a component. + /* @SimpleFunction(description = "Get all available methods from a component.") private YailList GetMethods(Component component) { // A list which includes designer properties. - List names = new ArrayList(); - for (Method mtd : component.getClass().getMethods()) - { - names.add(mtd.getName()); + List names = new ArrayList<>(); + for (Method method : component.getClass().getMethods()) { + names.add(method.getName()); } // Return the list. return YailList.makeList(names); } + */ - - - // ------------------------ - // PRIVATE METHODS - // ------------------------ - - // Getting key from value, found on: - // http://www.java2s.com/Code/Java/Collections-Data-Structure/GetakeyfromvaluewithanHashMap.htm + // Getting key from value, source: http://www.java2s.com/Code/Java/Collections-Data-Structure/GetakeyfromvaluewithanHashMap.htm public String getKeyFromValue(Hashtable hm, Object value) { - for (String o : hm.keySet()) { - if (hm.get(o).equals(value)) { - return (String)o; + for (String o : hm.keySet()) { + if (hm.get(o).equals(value)) { + return (String) o; + } } - } return ""; } - private Method FindMethod(Method[] methods, String name, Integer paramCount) { - Method m = null; - for (Method mtd : methods) - { - // Check for one parametered (setter) method. - if((mtd.getName() == name.trim()) && (mtd.getParameterCount() == paramCount)) - { - m = mtd; - break; - } + // Finds a method in method list by checking the name and parameter count. + private Method findMethod(Method[] methods, String name, Integer paramCount) { + for (Method method : methods) { + // Check for one parametered (setter) method. + if ((method.getName().equals(name.trim())) && (method.getParameterTypes().length == paramCount)) { + return method; } - return m; + } + return null; + } + + private void Parse(JSONObject js, String id) throws JSONException { + JSONObject data = new JSONObject(js.toString()); + data.remove("components"); + if (!"".equals(id)) + data.put("in", id); + PROPERTIESARRAY.put(data); + if (js.has("components")) { + for (int i = 0; i < js.getJSONArray("components").length(); i++) { + Parse(js.getJSONArray("components").getJSONObject(i), data.optString("id", "")); + } + } } - }