package net.shadowfacts.cacao.viewcontroller import net.shadowfacts.cacao.Window import net.shadowfacts.cacao.util.properties.ObservableLazyProperty import net.shadowfacts.cacao.view.View import net.shadowfacts.kiwidsl.dsl import java.lang.RuntimeException import java.util.* /** * The base Cacao View Controller class. A view controller is an object that owns and manages a [View]. * * The view controller receives lifecycle callbacks for its view. * * @author shadowfacts */ abstract class ViewController { /** * The window that contains this view controller. * This property is not set until either: * a) a [Window] is initialized with this VC as it's root view controller or * b) this VC is added as a child of another view controller. */ var window: Window? = null /** * Helper function for creating layout constraints in the domain of this VC's window. * This function is not usable until [window] is initialized. */ val createConstraints get() = window!!.solver::dsl /** * The view that this View Controller has. * This property is created by [loadView] and is not initialized before that method has been called. * * @see loadView */ lateinit var view: View protected set /** * This VC's parent view controller. If `null`, this VC is the root view controller of its [window]. */ var parent: ViewController? = null set(value) { willMoveTo(value) field = value didMoveTo(value) } // _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods private var _children = LinkedList() /** * The list of all the child VCs of this view controller. * This list should never be mutated directly, only by the [embedChild]/[removeChild] methods. */ val children: List = _children /** * This method somehow loads a [View] and sets this VC's [view] property to it. * * This method should only be called by the framework. After the [view] property is set, the framework is * responsible for initializing its [View.window]/[View.solver] properties and calling [View.wasAdded]. * * The default implementation simply creates a [View] and does nothing else with it. */ open fun loadView() { view = View() } /** * This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been * called. */ open fun viewDidLoad() {} /** * This method is called immediately before the [Window.solver] is going to solve constraints and update variables. * If overridden, the superclass method must be called. */ open fun viewWillLayoutSubviews() { children.forEach(ViewController::viewWillLayoutSubviews) } /** * This method is called immediately after the [Window.solver] has solved constraints and variables have been updated. * This method is responsible for invoking the VC's [View.didLayout] method. * If overridden, the superclass method must be called. */ open fun viewDidLayoutSubviews() { view.didLayout() children.forEach(ViewController::viewDidLayoutSubviews) } /** * Called when the VC's view has been added to the screen and is about to be displayed. */ open fun viewWillAppear() { children.forEach(ViewController::viewWillAppear) } /** * Called immediately after the VC's view has first been displayed on screen. */ open fun viewDidAppear() { children.forEach(ViewController::viewDidAppear) } /** * Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen * or because the [net.shadowfacts.cacao.CacaoScreen] has been closed. */ open fun viewWillDisappear() { children.forEach(ViewController::viewWillDisappear) } /** * Called after the view has disappeared from the screen. */ open fun viewDidDisappear() { children.forEach(ViewController::viewDidDisappear) } /** * Called before the view controller's parent changes to the given new value. * * @param parent The new parent view controller. */ open fun willMoveTo(parent: ViewController?) {} /** * Called after the view controller's parent has changed to the given new value. * * @param parent The new parent view controller. */ open fun didMoveTo(parent: ViewController?) {} /** * Embeds a child view controller in this VC. * * @param viewController The new child VC. * @param container The view that will be used as the superview for the child VC's view. Defaults to this VC's [view]. * @param pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges. * Defaults to `true`. */ fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) { viewController.parent = this viewController.window = window _children.add(viewController) viewController.loadView() container.addSubview(viewController.view) if (pinEdges) { createConstraints { viewController.view.leftAnchor equalTo container.leftAnchor viewController.view.rightAnchor equalTo container.rightAnchor viewController.view.topAnchor equalTo container.topAnchor viewController.view.bottomAnchor equalTo container.bottomAnchor } } viewController.viewDidLoad() } /** * Removes the given view controller * * @param viewController The child VC to remove from this view controller. * @throws RuntimeException If the given [viewController] is not a child of this VC. */ fun removeChild(viewController: ViewController) { if (viewController.parent != this) { throw RuntimeException("Cannot remove child view controller whose parent is not this view controller") } viewController.parent = null _children.remove(viewController) } /** * Removes this view controller from its parent, if it has one. */ fun removeFromParent() { parent?.removeChild(this) view.removeFromSuperview() // todo: remove view from superview } }