LUMIERA.clone/tests/stage/gen-node-location-query.hpp

299 lines
12 KiB
C++
Raw Permalink Normal View History

/*
GEN-NODE-LOCATION-QUERY.hpp - pose UI-coordinate location queries against a GenNode structure
Copyright: clarify and simplify the file headers * Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
Copyright (C)
2017, Hermann Vosseler <Ichthyostega@web.de>
Copyright: clarify and simplify the file headers * Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
  **Lumiera** 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. See the file COPYING for further details.
*/
/** @file gen-node-location-query.hpp
** Implementation of the stage::interact::LocationQuery interface to work on a GenNode tree.
** The interface allows to pose queries against a concrete structure to verify and reshape some
** [UI Coordinate specification](\ref UICoord); basically it offers methods to navigate within a
** tree-like structure. While in the actual implementation, such a query interface would be backed
** by navigating real UI structures, the implementation given here instead uses a generic tree structure
** given as `Record<GenNode>`.
**
** # Representing UI structure as GenNode tree
**
** While basically the interface LocationQuery abstracts and reduces the structure of an UI into
** just some hierarchically arranged and nested IDs, we should note some specific twists how a GenNode
** tree is used here to represent the structure elements as defined through [UI coordinates](\ref UICoord):
** - we use the special _type_ attribute to represent the _perspective_ within each window;
** deliberately, we'll use this twisted structure here to highlight the fact that the
** backing structure need not be homogeneous; rather, it may require explicit branching
** - we use the _attributes_ within the GenNode "object" representation, since these are _named_
** nested elements, and the whole notion of an UI coordinate path is based on named child components
** - relying upon the [object builder notation](\ref Record::Mutator), it is possible to define a whole
** structure as nested inline tree; named nested elements can be added with the `set(key, val)`
** builder function, and for each nested scope, we start a new nested builder with `MakeRec()`.
** - since GenNodeLocationQuery is conceived for writing test and verification code, there is a
** special convention to set the `currentWindow` to be the last one in list -- in a real UI
** this would not of course not be a configurable property of the LocationQuery, and rather
** just reflect the transient window state and return the currently activated window
**
** @todo WIP 10/2017 started in the effort of shaping the LoactionQuery interface, and used
** to support writing unit tests, to verify the UiCoordResolver. It remains to be seen
** if this implementation can be used beyond this limited purpose
**
** @see UICoordResolver_test
** @see IterExplorer_test
** @see ui-coord-resolver.hpp
** @see navigator.hpp
*/
#ifndef STAGE_INTERACT_GEN_NODE_LOCATION_QUERY_H
#define STAGE_INTERACT_GEN_NODE_LOCATION_QUERY_H
#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "stage/interact/ui-coord-resolver.hpp"
#include "lib/diff/gen-node.hpp"
#include "lib/format-string.hpp"
#include "lib/iter-explorer.hpp"
#include "lib/iter-source.hpp"
#include "lib/itertools.hpp"
#include "lib/util.hpp"
#include <utility>
#include <string>
namespace stage {
namespace interact {
namespace error = lumiera::error;
using lib::Symbol;
using lib::Literal;
using lib::diff::Rec;
using util::isnil;
using util::_Fmt;
using std::forward;
using std::string;
/**
* Test/Diagnostics: implementation of the LocationQuery-API
* based on a abstract topological structure given as Record<GenNode> ("GenNode tree").
* @remark intended for verifying path resolution and navigation through unit tests
*/
class GenNodeLocationQuery
: public LocationQuery
{
Rec tree_;
public:
template<class REC>
GenNodeLocationQuery(REC&& backingStructure)
: tree_(std::forward<REC>(backingStructure))
{ }
/* === LocationQuery interface === */
/** resolve Anchor against GenNode tree */
virtual Literal
determineAnchor (UICoord const& path) override
{
if (isnil(tree_) or not path.isPresent(UIC_WINDOW))
return Symbol::BOTTOM;
if (UIC_FIRST_WINDOW == path.getWindow())
return getFirstWindow();
if (UIC_CURRENT_WINDOW == path.getWindow())
return getCurrentWindow();
if (not tree_.hasAttribute(string{path.getWindow()})) /////////////////////////////////////TICKET #1113 : unnecessary repackaging of a Literal into string when GenNode ID is based on Literal
return Symbol::BOTTOM;
return path.getWindow();
}
/** evaluate to what extent a UIcoord spec matches the structure given as GenNode tree */
virtual size_t
determineCoverage (UICoord const& path) override
{
size_t depth = 0;
drillDown (tree_, path, path.size(), depth);
return depth;
}
/** get the sequence of child IDs at a designated position in the backing GenNode tree */
virtual ChildIter
getChildren (UICoord const& path, size_t pos) override
{
size_t depth = 0;
Rec const& node = drillDown (tree_, path, pos, depth);
2017-10-28 02:06:05 +02:00
if (depth != pos)
throw error::State(_Fmt{"unable to drill down to depth %d: "
"element %s at pos %d in path %s is in "
"contradiction to actual UI structure"}
% pos
% (depth<path.size()? path[depth] : Symbol::BOTTOM)
% depth
% path
);
return TreeStructureNavigator::buildIterator(
childNavigator (node, depth));
}
private:
Literal
getFirstWindow()
{
return Symbol{*tree_.keys()}; //////////////////////////////////////////////TICKET #1113 : warning use of Symbol table becomes obsolete when EntryID relies on Literal
}
Literal
getCurrentWindow()
{
return Symbol{lib::pull_last (tree_.keys())}; //////////////////////////////////////////////TICKET #1113 : warning use of Symbol table becomes obsolete when EntryID relies on Literal
} // special convention for unit-tests
2017-10-28 02:06:05 +02:00
Literal
resolveElm (UICoord const& path, size_t depth)
{
REQUIRE (path.isPresent(depth));
return depth==UIC_WINDOW? GenNodeLocationQuery::determineAnchor(path)
: path[depth];
}
2017-10-28 02:06:05 +02:00
Rec const&
drillDown (Rec const& tree, UICoord const& path, size_t maxDepth, size_t& depth)
{
if (depth<maxDepth and path.isPresent(depth))
{
CStr pathElm = resolveElm (path, depth);
2017-10-28 02:06:05 +02:00
if (hasNode (tree, pathElm, depth))
{
++depth;
return drillDown (descendInto(tree,depth-1,pathElm), path, maxDepth, depth);
}
}
return tree;
}
/** does the guiding tree contain the element as requested by the UICoord path?
* @remark this function abstracts a special asymmetry of the tree representation:
2017-10-28 02:06:05 +02:00
* at `level==UIC_PERSP` (the second level), the perspective info is packed into
* the type meta attribute. This was done on purpose, to verify our design is able
* to handle such implementation intricacies, which we expect to encounter
* when navigating the widgets of a real-world UI toolkit set
*/
static bool
hasNode (Rec const& tree, CStr pathElm, size_t depth)
{
return depth==UIC_PERSP? pathElm == tree.getType()
: tree.hasAttribute(pathElm);
}
/** within `tree` _at level_ `depth` descend into the child element designated by `pathElm` */
static Rec const&
descendInto (Rec const& tree, size_t depth, CStr pathElm)
{
return depth==UIC_PERSP? tree // perspective info is attached as type at the parent node
: tree.get(pathElm).data.get<Rec>();
}
/* ==== iterate over siblings with the ability to expand one node's children ==== */
/** @return a heap allocated object attached at "current tree position" while
* exposing the names of all child nodes [through iteration](\ref lib::IterSource)
*/
static TreeStructureNavigator*
childNavigator (Rec const& node, size_t depth)
{
//////////////////////////////////////////////////////////////////////////TICKET #1113 : capturing the string into the global Symbol table becomes obsolete, once GenNode exposes Literal as ID
auto internedString = [](string const& id) -> Literal
{
return Symbol{id};
};
return depth==UIC_PERSP? buildNavigator (node, depth, singleValIterator (internedString (node.getType())))
: buildNavigator (node, depth, transformIterator (node.keys(), internedString));
}
/**
* Helper to navigate a tree topology represented as GenNode tree.
* Basically this is a lib::IterSource<Literal> to encapsulate a sequence of sibling nodes.
* A "current element" representation is layered on top to allow to expand one level deeper
* on demand. This "child expansion" is triggered by invoking the `expandChildren()` function
* on the iterator front-end provided as LocationQuery::ChildIter
*/
template<class PAR>
class GenNodeNavigator
: public PAR
{
Rec const& pos_;
size_t depth_;
virtual TreeStructureNavigator*
expandChildren() const override
{
return childNavigator (descendInto (pos_,depth_, currentChild_), depth_+1);
}
///////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1125 : work around the misaligned IterSource design
// The design of IterSource attempts to be too clever, and we have to pay for it now...
// If IterSource would just work like a StateCore and expose the "current element" via API call,
// then we'd be able to retrieve the name of the current child node. Unfortunately it doesn't
// and thus we rig a "wire tap" here and capture the node names whenever an iteration happens.
//
Literal currentChild_ = Symbol::BOTTOM;
using Pos = PAR::Pos;
virtual Pos
firstResult () override
{
Pos pos = PAR::firstResult();
if (pos) currentChild_ = *pos;
return pos;
}
virtual void
nextResult(Pos& pos) override
{
PAR::nextResult (pos);
if (pos) currentChild_ = *pos;
}
///////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1125 : work around the misaligned IterSource design
public:
template<class IT>
GenNodeNavigator(Rec const& node, size_t depth, IT&& rawChildIter)
: PAR{forward<IT> (rawChildIter)}
, pos_{node}
, depth_{depth}
{ }
};
/** type rebinding helper to pick up the concrete child iterator type `IT` */
template<class IT>
static TreeStructureNavigator*
buildNavigator (Rec const& node, size_t depth, IT && rawIterator)
{
return new GenNodeNavigator<
lib::WrappedLumieraIter<IT,
TreeStructureNavigator>> {node, depth, forward<IT> (rawIterator)};
}
};
}}// namespace stage::interact
#endif /*STAGE_INTERACT_GEN_NODE_LOCATION_QUERY_H*/