Glok / Comparison with JavaFX.md
Comparison with JavaFX
I'm not trying to create a clone of JavaFX, so expect many differences. When Glok's approach is different from JavaFX, I'll note it here.
Many of these differences are improvements IMHO. That was a pleasant surprise, because I set out to build a good enough alternative to JavaFX. But as I started working on it, I've seen plenty of opportunities for improvements.
Float not Double
JavaFX's coordinate system uses Double, Glok uses Float.
Is there something I'm missing? Float has 7 digits of precision. Easily enough. Isn't it? Displays are thousands of pixels wide, not millions!
Glok doesn't support transformations (e.g. rotation), maybe that's why JavaFX chose Double??? Update. Glok now supports rotations in multiples of 90°.
Event Handling
Glok probably doesn't behave exactly like JavaFX in the order/way events are processed, especially mouse events (there are lots of edge cases).
One node can have multiple event handlers for a single event type.
This is only true for some events in JavaFX. For example Button's onAction is a single instance, not a collection.
So in JavaFX, there is only 1 onAction
handler.
In Glok, onAction
is a single instance, but that instance can be a chain
.
When setting onAction
, you can choose to replace the existing handler (if there is one), or
chain them (with the new handler either before, or after the existing handler).
Drag Events
Glok currently only supports the simple press-drag-release
gesture, whereas JavaFX has 3 different
drag gestures.
Also, the way that dragging is initiated is different in Glok. We have MouseEvent.capture(), whereas JavaFX seems to detect dragging automatically. I think it checks if a mouse movements is beyond a threshold. If so, there's some edge cases which are be problematic. e.g. If you press the edge of a button and move slightly (less than the threshold), and the mouse is now in another button. Should the small movement issue a mouse move event, what about mouse enter/exit events? It seems fragile.
So, while Glok's solution requires an extra line of application code, IMHO, it is cleaner.
Also, when/if Glok implements the other 2 drag gestures available in JavaFX, then capture()
will be a good place to state which type to use.
No Skins
Controls are not Skinnable (but are all stylable, using a Theme
).
I do like the clear separation of model
and view
when using JavaFX, but I don't see the need for skins.
FYI, the Skin is generally much more complicated than the Control class itself.
So to create a new Skin is not much easier (if at all) than creating a new Control from scratch.
I've never seen an alternate skin for a built-in JavaFX control. Styling seems to be sufficient (which Glok does support).
All Glok controls extend Region
directly, there is no Control
class.
The Default Theme
Glok's default theme shamelessly mimics that of Muse Score
,
a music composition application under a GPL license.
Unlike JavaFX, Glok's themes do not use CSS, they are created using A DSL (domain specific language), which is Kotlin code.
This has many benefits :
Makes full use of your IDE's syntax highlighting, auto-complete etc.
Less error-prone, as syntax errors are highlighted while typing.
Named constants can be used instead of plain strings (even less error-prone).
Much more flexible, because any Kotlin code can be used. e.g. you can create your own function to create a derived color from a base color.
Themes can be changed at run-time very easily. The default theme (Tantalum) uses
Properties
. If you change any of these properties, the theme will be regenerated. For example, Changing theaccent
color throughout your application is as easy as :Tantalum.accentColor = Color.RED
This is true of other properties, such as padding for
Buttons
.The default theme uses a color palette of 12 colors, which can be switched from light to dark with a single line of code.
ContentDisplay
Glok's default theme sets Buttons in a ToolBar to use ContentDisplay.GRAPHIC_ONLY
.
This means that a button which has text and a graphic will only display the graphic.
This is important (and different to JavaFX), because we can define buttons with both a graphic and text, and if they are in a ToolBar, the text will be omitted, but in other places the text will be displayed.
The action
package relies on this behaviour. e.g. a Save
Action creates a button with text AND a graphic,
but when that button is placed in a ToolBar, it will only show the graphic, not the "Save" text.
If (on very rare occasions) you want a toolbar button to display the text as well as the graphic,
set contentDisplay = ContentDisplay.LEFT
.
IMHO, JavaFX's contentDisplay = ContentDisplay.GRAPHIC_ONLY
is a really neat idea,
and is underused within JavaFX because JavaFX's default theme does not set it within ToolBars.
Input Focus and Focus Navigation
Not finished yet.
JavaFX highlights the control with input focus, regardless of how it gained focus (via mouse clicks or via the keyboard). IMHO, this is wrong.
When you click a button, you know you've done it, why do you need a colored border around it? This is visual clutter.
For those who like, or need, to use the keyboard to navigate, additional visual clues are required. This part isn't implemented yet.
I also plan taking navigation ideas from Muse Score too. One shortcut allows you to jump from one "section" of the GUI to another
TextArea
Rather than storing the text as a simple String
, Glok's TextArea uses a TextDocument
class,
which has an ObservableList
of String
s.
TextDocument fires TextChange
events whenever it changes
Advantages :
- A TextDocument can be shared by two TextAreas (e.g. use a SplitPane for 2 views of the same TextDocument)
- Efficiency. A traditional approach gets bogged down when documents are long. IMHO, 1,000 lines is short ;-)
- The caret and anchor positions are sensible, giving a row and column index. A traditional approach only supplies an Int, which is the index from the start of the document. IMHO, this is daft/useless!
Disadvantages :
- The
text
property is a calculation, and therefore slow. - If you want to observe a TextView, you need to observe the
ObservableList
, or use aDocumentListener
. TextArea
andTextField
share less in common (theTextInputControl
base class).
StyledTextArea
StyledTextArea
is very similar to TextArea
, but also has the ability to style parts of the document.
The styling is limited - all fonts use must be fixed-width and the same size.
This still allows for different styles (bold, italics, plain), and also lets you change the font's color
as well as the background color.
Underlining text can also be achieved using UnderLineBackground
. However, you cannot underline, AND
change the background color.
Use cases :
- Syntax highlighting of source code.
- Styled read-only text (as seen in the "About" tab of each Demo).
- Highlighting search matches as part of a search and replace tool
- Other highlights (e.g. highlight a line which contains an error)
At present, StyledTextArea
is not suitable as a rich text editor, because the highlights are
not part of the document's undo/redo mechanism.
ScrollPane
Maybe I don't understand all the use-cases for ScrollPane - Glok's implementation is drastically
simpler than JavaFX's.
There are no external properties for hMin
hMax
hValue
etc. They are calculated based on
the viewport size, and the preferred size of the content.
FitToWidth / fitToHeight default to true
. I think JavaFX defaults to false
(and I'm not sure if the semantics are the same).
Property Beans
In Glok, the bean and beanName are set for you, if you use a Kotlin delegate to create the Property like so :
class MyObject {
val widthProperty by doubleProperty(1.0)
var width by widthProperty
init {
println( widthProperty )
}
}
The result is :
MyObject.width = 1.0
We can see it's working from the output - Property's toString()
prints the bean's
simple class name, and the beanName. In this case MyObject
and width
.
EventType
Is it just me, or were you dumbfounded when you first saw JavaFX's EventType
?
I remember looking for EventType.MOUSE_PRESSED
, and not being able to find it.
Glok puts them all in EventType
. JavaFX smears them out across many Event
classes.
Adding event handlers in Glok is simpler (despite using identical constructs behind the scenes) :
myNode.onMousePressed { println("Hello World") }
But you can still do it the JavaFX way if you prefer.
Event isn't a generic type. There might be cases where this is a disadvantage, but it's fine for the vast majority of the time.
Key Events
There are separate, independent classes for KeyEvent
and KeyTypedEvent
,
as they contain different data.
Mouse Events
The position of the mouse is only given relative to the scene, not relative to the Node handing the event. In the vast majority of cases, we don't care where the mouse is, so IMHO for Glok to do this calculation is wasteful.
To find the local coordinates : event.sceneX - myNode.sceneX
, and the same for Y
.
However, this won't give the answer you might expect for nodes with transformations
. e.g. when using the Rotation
Node.
Sorry. This is in my bug list, please be patient.
It's easy to get around this, on an ad-hoc basis. But a complete (and fast) solution is trickier.
And I haven't decided on the best approach yet.
Listeners
JavaFX uses the name addListener
for both InvalidationListener
and ChangeListener
.
This is annoying when using lambdas. So Glok uses two different names :
- addListener for
InvalidationListener
- addChangeListener for
ChangeListener
/ListChangeListnerer
/SetChangeListener
.
As with JavaFX, Glok uses strong references for listeners by default, and there are wrapper classes
WeakInvalidationListener
and WeakChangeListener
, which can help prevent memory leaks.
Glok also provides the methods addWeakListener
and addWeakChangeListener
which is simpler than using the wrapper
classes directly.
Generic Property Types
JavaFX has specific subclasses of Property (such as StringProperty
) and a generic ObjectProperty<T>
.
In JavaFX, if you want a Color property, you use ObjectProperty<Color>
.
Glok is similar, it has a generic Property<V>
, and specifically typed subclasses,
such as StringProperty
, FloatProperty
.
However, unlike JavaFX, Glok has specifically typed subclass for all properties.
e.g. there is a ColorProperty
which extends Property<Color>
.
Glok applications are encouraged to do the same, but you don't have to.
There is a template file, and a shell script which generates the required boilerplate. Feel free to copy/paste them into your application!
There is a downside to this approach, the API looks scary.
The boilerplate
package is a
monster
.
This extra boilerplate lets us use property-functions
, that would be impossible otherwise (due to Type-Erasure).
e.g. Consider a SideProperty
(values TOP, LEFT, BOTTOM, RIGHT), we can define an extension function, which converts
it to an ObservableOrientation
(values HORIZONTAL, VERTICAL) :
val sideProperty by sideProperty( Side.LEFT )
var side by sideProperty
val orientationProperty = sideProperty.orientation()
val orientation by orientationProperty
FYI The definition of the orientation()
method is :
fun ObservableSide.orientation(): ObservableOrientation = OrientationUnaryFunction(this) { it.orientation() }
Changing side
automagically changes orientation
, and we can listen to orientationProperty
for when
side
changes from TOP/BOTTOM to LEFT/RIGHT or vice-versa.
Grow/Shrink Priority
JavaFX uses an enum class Priority
to decide which Nodes should grow when laying out
child Nodes.
Glok takes a different approach. Every Node has a growPriority
.
When the parent Node is larger than required by its children's sizes, then any children
with a growPriority
> 0 will become larger than their pref
size.
The extra amount allocated is the ratio of a child's growPriorty
to the total of all children's growPriority
.
For example, if one child has a priority of 1 and the other has a priority of 0.5,
then the first child will be given twice as much extra space as its sibling.
A similar thing occurs when the available size is less than required by the children's pref size,
but this time shrinkPriority
is used.
This, IMHO, gives a more pleasing result than JavaFX's approach.
Especially when you want the shrinkPriority
to be different to the growPriority
(which JavaFX doesn't support).
FYI, For a near identical behaviour to JavaFX :
- Priority.ALWAYS -> grow and shrinkPriority of 1.0
- Priority.SOMETIMES -> grow and shrinkPriority of 0.001 (an arbitrary small number)
- Priority.NEVER -> grow and shrinkPriority of 0
Node.visible is considered during layout
Unlike JavaFX, the Node's visible
property is considered when laying out children.
So if you make a child invisible, other nodes will move into that space.
With JavaFX an invisible Node still occupies space.
I can't think of a single time that I've wanted JavaFX's behaviour,
and plenty of times that I've wanted Glok's.
This is a very big deal to me, and IMHO, JavaFX took a big step backwards in this regard. Temporarily hiding a button in a ToolBar is really hard in JavaFX!
Collections
Glok uses Kotlin naming convention for List and Set.
i.e. ObservableList
and ObservableSet
are immutable,
while MutableObservableList
and MutableObservableSet
are mutable.
JavaFX uses ReadOnlyObservableList
and ObservableList
(the latter is mutable).
Unlike JavaFX, Glok only passes a single ListChange
to ListChangeListeners
.
This means that there are multiple events fired for ObservableList.retainAll( Collection )
and
ObservableList.removeAll( Collection )
.
The same applies to ObservableSet
.
Collections are in the subproject glok-model
, so you can use them without having a dependency on glok-core
.
Themes (Styling a Scene)
Glok doesn't use CSS to style scenes.
Instead, it has a DSL (Domain Specific Language) to define a Theme
.
IMHO, this is much nicer to use, and is more flexible.
Here's a snippet from a theme definition :
val buttons = button or toggleButton or radioButton
buttons {
borderSize(1)
plainBorder(borderColor)
background(buttonColor)
padding(buttonPadding)
labelPadding(labelPadding)
HOVER {
plainBorder(highlightColor)
}
ARMED {
background(highlightColor)
}
// Only use by ToggleButton and RadioButton, but not Button.
SELECTED {
background(panelColor.brightnessFactor(0.8f))
border(RectangleBorder(borderThickness, highlightColor))
}
}
Isn't that cleaner than CSS? This is Kotlin code, so your IDE will auto-complete, syntax highlight etc. Note, that this defines the styles for all the button types, not just Button itself.
BTW, in this snippet buttonColor
, labelPadding
are the values of Properties
.
If we change the property value mid-application, the scene will be restyled to reflect that change.
So, for example, it is easy to let users choose the highlightColor at runtime.
Oh, BTW, Node.styles
is equivalent to JavaFX's Node.styleClasses
, but Glok uses an ObservableSet
rather than an ObservableList
.
Scaling for High DPI Monitors
Scaling for high DPI devices (dots per inch) can be changed at runtime. JavaFX reads the env variable GDK_SCALE only at startup, and cannot change scale at runtime.
Glok does the same at start-up, as well as applying a heuristic based on monitor size if GDK_SCALE is absent. However, it stores the scaling factor as a property on [Application], which you can change mid-application to scale the entire GUI. Nice.
Fonts and images can use the full resolution of your display, while the application uses
logical pixels
, rather than physical pixels
to define everything.
This means your application is easy to code (because it uses logical
pixels everywhere),
and looks good (because the full resolution of the display is used).
TextField
As well as prefColumnCount
, there's also minColumnCount
.
It has an expectDigits
property, which, when true, uses the max width of digit glyphs (as well as .
, -
)
rather than W
when calculating prefWidth
and minWidth
from prefColumnCount
and minColumnCount
.
Without this (I'm look at you JavaFX), prefColumnCount
is useless for number fields!
The default value for expectDigits
is true
for Spinner
's editor, but false
everywhere else.
TabPane
Glok's TabPane is a simple wrapper around two other independent controls : TabBar
and SingleContainer
.
A TabBar
only show the tabs, not the contents of the current tab.
By breaking it into two parts, we get greater flexibility.
For example, you can place a TabPane
into a ToolBar
, and add extra buttons to the ToolBar
.
e.g. A "Open Recent Files" pull-down, and a "New Tab" button.
It also gives greater flexibility for the layout of your scene.
e.g. you could place a ToolBar
between the TabBar
and the SingleContainer
.
(I'm not a fan of this design, but many web browsers have their toolBars between the TabBar
and the main content).
The API of a TabPane
is what your would expect, but it has very little code, which just forwards
everything to the TabBar
, which does all the hard work.
Glok's default Theme
has two different styles for TabBar
/TabPane
, as there are two very different uses :
- Tabs containing your applications documents
- Different sections, e.g. Application preferences split over many tabs.
SplitPane
SplitPane currently ignores minWidth
/minHeight
of its child nodes.
For my needs this isn't an issue, but I do plan on respecting minWidth/minHeight at a later date.
Applications which prevent me from shrinking a Docked
panel annoy me!
(Due to the SplitPane respecting the minWidth of the Dock).
ListView
Only supports vertical lists (and I have no plans to implement horizontal lists).
Currently only supports single-selection model.
Doesn't support editing in place, although custom ListCells can have editable controls (e.g. TextFields rather than Labels).
ListCells are not reused (as they are in JavaFX). As you scroll through a ListView, new ListCells are created as required (and the old ones garbage collected).
IMHO, this makes application code involving custom cells simpler. The Glok code is simpler too.
There is a small performance hit, but it isn't significant.
TreeView
Like ListView
, TreeCell
s are not reused (as they are in JavaFX).
Glok also has a MixedTreeView
, which is designed for trees where the items can be of different types.
I've always disliked using JavaFX's TreeView
and never knew why until I wrote MixedTreeView
;-)
FYI, TreeView
and MixedTreeView
look identical, and use the same Theme
rules.
Stage and Window
Unlike JavaFX, Stage is not a subclass of Window. There is no public API to get access to Glok's Window.
Window contains OpenGL specific code, nothing that an application should ever need access to.
Glok has two distinct types of Stage
A RegularStage
is implemented by a native window, but Glok also has OverlayStage
, which
are fake windows drawn on top of a RegularStage
.
There are a few reasons for this.
- I couldn't find a way to create a modal window using
LWJGL
(the library Glok depends on for OpenGL). - When in
full-screen mode
, you cannot open another window. - If Glok is ever used within a web browser, we cannot open a new window
RegularStage
and OverlayStage
both implement the Stage
interface, so app developers
shouldn't care which implementation is used.
Note however, OverlayStage
's window decorations won't match the native ones.
They are rendered by Glok, not the platform's window manager
(and are styled using a Theme
, just like all other Glok Nodes).
PopupMenu
s and ToolTip
s both use OverlayStage
.
This has a couple of drawbacks :
PopupMenu
s cannot extend beyond the bounds of theRegularStage
. So if you have a long menu, and a smallRegularStage
, the menu will be cropped.- If you open a
PopupMenu
and then click theRegularStage
's title bar, thePopupMenu
doesn't disappear. Glok doesn't receive that mouse click event, so there's nothing I can do about it :-(
Menus
PopupMenu
can contain ANY node types, and MenuItem
is just a Node
.
i.e. there is no special hierarchy for menu items.
A few advantages :
- Less Glok code (as
MenuItem
is just another subclass ofNode
) - More flexible
- Menus can be styled using
Theme
s, just like any otherNode
s.
Command Line Arguments
IMHO, JavaFX makes a pig's ear of handling command line arguments.
Firstly, it calls them Parameters
- Grr - Parameters don't have values, an argument
is the value
that is supplied to a particular parameter.
So the data structure which holds values, should be called Arguments
.
Secondly, JavaFX has built-in argument parsing, but doesn't follow the Posix standard of
parsing --
as the end of arguments. Grr.
Glok doesn't automatically attempt to parse arguments at start-up (which is impossible without knowing the allowable options, and which options have values, and which are just flags).
However, there are two built-in argument parsers which your application can call if you wish :
- fun posixArguments( ... ). Uses single dashes for flags and options expecting values. e.g.
-foo value
- fun doubleDashArguments( ... ). Uses single dashes for single-character flags,
and two dashes for longer flag/parameter names. The value is always preceded by a space.
e.g.
--foo value
.
I considered writing a GNU-style version which uses --foo=value
syntax, but it's so
annoying to use, due to the problem with filename completion.
Feel free to ignore both implementations, and parse the arguments as you see fit.