/* UI-COORD-RESOLVER.hpp - resolve UI coordinate spec against actual window topology Copyright (C) Lumiera.org 2017, Hermann Vosseler This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ /** @file ui-coord-resolver.hpp ** Evaluation of UI coordinates against a concrete window topology. ** [UI-Coordinates](\ref UICoord) allow to describe and locate an interface component within the ** Lumiera GUI through a topological access path. As such these coordinate specifications are abstract, ** and need to be related, attached or resolved against the actual configuration of widgets in the UI. ** Through this relation it becomes possible to pose meaningful queries over these coordinates, or to ** build, rebuild and remould a coordinate specification. ** ** We need to avoid tainting with the intrinsics of the actual UI toolkit though -- which indicates ** the UICoordResolver should be designed as an abstract intermediary, built on top of a command and ** query interface, provided by the \ref Navigator and backed by the actual UI configuration. ** ** # Abstraction ** ** The abstraction used to found this interface is twofold. For one, we rely on the notion of logical, ** topological [Coordinates in User Interface space](\ref UICoord). And secondly, we rely on a very ** limited form of navigation: we navigate a tree-shaped (abstracted) structure just by ** - iteration over siblings, which are children of our previous starting point ** - the ability, _on this iterator,_ to expand the "current child" and inject ** the next level of child iteration at its place, similar to the `flatMap` ** operation known from functional programming. ** Together, these two capabilities allow us to build exploring and backtracking evaluations, ** which is enough to build a secondary helper component on top, the gui::interact::UICoordResolver ** ** @todo WIP 10/2017 first draft ////////////////////////////////////////////////////////////////////////////TICKET #1106 generic UI coordinate system ** ** @see UICoordResolver_test ** @see UICoord_test ** @see navigator.hpp ** @see view-locator.hpp */ #ifndef GUI_INTERACT_UI_COORD_RESOLVER_H #define GUI_INTERACT_UI_COORD_RESOLVER_H #include "lib/error.hpp" #include "lib/symbol.hpp" #include "lib/format-string.hpp" #include "gui/interact/ui-coord.hpp" #include "lib/iter-tree-explorer.hpp" #include "lib/iter-source.hpp" #include "lib/util.hpp" #include #include namespace gui { namespace interact { namespace error = lumiera::error; using std::unique_ptr; using util::unConst; using lib::Literal; using lib::Symbol; /** * Interface to locate and move within a tree shaped structure. * The actual nature of this structure is to be kept abstracted through this interface. * The purpose of this construct is to build evaluations and matching operations on top. */ class TreeStructureNavigator : public lib::IterSource { public: virtual ~TreeStructureNavigator(); ///< this is an interface /** expand into exploration of child elements at "current position". * At any point, a TreeStructureNavicator instance indicates and represents a position * within a tree-like structure. At the same time, it is part of a sequence of siblings, * which is accessible through iteration. This operation now allows to extend visitation * of siblings by consuming the current element and replacing it with the sequence of its * immediate child elements, exposing the first one as the _"current position"_. * @return pointer to a new heap allocated TreeStructureNavigator implementation, which * represents the sequence of children. The object `this` will not be affected. * @note it is the caller's responsibility to own and manage the generated navigator. * The typical (and recommended) way to achieve this is to rely on the embedded * type #iterator, which exposes an appropriately wired iterator::expandChildren() */ virtual TreeStructureNavigator* expandChildren() const =0; /** build a Lumiera Forward Iterator as front-end and managing Handle for a TreeStructureNavigator * or subclass. The provided pointer is assumed to point to heap allocated storage. * @return copyable iterator front-end handle, which allows to retrieve once all values * yielded by this IterSource. The front-end _takes ownership_ of the given object. * @note the generated iterator is preconfigured to allow for _"child expansion"_, thereby * calling through the virtual API function expandChildren() */ static auto buildIterator (TreeStructureNavigator* source) { return lib::treeExplore (source) .expand([](TreeStructureNavigator& parent){ return parent.expandChildren(); }); } }; /** * Interface to discover a backing structure for the purpose of path navigation and resolution. * UICoord are meant to designate a position within the logical structure of an UI -- yet in fact * they may be resolved against any tree-like topological structure, which can be queried through * this interface. * @see Navigator the implementation used in the Lumiera UI, as backed by actual GUI components */ class LocationQuery { public: virtual ~LocationQuery(); ///< this is an interface using ChildIter = decltype (TreeStructureNavigator::buildIterator(0)); /** make the real anchor point explicit. * @param path an explicit UICoord spec to be anchored in the actual UI * @return _explicit_ literal window name, where the path can be anchored * Symbol::BOTTOM in case the given path can not be anchored currently. * @remark here "to anchor" means to match and thus "attach" the starting point * of the UIcoord path, i.e. the window spec, with an actual top-level * window existing in the current UI configuration and state. This operation * either confirms existence of a window given by explicit ID, or it supplies * the current meaning of the meta specs `currentWindow` and `firstWindow`, * again in the form of an explicit window name */ virtual Literal determineAnchor (UICoord const& path) =0; /** evaluate to what extent a UIcoord spec matches the actual UI * @return the depth to which the given spec is _"covered"_ by the actual UI. * Can be zero, in which case the given coordinates can not be resolved * and addressed within the currently existing windows, panes and views. * @remark a depth > 0 also implies that the path can be _anchored._ * @note this operation does not perform any _resolution_ or interpolation of wildcards, * it just matches explicit UI component names. * @see UICoordResolver for a facility to perform such a resolution and to navigate paths. */ virtual size_t determineCoverage (UICoord const& path) =0; /** get the sequence of child components at a designated position in the actual UI * @param path an explicit UIcoord spec, expected to be anchored and at least partially * covered within the current configuration and state of the UI * @param pos depth where the given path shall be evaluated, starting with 0 at window level * @return an iterator to enumerate all child components actually existing in the current * UI below the location designated by path and pos. * @remark the path is only evaluated up to (not including) the given depth. Especially * when `pos == 0`, then the path is not evaluated and matched at all, rather * just the current list of top-level windows is returned. * @throw error::State when navigating the given path touches a non-existing element */ virtual ChildIter getChildren (UICoord const& path, size_t pos) =0; }; /** * Query and mutate UICoord specifications in relation to actual UI topology. * * @todo initial draft as of 10/2017 */ class UICoordResolver : public UICoord::Builder { struct Resolution { const char* anchor = nullptr; size_t depth = 0; unique_ptr covfefe{}; bool isResolved = false; }; LocationQuery& query_; Resolution res_; public: UICoordResolver (UICoord const& uic, LocationQuery& queryAPI) : Builder{uic} , query_{queryAPI} , res_{} { attempt_trivialResolution(); } UICoordResolver (UICoord && uic, LocationQuery& queryAPI) : Builder{std::move(uic)} , query_{queryAPI} , res_{} { attempt_trivialResolution(); } /* === query functions === */ /** is this path explicitly anchored at an existing window? * @remarks this also implies the path is complete and explicit (no wildcards). */ bool isAnchored() const { return res_.anchor and res_.anchor != Symbol::BOTTOM; } /** determine if a mutation is possible to anchor the path explicitly * @remarks basically this either means the path isAnchored(), or we're able * to calculate a path resolution, interpolating any wildcards. * And while the path resolution as such might fail, it was at least * successful to determine an anchor point. The existence of such an * anchor point implies the path is not totally in contradiction to the * existing UI */ bool canAnchor() const { return isAnchored() or (res_.isResolved and res_.covfefe) or unConst(this)->pathResolution() or isAnchored(); // resolution failed, but computed at least an anchor } /** is this path at least _partially_ covered? * A covered path describes an access path through widgets actually existing in the UI. * @remark this also implies the path is anchored, complete and explicit. * @note this predicate tests for _partial_ coverage, which means, there might * be some extraneous suffix in this path descending beyond existing UI */ bool isCoveredPartially() const { return res_.isResolved and res_.depth > 0; } /** this path is completely covered by the currently existing UI structure; * @remark there is no extraneous uncovered suffix in this path spec; * moreover, the path is anchored, complete and explicit */ bool isCovered() const { return res_.isResolved and res_.depth == this->uic_.size(); } /** determine if a mutation is possible to get the path (partially) covered. * @remarks in order to be successful, a path resolution must interpolate any gaps in the * path spec _and_ reach a point behind / below the gap (wildcards), where an existing * explicitly stated component in the path can be confirmed (covered) by the existing UI. * The idea behind this definition is that we do not want just some interpolated wildcards, * rather we really want to _confirm_ the essence of the path specification. Yet we accept * an extraneous suffix _in the explicitly given part_ of the path spec to extend beyond or * below what exists currently within the UI. */ bool canCover() const { return isCovered() // either explicit coverage known or (res_.isResolved and res_.covfefe) // or previous matching run found (partial) solution or unConst(this)->pathResolution() // perform matching run now to find total coverage or (res_.covfefe); // or at least partial coverage was found } /* === mutation functions === */ /** mutate the path to get it totally covered * - make the anchorage explicit * - possibly match and expand any wildcards * - then truncate the UI-Coordinate spec to that part * actually covered by the UI * @note if the coordinate spec can not be covered at all, * it will be truncated to zero size */ UICoordResolver&& cover() { if (isCoveredPartially() and not res_.covfefe) { ASSERT (res_.anchor); // depth > 0 implies anchorage window (res_.anchor); // thus make this anchor explicit truncateTo (res_.depth); } else if (canCover()) { ASSERT (res_.isResolved); REQUIRE (res_.covfefe); res_.depth = res_.covfefe->size(); this->uic_ = std::move (*res_.covfefe); res_.covfefe.reset(); } else { ASSERT (res_.isResolved); REQUIRE (res_.depth == 0); REQUIRE (not res_.covfefe); truncateTo (0); } ENSURE (isCovered()); return std::move (*this); } /** mutate the window part of the path such as * to make the anchorage explicit, if possible * @remark if the path starts with meta specs like * `firstWindow` or `currentWindow`, they will be * replaced by their current meaning. If the path * is incomplete, but can somehow be resolved, we * use the anchorage as indicated by that resolution, * without interpolating the rest of the path. */ UICoordResolver&& anchor() { if (canAnchor()) { window (res_.anchor); normalise(); } return std::move (*this); } /** mutate the path to extend it while keeping it partially covered */ UICoordResolver&& extend (Literal pathExtension) { if (not isCovered()) cover(); ENSURE (isCovered()); append (pathExtension); res_.depth = query_.determineCoverage (this->uic_); // coverage may grow return std::move (*this); } UICoordResolver&& extend (UICoord const& partialExtensionSpec) { if (not canCover()) uic_ = partialExtensionSpec; else { REQUIRE (res_.isResolved); size_t coverable = res_.covfefe? res_.covfefe->size() : res_.depth; auto newContent = partialExtensionSpec.begin(); size_t extensionPos = newContent? partialExtensionSpec.indexOf(*newContent) : 0; if (coverable >= extensionPos) throw error::Invalid (util::_Fmt{"Attempt to extend covered path %s with %s " "would overwrite positions %d to %d (incl)"} % (res_.covfefe? *res_.covfefe : UICoord{uic_.rebuild().truncateTo(res_.depth)}) % partialExtensionSpec % extensionPos % coverable); cover(); for ( ; newContent; ++newContent, ++extensionPos ) overwrite (extensionPos, *newContent); normalise(); } res_ = Resolution{}; // start over with pristine resolution state attempt_trivialResolution(); canCover(); return std::move (*this); } /** diagnostics */ operator string() const { return string(this->uic_); } size_t coverDepth() const { return res_.depth; } private: /** establish a trivial anchorage and coverage, if possible. * @note when the UICoord contains wildcards or is incomplete, * a full resolution with backtracking is necessary to * determine anchorage and coverage */ void attempt_trivialResolution() { res_.anchor = query_.determineAnchor (this->uic_); if (not uic_.isExplicit()) return; res_.depth = query_.determineCoverage(this->uic_); res_.isResolved = true; } /** @internal algorithm to resolve this UICoord path against the actual UI topology. * @return true if total coverage is possibly (by interpolating wildcards) * @remark after invoking this function, res_.isResolved and possible coverage are set. */ bool pathResolution(); }; }}// namespace gui::interact #endif /*GUI_INTERACT_UI_COORD_RESOLVER_H*/