FANDOM



Alright! This is the long awaited, anticipated, tutorial. Written by none other than me. :D This tutorial is an intro into the basics of making Widgets that can "spawn" or make copies of themselves. There are two different ("major") ways to accomplish this, but I will only focus on the method that doesn't involve making copies of the Widget's main file. This "intro", being written by me, will show my point of view and possibly some of my opinions about programming (or how I program), and in general. This is to say that there is more than one way to do this, and the methods I will cover are just one way to do it. Feel free to make your own. That statement also means that if I offend anyone, I'm sorry. I apologize in advance.


Overview Edit

Widgets that "spawn" is nothing new. Harry Whitfield created the Widget "Ancestor Spawning"* which created new copies of the Widget by copying the base file and then running it. This of course required "frightfully" complex code (no offense) to manage these new copies, which "scared" me off from trying the concept the first time I saw it. The one advantage it had/has over my technique was the concept of "parents and siblings". Each new "spawn" or copy could make x many copies of itself and they would all be children of that "parent". This type of Widget spawning no longer became cross-platform viable when Y!WE for Windows started to consolidate Widget registry keys. My method does not require multiple copies of the same Widget, only one. However, internally, there must be an object that can easily have copies made of itself. However, the purpose of this is not to debate methods, so I'll move on.

Some things that a developer must consider are these: "Does my Widget really need a 'spawning' like capability?", "Is this feature more useful than annoying?", and "If so, have I implemented enough/the right features to make this useful?". I can help with the last one, but it is up to the developer to decide if their Widget/idea passes the first two and merits this "feature". All features can be dubious in value. If done well, they can be helpful. However, if they are done wrong, overly-done, or not even related, then can be a hindrance. I'll move on again, because I don't want to bore you with too many "moral lessons".

For those who are impatient, I have "unpacked" and heavily commented the source code of my best example, CanvasGauges (rehash) and put it up as a flat-file Widget at savefile.com. However, I would suggest waiting until the end to look at it as I do a few things differently (than when I programmed the Widget) and cover a few topics that may not present themselves in the source.


* Which is sadly no longer in the gallery, for the reason mentioned above I suppose.

Outline Edit

Here are the categories that I plan to cover (also to give me a defined start and end ...) In the first half, I will do a quick intro to "objects", cover some of their useful functions, and go over some simple "object spawning". The second half will cover dynamically controlling the "spawning", object birth and death, and individual preferences. Last I will go over the very important topic of loading a Widgets "state".

All of these sections will build on the previous section, so I don't suggest skipping around if this is your first time reading it.

Before we get started, I will be doing the tutorial along with you, and by the end will have a generated a simple sample file. I would suggest that you do the same. For simplicity, all the following programming will be done in the main .kon file, in the onLoad tag, and inside CDATA tags.


Object Related Edit

Intro to Objects Edit

Okay, what is an object? To advanced programmers/developers this is a dumb question. Nevertheless, an object is something concrete, a "something" that you can "pick up" and "play with". In terms of Y!WE, images are objects, they also have properties. You can change their source, or colorize them. Timers, windows and frames are also objects. I would like to focus on objects that are not built in, but are created dynamically by the developer.

A simple example would be like this:

function MyObject() {

	this.color = "red";
	this.size = "10";
}


To create a new instance (independent copy, simply put) of this object, one simply needs to do the following:

var a = new MyObject();

All it's properties are public (available to everyone outside that object), so to print them to the console (debug window):

print(a.color);
print(a.size);

Later I'll suggest methods to control access to these variables/properties.


Useful Object Functions Edit

So far, I've shown you a simple object, and how to get at it's properties. Objects can do much more, however. They can have functions to allow you to interact with them and also to limit access to "internal" variables and properties. Some useful function include:

  • .equals(object) [complex objects cannot be compared with ==(=) or < and >]
  • .compareTo(object) [useful for sorting]
  • .toString()
  • .serialize();
  • Various "getters", "setters" and other functions to interact with the object.

I won't focus on the first two, but if you want to know more, look them up, or learn Java.

Ignoring my list order, I'll start with "Various ...." When creating objects, just like any other function, you can have parameters:

function MyObject(clr, sz) {

	this.color = "red";
	this.size = "10";
}

However, unlike other programming languages, JavaScript only gives you an error if you try to pass parameters to a function without any. So you have to remember to check for that possibility.

