This is the documentation for developers wanting to extend the extension. Users wanting to learn about the extension should start here instead.
The eXtraWidget architecture is fairly modular. Complete understanding of it is not strictly required for developing new kinds of widgets, but it can't hurt to have an overview.
Here is the dependency diagram:
It is basically a summary of the information contained in the main build.sbt
. Each module is a separate sbt sub-project and compiles to its own JAR.
All new widget kinds should only depend on the api
JAR.
Ideally, api
would have contained only interfaces, but as it stands, it contains implementations as well. Future work on the project should try to move most implementations to core
.
The point of having separate modules for core
and xw
(the latter being the extension itself) is that, in theory, the extension is only one of the possible ways to manipulate extra widgets. We could envision a plugin that would manipulate all widgets through the GUI. This plugin would depend on core
and could co-exist with the extension while remaining independent from it.
The state of the extension (i.e., widgets and the values of their properties) is stored in a map of maps: Map[WidgetKey, Map[PropertyKey, PropertyValue]]
. In other words, a map from widget keys to maps from property keys to property values. These maps are Java ConcurrentSkipListMap
, which allows for concurrent access and keeps the keys in alphabetical order. Keys are strings and property values are AnyRef
(the Scala equivalent of java.lang.Object
). The concurrent access part is important because the map can be modified from both NetLogo's job thread (when extension primitives are used) and the AWT Event thread (when a user manipulates widgets on the screen). You should probably read the note on threads in the NetLogo architecture guide to get a better idea of what this implies.
Since the extension is built to work with NetLogo 6.x, you need to use Scala 2.12.x and Java 8.
As you can see from the source code of various widget kinds bundled with the extension, you need to define at least two classes to create a new widget kind:
-
A class that extends
uk.ac.surrey.xw.api.WidgetKind
. This is the definition of your widget that will be loaded at runtime by the extension to find out what properties are available for your kind and what to name the primitives that it will generate. -
A class that extends
uk.ac.surrey.xw.api.ExtraWidget
. This is the class of your widgets themselves, those that will appear on the screen.
We'll say a few words about each of those in turn. This, along with Properties, should be enough to get you started.
You should refer to the ScalaDoc for details, but when extending WidgetKind
, there are two things that you absolutely need to provide:
- A name for your class (
name
) that the extension will use to generate primitives. - A constructor function (
newWidget
) that the extension will use to create widget instances.
In addition, you will most likely want to provide:
- A few extra properties (
WidgetKind
gives you theKIND
andKEY
properties, but your widget would not be very useful if that is all you had). Be sure to overridepropertySet
to include your custom properties, probably by appending your own set tosuper.propertySet
. - A default property (
defaultProperty
) to be used with thexw:get
andxw:set
primitives.
In some instances, you may want to also provide:
- A plural name for your kind (
pluralName
). The extension usually just adds an'S'
, but sometimes, that's not enough (e.g.,"CHECKBOX"
→"CHECKBOXES"
). - Default values for your properties (
defaultValues
).
Also note that WidgetKind
has a type parameter that indicates the type of the widget instances. When extending WidgetKind
, you will typically do something like this:
class MyWidgetKind[W <: MyWidget] extends WidgetKind[W]
Well, that's not quite true. The api does provide JComponentWidgetKind
, a class that extends WidgetKind
with most of the properties you need for a JComponent
widget. I see no reason why you wouldn't want to do that. So:
class MyWidgetKind[W <: MyWidget] extends JComponentWidgetKind[W]
Note that, in such a case, MyWidget
would need to extend JComponentWidget
(which, again, is something that you probably want. The extension also provide a couple of more specialized kinds, of which LabeledPanelWidgetKind
(the "super kind" of choosers, multi-choosers, sliders and inputs) is more likely to be useful.
To conclude with an example, here is (as of December 2014), the full SliderKind
class (a lot of stuff being provided by LabeledPanelWidgetKind
, of course):
class SliderKind[W <: Slider] extends LabeledPanelWidgetKind[W] {
override val name = "SLIDER"
override val newWidget = new Slider(_, _, _)
val valueProperty = new DoubleProperty[W](
"VALUE", Some(_.sliderData.setValue(_)), _.sliderData.value, 50)
override val defaultProperty = Some(valueProperty)
override val propertySet = super.propertySet ++ Set(valueProperty,
new StringProperty[W]("UNITS",
Some(_.setUnits(_)), _.units),
new DoubleProperty[W]("MINIMUM",
Some(_.sliderData.setMinimum(_)), _.sliderData.minimum, 0d),
new DoubleProperty[W]("MAXIMUM",
Some(_.sliderData.setMaximum(_)), _.sliderData.maximum, 100d),
new DoubleProperty[W]("INCREMENT",
Some(_.sliderData.setIncrement(_)), _.sliderData.increment, 1d)
)
}
This is the code for your actual widget.
One requirement is that your constructor should take parameters key: WidgetKey
, state: State
, and ws: GUIWorkspace
, and that those are declared as val
so that they implement abstract values of the same name in ExtraWidget
.
The other requirement is that you provide a kind
value for instances of your class. The code for Slider
illustrates these requirements:
class Slider(
val key: WidgetKey,
val state: State,
val ws: GUIWorkspace)
extends LabeledPanelWidget {
override val kind = new SliderKind[this.type]
// ... lots of other stuff ...
}
(Note: perhaps there would be a way to organize the types such that it would not be necessary to provide the widget's singleton type (this.type
) to the kind class, but I haven't found it yet.)
Notice how the class extends LabeledPanelWidget
(which in turn extends JComponentWidget
and so on until we reach ExtraWidget
as the root class).
The actual content of the class will vary a lot from one widget to another.
- A bunch of getters and setters to be used by properties.
- A swing event listener that will update the state of the extension using the
ExtraWidget.updateInState
method.
The goal of updateInState
is to update the value of a property in the extension's State
following user interactions. In a slider, for example, this is what happens when a user drags the slider's knob around:
slider.onStateChange { _ =>
sliderData.updateFromTicks(slider.getValue)
valueLabel.update()
updateInState(kind.valueProperty)
}
The onStateChange
method (defined in the uk.ac.surrey.xw.api.swing
package) is just a shortcut to add a javax.swing.event.ChangeListener
to our slider. sliderData.updateFromTicks(slider.getValue)
and valueLabel.update()
update some fields of the widget class following the change of value.
The interesting line is updateInState(kind.valueProperty)
: it tells the extension to update the valueProperty
of the slider (as defined in SliderKind
) in its state map. And properties happen to be the topic of our next section.
Unless you need Float
s or Long
s (for which I haven't defined property classes) you shouldn't have to write property classes: you just need to understand how to create instances of the provided classes. Those are:
ObjectProperty
StringProperty
BooleanProperty
IntegerProperty
DoubleProperty
ColorProperty
ListProperty
As might you might have guessed, each of them allows you to define a property of a particular type. When creating an instance, you will need to provide:
- The type of the widget that this property applies to. This is normally the type parameter of the widget kind itself. For example, if your kind is defined as
SliderKind[W <: Slider]
, you just pass onW
to your property definitions, e.g.:new StringProperty[W](/*...*/)
. - The property key, as an uppercase string.
- A setter function of type
Option[(W, T) ⇒ Unit]
, whereW
is the widget's type andT
is the property's type. Often, this will just forward a call the a method of the widget. For example, the setter of the"UNITS"
property in a slider is:Some(_.setUnits(_))
. If the property should be read-only, simply passNone
instead. - A getter function of type
W ⇒ T
. Again, this is most likely just a call to a getter method in the widget, e.g.:_.units
. - A default value for the property. That last parameter can be omitted, in which case the default will be:
nobody
for anObjectProperty
,""
for aStringProperty
,false
for aBooleanProperty
,0
for anIntegerProperty
,0.0
for aDoubleProperty
,white
for aColorProperty
and[]
for aListProperty
.
And that's it. Don't forget to make sure that your properties are included in the propertySet
of your WidgetKind
.
I think it should be possible to create new widget kinds using Java (or any other JVM language, for that matter), but I haven't tried. That being said, interacting with NetLogo in general is much easier from Scala then from Java. And there is a better chance that I will be able to help you with your project if you use Scala.
In addition to the usual options of opening a new issue or asking a question on StackOverflow, you're welcome to send me an email if you're thinking about developing a new widget kind. I am well aware that the instructions provided here are a little thin, and I'd be happy to fill in the gaps as needed.