Skip to content

Architecture

Nikolaus Knop edited this page Oct 14, 2020 · 2 revisions

Like in the Model-View-Controller Pattern there are three types of components:

  • the AST-node - corresponding to the model in MVC
  • the editor view - corresponding to the view in MVC
  • and the editor - corresponding to the controller in MVC
    The editor defines the different interactions that can be made with the AST-node. One editor can have multiple editor views, that display the editor on the screen and trigger the different interactions of the editor when the user interacts with the view. All the editor views of one particular editor stay in sync, because when one of those views triggers an interaction, the editor notifies all of his views of the resulting change.
    Image couldn't be loaded

Note that there is no arrow from the AST-node to one of the other components. This means that the syntax-model can be developed separately from the editors and editor views.
In Hextant all editors have to implement the generic interface hextant.core.Editor<R>. The type-parameter R stands for the type of AST-node that is produced by instances of an editor class. The Editor-interface has many properties and functions, but the most important one is the following property:

val result: ReactiveValue<Validated<R>>

The type of the result-property needs some explanation. Let's analyze the type from the inside to the outside. As was just explained, R is the type of AST-node that is produced by the editor. The sealed class Validated<T> is a discriminated union, that encapsulates either a valid result of type T or an invalid result attached with a String-message indicating why it is invalid. The type ReactiveValue<T> represents a value that varies over time and notifies registered handlers, everytime it changes. So condensing these three explanations, result is a time-varying validated value of type R.
When you are creating new editors, you don't want to implement all the properties and functions of the Editor-interface again. Therefore, there is the abstract base class hextant.core.editor.AbstractEditor<R, V>, which implements almost all definitions from the Editor-interface. The only property which is not implemented by AbstractEditor is the result-property. The class AbstractEditor also provides the functionality needed to register editor views and notify them.
Editor views can register themselves by calling the method addView(view: V) and editors extending AbstractEditor can notify all their views by calling the higher-order function views(action: V.() -> Unit), which executes the action on all views that are registered for the editor.
The interface for editor views is named hextant.core.EditorView. Hextant is built upon the JavaFX GUI-library and provides the abstract base class hextant.core.view.EditorControl, that is a subclass of javafx.scene.control.Control and implements the functionality required by EditorView. Now it's time to demonstrate all of this with a small example editor. First, let us create the model of our AST.

package counter

data class Counter(val value: Int)

Now we define an interface for the editor views, that can be used to display the counter. The showCount-function will be called everytime, the value of the counter changes.

package counter

import hextant.core.EditorView

interface CounterEditorView: EditorView {
    fun showCount(value: Int)
}

The most important part, of course, is the editor. The map function, which is used to obtain the result, is perhaps best explained by its signature:

fun <T, F> ReactiveValue<T>.map(f: (T) -> F): ReactiveValue<F>

It takes a function that converts values of type T to values of type F and returns a ReactiveValue of type F that is always updated when the original ReactiveValue changed. Fun-fact: the map-extension makes ReactiveValue a functor.

package counter

import hextant.context.Context
import hextant.core.editor.AbstractEditor
import reaktive.value.binding.map
import reaktive.value.now
import reaktive.value.reactiveVariable
import validated.valid

class CounterEditor(context: Context): AbstractEditor<Counter, CounterEditorView>(context) {
    private val value = reactiveVariable(0) //time varying variable holding the count

    override val result = value.map { v -> valid(Counter(v)) }

    //increment the counter and notify all the views about the change
    fun increment() {
        value.set(value.get() + 1)
        views { 
            showCount(value.get())    
        }
    }
    
    //when a view is added to this editor, it must show the current count
    override fun viewAdded(view: CounterEditorView) {
        view.showCount(value.get())
    }
}

All that is left, is the actual editor view. The editor is displayed as a button showing the current count and incrementing it when clicked. To let Hextant know, that instances of CounterEditor should be displayed with a CounterEditorControl, the constructor is annotated with the @ProvideImplementation annotation. In fact, this is just a specific use case of a much more general mechanism implemented by Hextant, about which you can learn in this tutorial.

package counter

import bundles.Bundle
import hextant.codegen.ProvideImplementation
import hextant.context.ControlFactory
import hextant.core.view.EditorControl
import javafx.scene.control.Button

class CounterEditorControl @ProvideImplementation(ControlFactory::class) constructor(
    private val editor: CounterEditor, arguments: Bundle
) : EditorControl<Button>(editor, arguments), CounterEditorView {
    private val btn = Button()

    init {
        btn.setOnMouseClicked { editor.increment() }
        editor.addView(this)
    }

    override fun createDefaultRoot(): Button = btn

    override fun showCount(value: Int) {
        root.text = value.toString()
    }
}

The next tutorial shows you how to implement a basic editor for arithmetic expressions.