You can use those passed in parameters to set "internal values" like so:

function MyObject(clr, sz) {

	this.color = clr;
	this.size = sz;
}

var a = new MyObject("red", "10");

print(a.color);
print(a.size);


You may have noticed that we are still accessing those values from the outside. In programming, that is not such a good idea. Most people ask "why?", and here is the (short) answer: If you code depends on one of those variables not to be (or expect it to be) a certain value, and something from the outside changes it, your code could break and go BOOM! So the best option is to limit access to those values. If they are accessible from anywhere, that is impossible, and you can only hope developers respect your "controlled access".

In other programming languages, a way to control access would be to make them "private", or only available to the object itself. JavaScript really doesn't have way to make things private, but it does have scoping (making variables local to where they were defined). Normally variables scoped inside a function "die" as soon as the function ends. With objects, there is always a reference keeping them alive, so the variables are available to the object only for the lifetime of the object. (until it is deleted)

Once the variables have been made "private", there needs to be way to get at the ones you want to modify (controlled access). This is where functions come in. You can define functions to get, or set the internal variable. Once the passed in value is inside the function, the object can reject, modify, or do what ever it wants with it, thus controlling what actually happens.

To make them private and yet still accessible we would do the following:

function MyObject(clr, sz) {

	var color = clr;
	var size = sz;

	this.getColor = function() {
		return (color);
	};

	this.setColor = function(value) {

		// I do not like black (but only for this example)
		if(value === "black") {

			// Make it my favorite color.
			value = "green"
		}

		color = value;
	};
}

var a = new MyObject("red", "10");

print(a.getColor());	// Print the initial value
a.setColor("black");	// Change the value
print(a.getColor());	// Print the changed value

Note that the color variable is only available through my functions, and I can control what and how things are set. If you look again, you'll notice there is no way to access the size variable anymore. I cut off access to it. Also, I decided that the objects color should never be "black". Watch out for inconsistencies! Even though my function prevents the color black, you can still set it as an initial parameter, without it being checked. Except in rare cases, you want to preform the same checks on values passed in when creating the object as you do when the same values are passed in by setter functions.

With that out of the way, we can move on to two functions I consider equally important: toString and serialize. I'll focus on the former first. toString is a method that, like the name implies, returns a string. This isn't an ordinary string, but a "human readable" representation of the object, suitable for printing to the debug window. This function is automatically called by JavaScript when print() is called, making it easy to use and easily the most handy function you can have. The built in objects don't have this method, sadly, but it can be added. However, that is a discussion for another time and place.

This function is certainly not hard to add:

function MyObject(clr, sz) {

	var color = clr;
	var size = sz;

	this.getColor = function() {
		return (color);
	};

	this.setColor = function(value) {

		// I do not like black (but only for this example)
		if(value === "black") {

			// Make it my favorite color.
			value = "green"
		}

		color = value;
	};

	this.toString = function() {
		return ("MyObject( color: " + color + ", size: " + size + ")");
	};
}

var a = new MyObject("red", "10");

a.setColor("black");	// Change the value
print(a.toString());	// Print the object.

The toString function can be formated and "displayed" any way you like. In this particular example, there are only two variables to worry about. However, in larger objects and since the debug window is usually small, you may want to pick and choose what to display.

The serialize method is my favorite method (as opposed to most useful ^ ). This method will be designed to return all the properties of the object as a delimited string suitable for easily reconstructing the object. I generally like to return a string that can be easily parsed.* It is generally not a good idea to return code, or something that is easily eval()able with this function. I will explain later how we will actually use what this function returns.

* Other people may wish to return something such as JSON, but I will just focus on what I know.

For a simple example, I will add the serialize method and print what it returns.

function MyObject(clr, sz) {

	var color = clr;
	var size = sz;

	this.getColor = function() {
		return (color);
	};

	this.setColor = function(value) {

		// I do not like black (but only for this example)
		if(value === "black") {

			// Make it my favorite color.
			value = "green"
		}

		color = value;
	};

	this.toString = function() {
		return ("MyObject(color: " + color + ", size: " + size + ")");
	};

	this.serialize = function() {
		return ("['" + color + "'|'" + size + "']");
	};
}

var a = new MyObject("red", "10");

print(a.serialize());		// Print the "serialized" object.

