sourceafReflux::View.fan

using afIoc

** Views are 'Panels' that are associated with an (editable) resource.
**
** For a 'View' to be displayed, a 'Resource' must list it as one of its 'viewTypes()'.
**
** 'Views' are automatically added to the 'EventHub', so to receive events they only need to implement the required event mixin.
@Js
abstract class View : Panel {
    @Inject private     Reflux          _reflux
    @Inject private     Errors          _errors
    @Inject private     RefluxEvents    _events
            internal    UndoRedo[]      _undoStack  := UndoRedo[,]
            internal    UndoRedo[]      _redoStack  := UndoRedo[,]

    ** The resource associated with this view.
    ** Set via 'load()'.
                    Resource?   resource

    ** Subclasses should define the following ctor:
    **
    **   syntax: fantom
    ** 
    **   new make(|This| in) : super(in) { ... }
    protected new make(|This| in) : super(in) { }

    ** Returns 'true' if the resource has unsaved change.
    ** 'Views' are responsible for setting this themselves.
    Bool isDirty {
        set {
            if (&isDirty == it) return
            &isDirty = it
            if (it) {
                name = "* ${name}"
            } else {
                if (name.startsWith("* "))
                    name = name[2..-1]
            }
            // could call this->onModify() but setting 'name' already does that
        }
    }

    ** Return 'true' if the View should re-used for the given resource.
    **
    ** By default this returns 'false'.
    virtual Bool reuseView(Resource resource) { false }

    ** Callback when the View should load the given resource.
    **
    ** By default this sets the resource, name and icon.
    virtual Void load(Resource resource) {
        if (isDirty)
            confirmClose(true)
        this.resource   = resource
        this.icon       = resource.icon
        this.name       = resource.name
    }

    ** Callback for when the panel should refresh it's contents.
    **
    ** By default this calls 'load()'.
    override Void refresh(Resource? resource := null) {
        if (resource != null && resource == this.resource) {
            load(resource)
            return
        }
        if (resource == null && this.resource != null) {
            load(this.resource)
            return          
        }
    }

    ** Callback when the View should save its resource. Only called when 'isDirty' is 'true'.
    **
    ** By default this just clears the dirty flag.
    virtual Void save() {
        isDirty = false
    }

    ** Callback when the view is being closed.
    ** Return 'false' if the view should be kept open.
    **
    ** Note: If 'force' is 'true' then the view **will** close regardless of the return value.
    **
    ** By default this returns 'true'.
    virtual Bool confirmClose(Bool force) {
        true
    }

    ** Callback to handle dropped files.
    ** Return 'true' to specify the drop event has been handled.
    virtual Bool onDrop(File[] droppedFiles) {
        return false
    }

    ** Add a pair of Undo / Redo commands.
    Void addUndoRedo(|->| undo, |->| redo) {
        // cap the history at something large but reasonable
        if (_undoStack.size > 999)
            _undoStack.size = 999

        _undoStack.add(UndoRedo {
            it.undo = undo
            it.redo = redo
        })

        // you can't return to the same future once you've changed the past!
        _redoStack.clear
        _events.onViewModified(this)
    }

    ** Undoes the last command.
    Void undo() {
        undoRedo := _undoStack.pop
        if (undoRedo == null) return

        try undoRedo.undo.call()
        catch (Err err)
            _errors.add(err)

        _redoStack.push(undoRedo)
        _events.onViewModified(this)
    }

    ** Redoes the last command.
    Void redo() {
        undoRedo := _redoStack.pop
        if (undoRedo == null) return

        try undoRedo.redo.call()
        catch (Err err)
            _errors.add(err)

        _undoStack.push(undoRedo)
        _events.onViewModified(this)
    }
}

@Js
internal class UndoRedo {
    |->| undo
    |->| redo
    new make(|This|f) { f(this) }
}