Using Layouts In Qooxdoo - Part 1
This is the first part of a tutorial series about layout managers, container objects and object hierarchies in Qooxdoo (qx for short from here on). Its target audience is mostly qx programmers coming from an (X)HTML/CSS/DOM background who have possibly used JavaScript libraries such as JQuery or prototype before. Qx is different, it is a JavaScript RIA Framework. If you have used a GUI toolkit such as Swing (Java), wxWidgets or GTK+ you should have no difficulty following this tutorial. In fact you will probably find it boring1. On the other hand if you are still trying to find out how you can attach your <div /> in an existing qx layout; you are in the right place.
The answer is “No”, to the question above. You don’t create DOM elements with qx (unless you are doing something really exotic). Actually you don’t need to concern yourself with the DOM at all. GUI is abstracted to a set of intuitive classes that you should be familiar from today’s desktop operating systems. This abstraction brings two big advantages:
- Developers using qx don’t need to concern themselves with cross-browser issues.
- Theming is easy and uniform.
Widgets Are The Building Blocks
A qx user interface is constructed using widgets. A widget is a class that encapsulates appereance, data and behaviour. For instance a TextField would render itself as a box and a caret, make the text you enter inside accessible programmatically and allow you to be notified whenever a the text data is changed. Widgets are customizable, subclassable and of course themable.
But a GUI hardly ever consists of a single widget. Instead there is a hierarchy of widgets. A container is a special kind of widget that has child widgets. A container widget ususally doesn’t have any visual parts itself and just render its children in its screen space. How child widgets are positioned within their container is decided by the container’s layout manager. Having a layout manager means the abstraction of layout strategy for maximum flexibility.
So how do containers, child widgets and layout managers work together? First of all qx positions everything with absolute coordinates (Yes; position:absolute) internally. But that doesn’t mean you need to position anything absolutely on the API level. Speaking in terms of JavaScript libraries; you can take advantage of floating/auto-sizing/liquid layouts. Position and size of all widgets are negotiated through this hierarchy. Theoretically everyone has a say. This means that you can set some constraints on parents and then some others on child widgets and they all work together. The resulting layout is then converted to absolute coordinates for rendering. (Examples in the next part)
Recap
- Qx GUI’s are hierarchically constructed from widgets.
- Each application has a root container.
- Each container has a layout manager and one or more child widgets2.
- A child of a container can be either:
- Another container.
- Or a control widget (such as a form widget).
NEXT PART: VBox Layout
1: You might want to see Qooxdoo - API documentation instead.
2: Although it is possible that a container may have no children, it is not sensible.
I Can’t Learn From My Fitness Instructor Because I’m Prejudiced
I had an interesting morning today. I dragged myself to the gym as usual[1] for my morning cardio. I said hello to this young instructor and that led us to how are you part. I said “I feel tired and broken”, which is only natural in the middle of a diet. Being young and enthusiastic he first asked me questions about my training and then, I guess when he decided he had enough information, he started giving me advice. Imagine my disappointment and amazement combo there! And the advice was basically take it easy. Yeah, sure.
I could have just told him to get lost. That is usually what I do, because these so-called instructors think of themselves as gurus and all you can be is a newbie to be guided and advised and… Yes, instructed :) . They are usually stupid people with a crappy education. But this was a nice person, so I didn’t gave him the usual. Instead I tried to explain him. I thought that if I explained things calmly and simply, he would at least understand part of it. But I was wrong.
I decided that it was enough when he told me “perhaps you have the prejudice that you know better than us (instructors)”. Prejudice? Prejudice! I asked a single question “Have you ever done bodybuilding?”. The answer was of course no. “Then how the hell can you tell me that you know about this stuff?”. Really, it is not something you can learn from books. For training other people, or even for your own training. So I don’t have a prejudice, you moron. I have seen you show people incline bench press on a 45 degrees bench. Forget bodybuilding, you simply don’t know the first thing about weight training.
Guru Happens In Three Months
I see this happening in gyms all the time. A newbie trains for three months, and then decides that he has mastered it all. He compassionately comes to you, the pathetic loser who obviously know nothing, to share his infinite wisdom. And you tell him to go bother someone else, directly and probably in the presence of others. Not a pleasant scene. I believe he acts with good intentions, but that doesn’t make it less insulting.
Well, fools are fools. I can never imagine to surmount the limitless power of foolishness. But there is a pattern, and I think it is worth drawing some attention to it. What happens is not significantly important. Neither your reaction. But it is important to realize and observe it.
I have worked as a cashier for a brief period of time. If you are working at a busy place it is not at all easy to get used to. But once you do, you start experiencing something (IMO) unique. Time slows down for you, relative to the person paying of course. While she is hurrying to get it over with and (hopefully) get it right, you do your part efforlessly. Simply because you have done that for too many times before. The customer has no way of seeing this, she is just too busy with finding her purse, counting the money or whatever. On the other hand it has become a reflex for you, so you can observe the person in front of you shutting down everything but the task at hand. My point here is; who has a better understanding of the subject inevitably can see things in a much larger field of view, but the other is oblivious to this fact.
What happened this morning made me think; I must be doing the same thing (being a fool) on some other subjects. Of course I am not aware of it. I am oblivious to my own ignorance. Isn’t that convenient?
What Is Missing In This Picture
Human eye has about 200 degrees viewing angle. It is a pretty wide angle. But still it is just a little more than half of panoramic view. Half of the truth. Actually it is much less than the half, considering a 3 dimensional space. Our vision is just a projection of our surroundings, very limited information in this sense. And how many of us now feel like visually inadequate?
Not likely. Because our brain compensates for the holes in our knowledge. It puts the pieces together and infers for what is missing. We think we know what is around us, but in reality we just make things. up.
This is not a necessarily a bad thing. We can work with limited information and create art. An artist might reach exactly the same result as she imagined it to be. But most of the time there is no instantiated idea that makes it to the final creation. Another example is martial arts. There is no way to stop and observe your surroundings and your opponent. And these change constantly with time. Unless of course you are already knocked out. Yet your mind can fill the gaps in this very limited 2 dimensional information to create a 4 dimensional model and makes predictions based on that. These activities (artistic creation, martial arts) have significantly different time scales. But a lot more than we are counciously aware of is happening in both. Autonomous nature of this guessing shouldn’t mean becoming aware of and having some control over it is worthless.
This counciousness can sometimes save you from being fooled (or being a fool). As crude example; people with high self-confidence speak loud and clear, right? Yes. And people with low self-confidence do that as well, they are possibly even louder. If you buy into this stereotype you will probably end up misjudging people. Or worse, you might speak too loud when you feel unconfident.
Actually relying on this mechanism too much might lead to a kind of lazyness. Adults are much less explorative than children. I don’t think this is because they have learned so much, or opinionated themselves after rigorous thinking. It is simply easier to be lazy and after a while it becomes a routine. Growing up, after a certain age, is replaced by getting old. Sure, we all get old. But we don’t have to stop growing up. And then we don’t have to reverse the process, to the point live life on auto-pilot.
A Meta-Solution
How do we protect against this exporative lazyness? I don’t even know what it is and how it works exactly. But I will try to apply two principles that works well with regular lazyness;
- Setting expectations the right way. Getting rid of habits and building skills are two different things. You are not trying to free yourself of the non-existance of the skill, and the habit is already working on you from t=0. You can not expect incremental gains. You will win some, and then lose some. Think about the process as many iterations instead, many being a strictly unknown number. Expect to get back to where you have started, hopefully with positive changes.
- Taking advantage of external forces. We are affected from both internal and external stimuli. But external stimuli has much power than internal. This is great of course, because our lazyness is internal. The tricky part is to find out that external thing we can use. We need to get out of our comfort zone, for two reasons. Outside our comfort zone is a world stranger to us, this should supply material to observe. Getting out of our comfort zone is naturally forcing ourselves out, this should supply enough irritation. Think of it as a slap in the face. “Hey! Wake up!”
You Are Too Naive Too Fool Me
I never claim to be an expert on bodybuilding or something like that. But when people around are really clueless, as in thinking sit-ups will give them a six pack, and I point it out, it appears to be I am making such a claim.
It is sad actually. I see people everyday, thinking they are working out. But it is not working out. Can you imagine a perfectly healthy 30 year old and a pyhsically inactive 50 year old following exactly the same program. Oh, sorry; not exactly the same. Younger one is additionally doing sit-upa. To get a six pack of course. Good luck. :D
[1] | I’m on a diet now, so I do my cardio in the mornings and my weight training in the evenings. |
Adding GIT support to Meld
Meld is a great diffing/merging tool with version control support. GIT support doesn’t come out of the box though. To enable GIT support you need to copy this file into your /usr/lib/meld/vc directory. Then you can open the directory where your GIT repository is checked out (using New -> Version Control Browser of course).
I would like to include the contents of the file here as well (git.py):
# -*- coding: utf-8 -*-
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
### Copyright (C) 2002-2005 Stephen Kennedy <stevek@gnome.org>
### Copyright (C) 2005 Aaron Bentley <aaron.bentley@utoronto.ca>
### Copyright (C) 2007 José Fonseca <j_r_fonseca@yahoo.co.uk>
### Redistribution and use in source and binary forms, with or without
### modification, are permitted provided that the following conditions
### are met:
###
### 1. Redistributions of source code must retain the above copyright
### notice, this list of conditions and the following disclaimer.
### 2. Redistributions in binary form must reproduce the above copyright
### notice, this list of conditions and the following disclaimer in the
### documentation and/or other materials provided with the distribution.
### THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
### IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
### OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
### IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
### INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
### NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
### DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
### THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
### (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
### THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import errno
import _vc
class Vc(_vc.Vc):
CMD = "git"
NAME = "Git"
PATCH_STRIP_NUM = 1
PATCH_INDEX_RE = "^diff --git a/(.*) b/.*$"
def __init__(self, location):
self._tree_cache = None
while location != "/":
if os.path.isdir( "%s/.git" % location):
self.root = location
return
location = os.path.dirname(location)
raise ValueError()
def commit_command(self, message):
return [self.CMD,"commit","-m",message]
def diff_command(self):
return [self.CMD,"diff","HEAD"]
def update_command(self):
return [self.CMD,"pull"]
def add_command(self, binary=0):
return [self.CMD,"add"]
def remove_command(self, force=0):
return [self.CMD,"rm"]
def revert_command(self):
return [self.CMD,"checkout"]
def get_working_directory(self, workdir):
if workdir.startswith("/"):
return self.root
else:
return ''
def cache_inventory(self, topdir):
self._tree_cache = self.lookup_tree()
def uncache_inventory(self):
self._tree_cache = None
def lookup_tree(self):
while 1:
try:
proc = os.popen("cd %s && git status --untracked-files" % \
self.root)
entries = proc.read().split("\\n")[:-1]
break
except OSError, e:
if e.errno != errno.EAGAIN:
raise
statemap = {
"unknown": _vc.STATE_NONE,
"new file": _vc.STATE_NEW,
"deleted": _vc.STATE_REMOVED,
"modified": _vc.STATE_MODIFIED,
"typechange": _vc.STATE_NORMAL,
"unmerged": _vc.STATE_CONFLICT }
tree_state = {}
for entry in entries:
if not entry.startswith("#\t"):
continue
try:
statekey, name = entry[2:].split(":", 2)
except ValueError:
# untracked
name = entry[2:]
path = os.path.join(self.root, name.strip())
tree_state[path] = _vc.STATE_NONE
else:
statekey = statekey.strip()
name = name.strip()
try:
src, dst = name.split(" -> ", 2)
except ValueError:
path = os.path.join(self.root, name.strip())
state = statemap.get(statekey, _vc.STATE_NONE)
tree_state[path] = state
else:
# copied, renamed
if statekey == "renamed":
tree_state[os.path.join(self.root, src)] = \
_vc.STATE_REMOVED
tree_state[os.path.join(self.root, dst)] = _vc.STATE_NEW
return tree_state
def get_tree(self):
if self._tree_cache is None:
return self.lookup_tree()
else:
return self._tree_cache
def lookup_files(self, dirs, files):
"files is array of (name, path). assume all files in same dir"
if len(files):
directory = os.path.dirname(files[0][1])
elif len(dirs):
directory = os.path.dirname(dirs[0][1])
else:
return [],[]
tree = self.get_tree()
retfiles = []
retdirs = []
for name,path in files:
state = tree.get(path, _vc.STATE_IGNORED)
retfiles.append( _vc.File(path, name, state) )
for name,path in dirs:
# git does not operate on dirs, just files
retdirs.append( _vc.Dir(path, name, _vc.STATE_NORMAL))
for path, state in tree.iteritems():
# removed files are not in the filesystem, so must be added here
if state is _vc.STATE_REMOVED:
if os.path.dirname(path) == directory:
retfiles.append( _vc.File(path, name, state) )
return retdirs, retfiles
def listdir(self, start):
# just like _vc.Vc.listdir, but ignores just .git
if start=="": start="."
if start[-1] != "/": start+="/"
cfiles = []
cdirs = []
try:
entries = os.listdir(start)
entries.sort()
except OSError:
entries = []
for f in [f for f in entries if f!=".git"]:
fname = start + f
lname = fname
if os.path.isdir(fname):
cdirs.append( (f, lname) )
else:
cfiles.append( (f, lname) )
dirs, files = self.lookup_files(cdirs, cfiles)
return dirs+files
Enjoy!
Dictionary Key Validation, Lists, Sets and Iterators
Recently I needed to check if a Python dictionary has a specific set of keys exactly or not. I made some tests to see which method works best. For the sake of simplicity, I’ve compared keys of two dictionaries instead of a dictionary’s keys against a sequence of keys.
All of the following test results are averages of 10 repetitions of the given code, in the form of:
sum(timeit.Timer(<test_code>).repeat(10))/10.0
First, I did some simple tests with small sizes:
>>> timeit.Timer('l1.sort();l2.sort();l1==l2', 'l1,l2=[1,2,3], [3,2,1]')
1.95
>>> timeit.Timer('set(l1)==set(l2)', 'l1,l2=[1,2,3], [3,2,1]')
2.85
The second one with set’s seems more intuitive to me. But it’s slower than the one with lists. Obviously you need to sort the lists before comparison. Because a dictionary’s keys are not ordered, and therefore its keys() would return a list with unpredictable order of items. On the other hand a set, even though it is unordered, would return the same hash for the same keys.
Now, what happens if we work on a larger number of items:
>>> timeit.Timer('l1.sort();l2.sort();l1==l2', 'l1,l2=range(100), \
... [100-i for i in range(100)]')
15.49
>>> timeit.Timer('set(l1)==set(l2)', 'l1,l2=range(100), \
... [100-i for i in range(100)]')
25.13
More or less the same results. Actually both of these tests are biased towards lists. We initialize our test data as lists. So while the first tests only consist of in-place sorting and comparison, the second ones involve creation of new set objects and comparison.
Let us see what happens when we start with actual dictionaries and do the same comparisons:
>>> timeit.Timer('l1,l2=d1.keys(),d2.keys();l1.sort();l2.sort();l1==l2', \
... 'd1,d2=dict([(str(i), i) for i in range(3)]),dict([(str(3-i), 3-i) \
... for i in range(3)])')
2.49
>>> timeit.Timer('set(d1.keys())==set(d2.keys())', 'd1,d2=dict([(str(i), i) \
... for i in range(3)]),dict([(str(3-i), 3-i) for i in range(3)])')
2.83
And for comparison the first test without sorting (of course it evaluates to False):
>>> timeit.Timer('d1.keys()==d2.keys()', 'd1,d2=dict([(str(i), i) \
... for i in range(3)]),dict([(str(3-i), 3-i) for i in range(3)])')
1.11
We can clearly see initializing sets takes slightly longer than in-place sorting. But what happens if we work with more keys:
>>> timeit.Timer('l1,l2=d1.keys(),d2.keys();l1.sort();l2.sort();l1==l2', \
... 'd1,d2=dict([(str(i), i) for i in range(10)]), \
... dict([(str(10-i), 10-i) for i in range(10)])')
5.21
>>> timeit.Timer('set(d1.keys())==set(d2.keys())', \
... 'd1,d2=dict([(str(i), i) for i in range(10)]), \
... dict([(str(10-i), 10-i) for i in range(10)])')
5.59
When we increased item number to ten performance gap decreased. This is probably because while set comparisons are based on hashes as I mentioned before, list comparisons are item-by-item comparisons due to its mutability. Let’s go even higher and see if the difference becomes more clear:
>>> timeit.Timer('l1,l2=d1.keys(),d2.keys();l1.sort();l2.sort();l1==l2', \
... 'd1,d2=dict([(str(i), i) for i in range(25)]), \
... dict([(str(25-i), 25-i) for i in range(25)])')
16.21
>>> timeit.Timer('set(d1.keys())==set(d2.keys())', \
... 'd1,d2=dict([(str(i), i) for i in range(25)]), \
... dict([(str(25-i), 25-i) for i in range(25)])')
9.79
This time sets are almost 100% faster. It seems sets perform better than sorted lists overall. They are not much slower at lower item counts and significantly faster at higher item counts. I dawned on me when using sets I can take advantage of iterators instead of creating list objects. Here are the results for all three sizes:
>>> timeit.Timer('set(d1.iterkeys())==set(d2.iterkeys())', \
... 'd1,d2=dict([(str(i), i) for i in range(3)]), \
... dict([(str(3-i), 3-i) for i in range(3)])')
2.07
>>> timeit.Timer('set(d1.iterkeys())==set(d2.iterkeys())', \
... 'd1,d2=dict([(str(i), i) for i in range(10)]), \
... dict([(str(10-i), 10-i) for i in range(10)])')
3.79
>>> timeit.Timer('set(d1.iterkeys())==set(d2.iterkeys())', \
... 'd1,d2=dict([(str(i), i) for i in range(25)]), \
... dict([(str(25-i), 25-i) for i in range(25)])')
8.92
It performs best for all sizes. In addition it is quite intuitive and readable. I think I have found the foundation of my comparison function, it should look similar to this:
if set(d.iterkeys())==set(sequence_of_keys):
Getting A Little Further Than Hello World With Qooxdoo
I have mentioned about Rich Internet Applications in a previous post. Qooxdoo is an AJAX framework, especially strong on creating desktop-like GUI’s. It allows you to build your interface in an object oriented manner. Like tkinter or GTK, and much more than the others it is like swing.
Qooxdoo is well documented and has a clean API. It comes with a Python program to help with builds. Because it is such a big framework you test on a partially compiled source and when finished this build program generates a single (actually two, it also generates a loader), compact (and somewhat obfuscated) file for performance. I strongly advise you to give it a try. The following is a small introductory tutorial. It aims to go little further than Hello World. This is not a tutorial explaining object oriented programming concepts, I assume you are already fluent in Object Oriented Programming.
Create The Skeleton
We’ll create a simple calculator-like application. I am assuming you have downloaded (latest version is 0.8 for today) and extracted the source into a directory. Let’s call it qxtut. The first thing we will do is to create a skeleton of our application with the following command;
./qooxdoo-0.8-sdk/tool/bin/create-application.py --name basicmath
This command has created the following directory structure under ./basicmath;
qxtut/
qooxdoo-0.8-sdk/
basicmath/
source/
build/
cache/
api/
config.json
Manifest.json
Well, some of the directories (build & cache) are not there yet. But as we go on they will appear, I just wanted you to see how the application is organized. config.json and Manifest.json files are configuration files for the build tool. We don’t need to change them for this tutorial, but you are welcome to check their contents.
Let us build our source for the first time;
cd basicmath
./generate.py source
Now if you open ./source/index.html in your browser you can see a Hello World application in action. We’ll replace this with our own program. But before we proceed I’d like to point out a few things;
- When we define our classes, we call a class method define on qx.Class and pass our class’ name (along with its namespace) and its contents as arguments. We don’t define our classes using prototypes, in order to take advantage of Qooxdoo’s object oriented programming features.
- Qooxdoo supports single inheritance (with mixins). We define our base class, if we have one, using extend key.
- Instance members are defined inside the members key and class members are defined inside the statics key.
- construct and destruct are two special functions for initialization and cleanup of the class.
- Qooxdoo supports properties as well, but it is outside of this tutorial’s scope.
- Finally, “#asset(basicmath/*)” line tells the build program to include assets (images etc) in qxtut/basicmath/source/resource/basicmath directory.
Custom Classes
Let’s start building our application now. Here is a compact version of Application.js:
/* ************************************************************************
#asset(basicmath/*)
************************************************************************ */
qx.Class.define("basicmath.Application", {
extend: qx.application.Standalone,
members: {
main: function()
{
this.base(arguments);
if (qx.core.Variant.isSet("qx.debug", "on")) {
qx.log.appender.Native;
qx.log.appender.Console;
}
// Our code will come here
}
}
});
Now we will create a custom class named Operation. This class will take two operands and perform an operation on them, and then later we will add it the ability to report the result of the operation. Paste this as Operation.js:
/* ************************************************************************
#asset(basicmath/*)
************************************************************************ */
qx.Class.define("basicmath.Operation", {
extend: qx.ui.container.Composite,
construct: function() {
this.base(arguments);
var layout = new qx.ui.layout.HBox(6);
this.setLayout(layout);
this.operand1 = new qx.ui.form.TextField("0");
this.operator = new qx.ui.form.SelectBox();
this.operator.add(new qx.ui.form.ListItem("add"));
this.operator.add(new qx.ui.form.ListItem("subtract"));
this.operator.add(new qx.ui.form.ListItem("multiply"));
this.operator.add(new qx.ui.form.ListItem("divide"));
this.operand2 = new qx.ui.form.TextField("0");
this.result = new qx.ui.basic.Label("0");
this.add(this.operand1);
this.add(this.operator);
this.add(this.operand2);
this.add(this.result);
},
members: {
operand1: null,
operator: null,
operand2: null,
result: null
}
});
The code should be self explanatory. Notice here, we define operand1, operator, operand2 and result as members of the class. Also notice we initialize those members in the constructor and not in the class body[1]. This is because their respected values (classes) are derived from the non-primitive Object type. Therefore if we have assigned them a non-primitive type (such as the array [1, 2, 3]) in the members section; all instances would point to the same object.
Let us now plug this object in our application. Replace the comment line “// Our code will come here” with the following;
this.getRoot().add(new basicmath.Operation);
Now, when we compile the source and run index.html we should see our widgets in place.
Simple Behaviour
We want our widget to calculate the operation and show us the result. Let’s update the members section of Operation with the following;
members: {
operand1: null,
operator: null,
operand2: null,
result: null,
updateResult: function() {
var v1 = this.cleanField(this.operand1);
var v2 = this.cleanField(this.operand2);
var r;
switch(this.operator.getValue()) {
case "add": r = v1+v2; break;
case "subtract": r = v1-v2; break;
case "multiply": r = v1*v2; break;
case "divide": r = v1/v2; break;
}
this.result.setContent(String(r));
this.operand1.setValue(String(v1));
this.operand2.setValue(String(v2));
},
cleanField: function(field) {
var val = parseInt(field.getValue());
return isNaN(val) ? 0 : val;
}
}
We have added two functions here updateResult and cleanField. Now we make use of them, change the construct with the following:
construct: function() {
this.base(arguments);
var layout = new qx.ui.layout.HBox(6);
this.setLayout(layout);
this.operand1 = new qx.ui.form.TextField("0");
this.operand1.addListener("input", this.updateResult, this);
this.operator = new qx.ui.form.SelectBox();
this.operator.add(new qx.ui.form.ListItem("add"));
this.operator.add(new qx.ui.form.ListItem("subtract"));
this.operator.add(new qx.ui.form.ListItem("multiply"));
this.operator.add(new qx.ui.form.ListItem("divide"));
this.operator.addListener("changeValue", this.updateResult, this);
this.operand2 = new qx.ui.form.TextField("0");
this.operand2.addListener("input", this.updateResult, this);
this.result = new qx.ui.basic.Label("0");
this.add(this.operand1);
this.add(this.operator);
this.add(this.operand2);
this.add(this.result);
}
We just added these three listeners to update the result when an operand or the operator changes:
this.operand1.addListener("input", this.updateResult, this);
this.operator.addListener("changeValue", this.updateResult, this);
this.operand2.addListener("input", this.updateResult, this);
The last parameter (this) for addListener (even though it is sometimes unnecessary) set the scope within the listener code (the second parameter). Qooxdoo handles most of the binding automatically, I think this is included for flexibility.
Let’s compile and run again. The result of the operation should update as you change the values now.
Events To Tie All Together
I’ll give you the finished code first and then we can go over the details. Here is Operation.js:
/* ************************************************************************
#asset(basicmath/*)
#asset(qx/icon/Oxygen/*)
************************************************************************ */
qx.Class.define("basicmath.Operation", {
extend: qx.ui.container.Composite,
construct: function() {
this.base(arguments);
var layout = new qx.ui.layout.HBox(6);
this.setLayout(layout);
this.operand1 = new qx.ui.form.TextField("0");
this.operand1.addListener("input", this.updateResult, this);
this.operator = new qx.ui.form.SelectBox();
this.operator.add(new qx.ui.form.ListItem("add"));
this.operator.add(new qx.ui.form.ListItem("subtract"));
this.operator.add(new qx.ui.form.ListItem("multiply"));
this.operator.add(new qx.ui.form.ListItem("divide"));
this.operator.addListener("changeValue", this.updateResult, this);
this.operand2 = new qx.ui.form.TextField("0");
this.operand2.addListener("input", this.updateResult, this);
this.result = new qx.ui.form.TextField("0");
this.result.setReadOnly(true);
var close_button = new qx.ui.form.Button(
null,
"qx/icon/Oxygen/16/actions/application-exit.png"
);
close_button.addListener("execute", function(e) {
this.fireDataEvent(
"changeResult",
0,
parseFloat(this.result.getValue()),
false
);
this.destroy();
}, this);
this.add(this.operand1);
this.add(this.operator);
this.add(this.operand2);
this.add(new qx.ui.basic.Label("="));
this.add(this.result);
this.add(new qx.ui.core.Spacer(8));
this.add(close_button);
},
events: {
"changeResult": "qx.event.type.Data"
},
members: {
operand1: null,
operator: null,
operand2: null,
result: null,
updateResult: function() {
var v1 = this.cleanField(this.operand1);
var v2 = this.cleanField(this.operand2);
var r;
switch(this.operator.getValue()) {
case "add": r = v1+v2; break;
case "subtract": r = v1-v2; break;
case "multiply": r = v1*v2; break;
case "divide": r = v1/v2; break;
}
this.fireDataEvent("changeResult",
r,
parseFloat(this.result.getValue()),
false
);
this.result.setValue(String(r));
this.operand1.setValue(String(v1));
this.operand2.setValue(String(v2));
},
cleanField: function(field) {
var val = parseInt(field.getValue());
return isNaN(val) ? 0 : val;
}
}
});
And Application.js:
/* ************************************************************************
#asset(basicmath/*)
#asset(qx/icon/Oxygen/*)
************************************************************************ */
qx.Class.define("basicmath.Application", {
extend : qx.application.Standalone,
members : {
main : function()
{
this.base(arguments);
if (qx.core.Variant.isSet("qx.debug", "on")) {
qx.log.appender.Native;
qx.log.appender.Console;
}
var layout = new qx.ui.container.Composite(
new qx.ui.layout.VBox(8)
);
var layout_footer = new qx.ui.container.Composite(
new qx.ui.layout.HBox(6)
);
var total = new qx.ui.basic.Label("0");
var add_button = new qx.ui.form.Button(
"Add New",
"qx/icon/Oxygen/16/actions/list-add.png"
);
add_button.addListener("execute", function(e) {
var new_operation = new basicmath.Operation();
layout.addBefore(new_operation, layout_footer);
new_operation.addListener("changeResult", function(e) {
var old_total = parseFloat(total.getContent());
var new_total = old_total - e.getOldData() + e.getData();
total.setContent(String(new_total));
}, this);
}, this);
layout_footer.add(add_button);
layout_footer.add(total);
layout.add(layout_footer);
add_button.execute();
this.getRoot().add(layout);
}
}
});
Let’s go top-down and begin with the changes in the Application.js:
var layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(8));
this.getRoot().add(layout);
We don’t necessarily need to subclass everytime we need specialized behaviour. Since I had intented to re-use Operation I have it as a seperate class. But for the layout of the application I just instanciated some classes and tweaked them inside Application.main. layout here is the topmost widget, we will put everything else in it. Basically one or more Operation’s and a footer to dispay the grand total. VBox layout is by the way a layout manager that stacks children vertically, and a HBox stacks horizontally.
var layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(8));
var layout_footer = new qx.ui.container.Composite(new qx.ui.layout.HBox(6));
var total = new qx.ui.basic.Label("0");
var add_button = new qx.ui.form.Button(
"Add New",
"qx/icon/Oxygen/16/actions/list-add.png"
);
add_button.addListener("execute", function(e) {
var new_operation = new basicmath.Operation();
layout.addBefore(new_operation, layout_footer);
new_operation.addListener("changeResult", function(e) {
var old_total = parseFloat(total.getContent());
var new_total = old_total - e.getOldData() + e.getData();
total.setContent(String(new_total));
}, this);
}, this);
We define a label total to hold the grand total of all operations and an add button for new operations. Notice the closures work on 7th line. Although we limit ourselves a little bit to take advantage of OOP, we are still in a dynamic environment. Finally we tie all these components together and finally add the layout to the application root.
add_button.execute();
This instantiates the first Operation for us. We execute the button instead of creating the widget programmatically to avoid the code duplication (see the “execute” listener on the add_button).
Now let’s take a look at the changes in Operation.js. I have replaced the result Label with a TextField (remember to run “generate.py source” each time dependencies change). I wanted to take advantage of the getOldData function on TextField’s changeValue event. But appereantly it doesn’t supply the old data. But I kept it as a TextField anyway, setting it read-only.
Then I decided that Operation should signal for a result change (maybe this is more politically correct) and added a custom event changeResult on it.
events: {
"changeResult": "qx.event.type.Data"
}
This event is fired inside Operation.updateResult:
this.fireDataEvent(
"changeResult",
r,
parseFloat(this.result.getValue()),
false
);
The second parameter is returned from e.getData() and the third is from e.getOldData(). Therefore we can calculate the grand total without iterating all Operations;
var new_total = old_total - e.getOldData() + e.getData();
An important point here is to fire changeResult to correct the grand total before we destroy an Operation:
close_button.addListener("execute", function(e) {
this.fireDataEvent(
"changeResult",
0,
parseFloat(this.result.getValue()),
false
);
this.destroy();
}, this);
Wrapping Up
Now it should work correctly, if I haven’t made a typo of course. Let us build it with;
./generate.py build
It generates a loader (~150kb) and an application script (~400kb). You don’t need the Qooxdoo source anymore, you can just upload the contents of the build directory and your application would run.
This concludes my a little further than Hello World tutorial. If you find errors or typos, or have any questions please feel free to contact me.
[1] | Here is an explanation in Qooxdoo manual. |