The first print statement shows you what the method returns. It should be something similar to a simple array. For small objects like this one, where all the properties can be passed as arguments when creating the object, serialize is an easy function to do. It even scales to larger objects with more properties. Be careful about which character you use as a delimiter* for the data, as it must not be a character which could appear in the data. Unexpected delimiters could mess up what we do with this data later. If needed, use multiple characters to ensure a unique delimiter.

* This example uses a pipe |

Object Spawning Edit

So far, all we've done is make one copy of this object. You may have got a bit ahead of me and tried making multiple copies. That's okay for now. This isn't really the "spawning" that this tutorial is really talking about but it is close. The spawning I'm talking about is visual, but technically anything where you create a new instance could be called that. I'll assume that you still have the objects defined, so as to save on space/length.

Creating multiple copies is easy, you just need different variables to save them in:

var a = new MyObject("red", "10");
var b = new MyObject("green", "20");
var c = new MyObject("blue", "5");

Alternatively, if you are planing on making a lot of them, and don't care if you know where they are defined, you can take advantage of JavaScript's self-expanding array feature:

var list = [];
list[list.length] = new MyObject("red", "10");
list[list.length] = new MyObject("green", "20");
list[list.length] = new MyObject("blue", "5");

Okay, now on to the good stuff. :) Lets make our objects capable of making themselves visible! I'm going to start simple, and not add anything more than the properties we already have. However, to make the object visible, we need to add some visible components. Such as a Canvas. (for simplicity)

I'll use only the following, again, to keep this simple:

	var window = new Window();
	var canvas = new Canvas();
	window.appendChild(canvas);

	var ctx = canvas.getContext("2d");

This creates a window that will expand to the size of the Canvas when its size is set later.

Next the object will need to important methods: clear and draw. These are important because all objects that have "changeable elements" such as color or size, need to know how to "draw" themselves or change/fix the visual layout to match the new settings or variables. In the case of Canvas with our color and size variables, changing the variables does not update the user interface (UI). So, the need for a "draw" function.

The functions for this object are pretty straight forward and simple:

	this.clear = function() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
	};

	this.draw = function() {

		// Set the Canvas size
		canvas.width  = size;
		canvas.height = size;

		// Set the Canvas's color
		ctx.strokeStyle = color;
		ctx.fillStyle = color;


		// Begin a new, fresh path, to prevent odd visual problems.
		ctx.beginPath();

		ctx.rect(0, 0, canvas.width, canvas.height);

		// "draw" the object
		ctx.globalAlpha = .4;
		ctx.fill();
		ctx.globalAlpha = 1;
		ctx.stroke();

		ctx.closePath();
	};

The reason I offer two separate functions instead of one, is that with the two operations, it is possible to have an "intermediate" state where nothing has been draw or is visible, even if I never use that state.

Including all the "necessary" functions we have so far, you object should look approximately like this:

function MyObject(clr, sz) {

	var window = new Window();
	var canvas = new Canvas();
	window.appendChild(canvas);

	var ctx = canvas.getContext("2d");

	var color = clr;
	var size = sz;

	this.clear = function() {
	};

	this.draw = function() {
	};

	this.toString = function() {
	};

	this.serialize = function() {
	};
}

I omitted the source because I typed it above, and no sense wasting space to show it twice.

Now, with this "visible object" you can make multiple copies using the array method from before and make them visible:

var list = [];
list[list.length] = new MyObject("red", "200");
list[list.length] = new MyObject("green", "100");
list[list.length] = new MyObject("blue", "50");

for(var i = 0; i < list.length; i++) {
	list[i].draw();
}

You can drag each square independently, as every one has it's own window. Now, while Canvas does know what to do with values such as "red", or "green", it may be better to specify colors in hexadecimal format such as "#FF0000" for red. However for now, I won't worry about that.

Okay, we've now successfully ended out tour of objects, object properties, and their associated functions. Creating the objects in the code was nice, but I think that kinda defeats the purpose. If this is a tutorial about "spawning", creating them in the code is a bit limiting. We want the user to be able to control what and how many they see, right? Well, if you're still hanging in there, that's covered in the next section.


Globally Related Edit

Dynamic Controls Edit

Dynamic controls are what the user interacts with to control the "spawn". This could be Widget preferences, forms, context menus, or anything else you can think of. Some usual controls include creating new spawns, destroying unwanted ones, and individual preferences (as opposed to global Widget ones). With more complex, specialized objects, there may be different controls. However, for the purpose of this, I'll just cover the basic three I mentioned.

Life Edit

Making new objects is easy. I proved that above with code. To be dynamic though, you have to let the user be able to create more. This requires a little bit more work, but not too much.

First, we need a simple function that does one thing: Creates new objects when called. However, now that the objects aren't hard coded, you need some "defaults". These are the attributes that objects have when they are first created. This isn't hard, you just have to pick values that make good settings for most users. Later when we learn how to do individual preferences, you may change these values.

list is the variable that I will use from now on to store all out generated objects. I probably won't bother to define it from now on. [just a heads up]:

var list = [];

function createNewObject() {

	list[list.length] = new MyObject("green", "100");
}


Second, we need a function to create a return context menus to us. This context menu will allow users to create new "spawns". These will get added to the windows of the objects. This function will keep getting expanded as we add more controls, so it is fairly important.

function createContextMenu() {

	var menu = [];
	menu[0] = new MenuItem();
	menu[0].title = "Build new Object";
	menu[0].onSelect = function() {
		createNewObject();
	}

	return (menu);
}


As I mentioned above, our Object needs to be modified to accommodate the new context menus:

	var window = new Window();
	window.contextMenuItems = createContextMenu();


Your object should look about like this. I've omitted the real code to save on post space.

function MyObject(clr, sz) {

	var window = new Window();
	window.contextMenuItems = createContextMenu();

	var canvas = new Canvas();
	window.appendChild(canvas);

	var ctx = canvas.getContext("2d");

	var color = clr;
	var size = sz;

	this.clear = function() {
	};

	this.draw = function() {
	};

	this.toString = function() {
	};

	this.serialize = function() {
	};

	this.draw();
}

If you noticed that I slipped in another line of code, you're right. The reason is, that it is stupid to have to call .draw() on every object you create. If you are storing them in a large list like we are, you may not always know the index. Technically you could add a line to createNewObject(), but I prefer to put it here. It's really up to you.

If you ran this right now, you may or may not see anything. If you left in your statements to create a few new objects, then you might. It is always a good idea to create at least one object the first time the Widget is run. This is because the context menu to create more is only available when you have at least one object. Depending on the purpose/task of the Widget you may create more by default, but I'll stick with one.

Since the .kon file is fairly sized at this point, I'll post it a codebox. If there is something you don't have, maybe you missed something.

function MyObject(clr, sz) {

	var window = new Window();
	window.contextMenuItems = createContextMenu();

	var canvas = new Canvas();
	window.appendChild(canvas);

	var ctx = canvas.getContext("2d");

	var color = clr;
	var size = sz;

	this.clear = function() {
		ctx.clearRect(0, 0, canvas.width, canvas.height);
	};

	this.draw = function() {

		// Set the Canvas size
		canvas.width  = size;
		canvas.height = size;

		// Set the Canvas's color
		ctx.strokeStyle = color;
		ctx.fillStyle = color;


		// Begin a new, fresh path, to prevent odd visual problems.
		ctx.beginPath();

		ctx.rect(0, 0, canvas.width, canvas.height);

		// "draw" the object
		ctx.globalAlpha = .4;
		ctx.fill();
		ctx.globalAlpha = 1;
		ctx.stroke();

		ctx.closePath();
	};

	this.toString = function() {
		return ("MyObject(color: " + color + ", size: " + size + ")");
	};

	this.serialize = function() {
		return ("['" + color + "'|'" + size + "']");
	};

	this.draw();
}

function createNewObject() {

	list[list.length] = new MyObject("green", "100");
}

function createContextMenu(obj) {

	var menu = [];
	menu[0] = new MenuItem();
	menu[0].title = "Build new Object";
	menu[0].onSelect = function() {
		createNewObject();
	};

	return (menu);
}

var list = [];
list[list.length] = new MyObject("red", "200");

If you ran the program, the newly created squares all appeared on top of each other if you didn't move them. That's why I made the squares slightly transparent, so that is was possible to tell. We can fix this later, but just leave it for now.

Death Edit

There are many ways for an object to die: deleting the visible objects, removing all references to it (making it a garbage collection candidate) ... In the end however, I like the most elegant solution. So, we're going to add a property and two functions to our object, specifically related to "deleting" this object. Now, these functions will not explicitly delete this object. The will only "mark" it for deletion and tell us the object's status. This is the opposite of actually deleting, say, the window (which should instantly disappear).

At the top, we'll have to add a new variable to store this state:

	var isMarkedforDeletion = false;
	var color = clr;
	var size = sz;

I like booleans, so I default it to false, as we would like the object to stick around. However, be careful, as the next method used to set and get this value preform no checks or type conversions. So you have to make sure that you use the same values for true and false throughout the whole program.

Later inside the object you will need to more functions, to get and set this object state:

	// Get whether this object is slated for deletion.
	this.isDeleted = function() {

		return (isMarkedforDeletion);
	};

	// Set whether this objectis slated for deletion.
	this.setDeleted = function(value) {

		isMarkedforDeletion = value;
	};

As I said above, these functions do not actually delete any parts of the object. They merely mark it for deletion. Not only is this more elegant, but there is very good reason for it, which I will cover later. The actual process of removal is best handled by a function that I will cover next.

In taking a break from "death", I think it is time to cover saving the preferences of objects to a file. We won't worry about reading them back in for right now, just saving them. This cannot be done by each object alone for a good reason. If each object saves itself, you may expect it to load itself. However, every time the Widget is closed, these objects are destroyed and cannot load themselves when you restart. (There is a way, but it is not worth pursuing at the moment, so I won't mention it.)

This function will look something like this:

function saveAllObjects() {

	// Remove the file first, to start over with a fresh save.
	if(filesystem.itemExists(system.widgetDataFolder + "/objectFile.txt")) {
		filesystem.remove(system.widgetDataFolder + "/objectFile.txt");
	}

	// Write a header to the file. This makes it easier later to determine
	// the version this file was made with, or if this is a valid file at all.
	filesystem.writeFile(system.widgetDataFolder + "/objectFile.txt",
		"/* Test Widget :: " + widget.version + " */\n");

	// This is a temporary list into which we store all the "non-deleted"
	// objects from the main list. See below.
	var temp = [];

	// Now to actually save all the objects.
	for(var i = 0; i < list.length; i++) {

		// Do not keep objects that have been "marked for deletion".
		if(!list[i].isDeleted()) {

			// APPEND all objects to the file.
			filesystem.writeFile(system.widgetDataFolder + "/objectFile.txt",
				list[i].serialize() + "\n", true);

			// Keep all non-deleted objects.
			temp[temp.length] = list[i];
		}
	}

	// start the list over with only the good objects. This is also the part that truly
	// removes the "deleted" objects. No more "marked for deletion", they are truly gone now.
	list.length = 0;
	list = temp;
}

This function should be called when part of the Widget changes an object, be it internally or for the individual preferences (later). This ensures that the file is always up to date. I have found that while trying to save the file in the "onUnload", does not always work and because of that cannot be counted on. In truth, I have only got it to work once: MicroColors. I have not been able to get it work right since. So I came up with this better, safer idea.

However, it is still a good idea to hook into the shutdown listener to have a last chance to save the preferences. Just don't actually put the code to save all the objects there. It won't work, but referencing a function defined elsewhere will. (Don't ask why it works this way, it just does.) Unlike everything else up to now and after this point, this tag goes at the Widget level, not inside the onLoad tag.

// Save all the data on exit.
<action trigger="onUnload">
	<![CDATA[
		saveAllObjects();
	]]>
</action>

Now that our object can "be deleted" we have to add a context menu item to allow users to delete them. This requires a little thinking though. What is the one thing we don't want to do? Let the user delete the last object of course. Remember from above that we have to have at least one object to be able to access the context menu controls? Same applies here.

In order to do this, we will have to modify our context menu returning function, and similarly our calling line inside our object, to take an object as a parameter. This is because each object doesn't know it's index in our really big container array. This can be changed as follows:

window.contextMenuItems = createContextMenu(this);

function createContextMenu(obj) {

	var menu = [];
	menu[0] = new MenuItem();
	menu[0].title = "Build new Object";
	menu[0].onSelect = function() {
		createNewObject();
	};
	menu[1] = new MenuItem();
	menu[1].title = "Delete Object";
	menu[1].onSelect = function() {

		if(list.length > 1) {

			obj.setDeleted(true);
			saveAllObjects();
		} else {
			alert("You can't delete the last Object.");
		}
	};

	return (menu);
}

Passing the object itself as a parameter allows us to set it deleted without know it's index in our array.

Individual preferences Edit

Alright! The user can now create and destroy any number of objects. Well, what's next? Preferences? Sure. Preferences involve a little more work than you'd think. Since the context menu is created external to the object to make it easier to change in th future, we need to call a method inside the object to launch the menu. Kind of like the context menu, I'm going to put the preferences in a disposable* object, and we will launch a new copy of the form-object when our object's launching function is called.

* Disposable because the way I design them does not make them reusable. With a little time, they could probably be made reusable, but it is easier (in the long run) just to build the menu over again.

First, lets create the object that creates and shows the preferences form:

function showObjectPreferences(col, sz) {

	var pColor = col;
	var pSize = sz;

	var menu = [];
	menu[0] = new FormField();
	menu[0].title = "Color:";
	menu[0].type = "color";
	menu[0].defaultValue = pColor;
	menu[1] = new FormField();
	menu[1].title = "Type:";
	menu[1].type = "popup";
	menu[1].option = ["Small", "Medium", "Large", "XLarge"];
	menu[1].optionValue = ["50", "100", "150", "200"];
	menu[1].defaultValue = pSize;


	// Get the answer from the user.
	var answer = form(menu,"SpawningWidget Tutorial");

	if(answer != null) {

		// Retrieve new preferences.
		pColor = answer[0];
		pSize = answer[1];
	}

	this.getColor = function() {
		return (pColor);
	};

	this.getSize = function() {
		return (pSize);
	};
}

When created, this will "block" execution of any other code until the dialog returns.

Before we update the context menu, our Object needs to have a method that the context menu can call to launch the menu:

function MyObject(clr, sz) {

	// All the previous functions and stuff.

	this.onMenu = function() {

		// Create and show the menu.
		var prefs = new showObjectPreferences(color, size);

		// Set the internal variables
		color = prefs.getColor();
		size = prefs.getSize();

		// Resave all the objects
		saveAllObjects();

		// update the visible components
		this.clear();
		this.draw();
	};


Now that we have a function to call, lets update the context menu. Please be careful, I added the new menu item first, because it makes more sense there. So just change the indexes of the menu items already there:

function createContextMenu(obj) {

	var menu = [];
	menu[0] = new MenuItem();
	menu[0].title = "Individual Preferences";
	menu[0].onSelect = function() {
		obj.onMenu();
	};
	menu[1] = new MenuItem();
	menu[1].title = "Build new Object";
	menu[1].onSelect = function() {
		createNewObject();
	};
	menu[2] = new MenuItem();
	menu[2].title = "Delete Object";
	menu[2].onSelect = function() {

		if(list.length > 1) {

			obj.setDeleted(true);
			saveAllObjects();
		} else {
			alert("You can't delete the last Object.");
		}
	};

	return (menu);
}

Go ahead play. Spawn, delete, and change squares. It's fun. In playing around with the squares you may have noticed a bug in Y!WE. When you click on any square (that is on top of another) and open its individual preferences, squares that were spawned before that one will now appear on top of it! Oops! Y!WE can't keep window ordering when it launches frames. If you play with that a bit, you will notice that launching the global Widget preference window does not have the same issue. This is on Windows XP (SP2+). Anyways ... moving on ...

Loading Widget State Edit

Users now have complete control. Except for one small annoyance. Restarting the Widget restarts the objects. If you spent all your time setting up these objects to make an image and had to restart it, your masterpiece would be destroyed. So, first off, lets fix our Object to accept window default coordinates. This will allow us to set the window right off the bat, as Y!WE only remembers the window position of the first dynamic window.

It should look as relatively as follows. For simplicity I'm only going to show the things that actually changed.

function MyObject(clr, sz, x, y) {

	var window = new Window();
	window.contextMenuItems = createContextMenu(this);
	window.hOffset = x;
	window.vOffset = y;

	// ... stuff ...

	this.serialize = function() {

		var x = window.hOffset;
		var y = window.vOffset;

		return ("['" + color + "'|'" + size + "'|'" + x + "'|'" + y + "']");
	};
}

function createNewObject() {

	list[list.length] = new MyObject("green", "100", screen.width/2 - 50, screen.height/2 - 50);
}

If you do not want the Objects to always appear in the middle, you can mess with random numbers. However I would suggest that you confine the "spawning" locations to a box in the middle, and not the whole screen.

Now that we've fixed the Object and the default to be able to load at specified a specified point (x, and y), we can move on to a very important point. If you have a spawning Widget, but can't "save state", or reload the way the Widget looked previously (the number of "spawned" objects, their preferences), the feature is worthless. Previously, I showed you ho I chose to save out the objects to a file. To come full circle, I'm going to go over how to read that sort of file back in. This is actually a rather large (not long, just large) process. So I'll lay it out and comment the code as I go along. This portion of "spawning" is always the hardest, and it takes even me several tries to get right.

This is the hardest part. I will even admit it. However hard does not equal an opportunity to get "cheap". I still employ a try catch and break apart read in data to form the object inside the code, rather than straight from the file. The main issue with this code is it isn't in a function (though I suppose it could be) and it isn't as portable as it could be. Each project you do will likely use about the same code for this, but it will always be different.

And it begins:

// This is the *really big list* that everything gets stored in.
var list = [];

// If the file exists, it is not the Widgets first time.
var first = !filesystem.itemExists(system.widgetDataFolder + "/objectFile.txt");

// If it is the first time loading, skip trying to load a non-existant file.
// If it is present, load all the contents and parse them.
if(!first) {

	// Store whether any objects are "broken".
	// Also read in the file and store it.
	var broke = false;
	var objects = filesystem.readFile(system.widgetDataFolder + "/objectFile.txt", true);

	// Check file header for correct version and information.
	if(objects[0] === "/* Test Widget :: " + widget.version + " */") {

		// Iterate through all "supposed objects"
		for(var i = 1; i < objects.length; i++) {

			// Skip comments.
			if(objects[i].indexOf("//") < 0) {

				// Check to see if it might contain a valid object.
				// This won't necessarily confirm anything,
				// but that's what the next part is for.
				if(objects[i].indexOf("[") === 0 && objects[i].indexOf("]") > 0) {

					// Safety
					try {

						// Split the string to get *only* the values we are looking for.
						// If this fails, then it wasn't really an object in the first place.
						var values = objects[i].substring(1, objects[i].length-1).split("|");
						list[list.length] = new MyObject(values[0], values[1], values[2], values[3]);
					} catch(e) {

						// Print error, and set up to notify user when done.
						print("ERROR: " + e);
						broke = true;
					}
				}
			}
		}

		// An Object or more failed in loading/creation. Tell the user.
		if(broke) {
			alert("Some Objects failed to load.\nThis may be due to invalid or corrupt files");
		}

		// No objects were loaded. Set it up to create a new one.
		if(list.length === 0) {
			first = true;
		}
	} else {
		// What do we do if the file header fails ...
		// You can do what ever you want here, this is just a simple fill in to give an alert.
		var answer = alert("The file header is corrupt or has been tampered.","Restart and Try Again","Close Widget");

		if(answer === 1) {
			reloadWidget();
		} else {
			closeWidget();
		}
	}
}

// If this is the first time run, or there are no objects, create a new one to start.
if(first) {
	createNewObject();
}

Bonus Edit

As you may have noticed on some of my Widgets, I also offer a "Delete All" option. This is fairly easy to implement, but I'll leave that up as an exercise for you. A few tips though: Offer the user a chance to think this choice over. (my usual implementations make this choice permanent) And, "Delete All" doesn't really mean delete all. It is more like "Start Over".


Review and Application Edit

Whew. That was a long tutorial. You don't need to tell me about it, because 90% of the way through, I realized my fan-pad for my Notebook PC was off when it overheated and shut off. (lost two paragraphs). Overall though, I really am glad that this is done. I hope that you actually learned something from it, experience programmers and beginners alike. In this tutorial I covered how to make an object with "protected" ("private") variables, how to create fully rounded objects with simple visual components that can be "spawned" easily, how to let the user control the spawned components and last but not least, how to save and reload those objects to preserve their state.

As for application, I suggest you look at the link at the top of the page, offered again here: CanvasGauges (rehash). I will mention that this Widget was written before this tutorial, and originally designed for a Widget 4K contest. That means that it makes use of a lot of simple "this." instead of private variables, and in the interest of saving space, uses the evil "eval" as a very bad shortcut. Please do not borrow the bad ideas from the example, as it they are potential security hole.

Again, I really thank you for your time, and I hope you managed to pull something useful out of this. I certainly had fun making this and would hope that you felt the same. So, until we meet again, have a great day!

Fin. :)

External Links Edit

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.