LUMIERA.clone/src/gui/interact/ui-coord.hpp
Ichthyostega 0daeb02e4a UI-Coordinates/Navigator: identify misconception in the Builder
the original construction works only as long as we stick to the "classical" Builder syntax,
i.e. use chained calls of the builder functions. But as soon as we just invoke
some builder function for sake of the side-effect on the data within the builder,
this data is destroyed and moved out into the value return type, which unfortunately
is being thrown away right afterwards.

Thus: either make a builder really sideeffect-free, i.e. do each mutation
on a new copy (which is kind of inefficient and counterfeits the whole idea)
or just accept the side-effect and return only a reference.
In this case, we can still return a rvalue-Reference, since at the end
we want to move the product of the build process out into the destination.

This works only due to the C++ concept of sequence points, which ensures
the original object stays alive during the whole evaluation of such a chained
builder expression.

NOTE: the TreeMutator (in namespace lib::diff) also uses a similar Builder construction,
but in *that* case we really build a new product in each step and thus *must*
return a value object, otherwise the reference would already be dangling the
moment we leave the builder function.
2018-01-07 05:26:16 +01:00

701 lines
21 KiB
C++

/*
UI-COORD.hpp - generic topological location addressing scheme within the UI
Copyright (C) Lumiera.org
2017, Hermann Vosseler <Ichthyostega@web.de>
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.hpp
** A topological addressing scheme to designate structural locations within the UI.
** Contrary to screen pixel coordinates, we aim at a topological description of the UI structure.
** This foundation allows us
** - to refer to some "place" or "space" within the interface
** - to remember and return to such a location
** - to move a work focus structurally within the UI
** - to describe and configure the pattern view access and arrangement
**
** As starting point, we'll pick the notion of an access path within a hierarchical structure
** - the top-level window
** - the perspective used within that window
** - the panel within this window
** - a view group within the panel
** - plus a locally defined access path further down to the actual UI element
**
** @todo WIP 9/2017 first draft ////////////////////////////////////////////////////////////////////////////TICKET #1106 generic UI coordinate system
**
** @note UICoord is designed with immutability in mind; possibly we may decide to disallow assignment.
**
** @see UICoord_test
** @see id-scheme.hpp
** @see view-spec-dsl.hpp
** @see view-locator.hpp
*/
#ifndef GUI_INTERACT_UI_COORD_H
#define GUI_INTERACT_UI_COORD_H
#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "lib/path-array.hpp"
#include "lib/util.hpp"
#include <cstring>
#include <string>
#include <vector>
#include <utility>
namespace gui {
namespace interact {
namespace error = lumiera::error;
using std::string;
using lib::Literal;
using lib::Symbol;
using util::unConst;
using util::isnil;
using util::min;
enum {
UIC_INLINE_SIZE = 8
};
/* === predefined DSL symbols === */
enum UIPathElm
{
UIC_WINDOW,
UIC_PERSP,
UIC_PANEL,
UIC_VIEW,
UIC_TAB,
UIC_PATH
};
extern Symbol UIC_CURRENT_WINDOW; ///< window spec to refer to the _current window_ @see view-locator.cpp
extern Symbol UIC_FIRST_WINDOW; ///< window spec to refer to the _first window_ of the application
extern Symbol UIC_ELIDED; ///< indicate that a component is elided or irrelevant here
/**
* Describe a location within the UI through structural/topological coordinates.
* A UICoord specification is a sequence of Literal tokens, elaborating a path descending
* through the hierarchy of UI elements down to the specific UI element to refer.
* @see UICoord_test
*/
class UICoord
: public lib::PathArray<UIC_INLINE_SIZE>
{
public:
/**
* UI-Coordinates can be created explicitly by specifying a sequence of Literal tokens,
* which will be used to initialise and then normalise the underlying PathArray.
* @warning Literal means _"literal"_ with guaranteed storage during the whole execution.
* @remarks - in case you need to construct some part, then use \ref Symbol to _intern_
* the resulting string into the global static SymbolTable.
* - usually the Builder API leads to more readable definitions,
* explicitly indicating the meaning of the coordinate's parts.
*/
template<typename...ARGS>
explicit
UICoord (ARGS&& ...args) : PathArray(std::forward<ARGS> (args)...) { }
UICoord (UICoord&&) = default;
UICoord (UICoord const&) = default;
UICoord (UICoord& o) : UICoord((UICoord const&)o) { }
UICoord& operator= (UICoord const&) = default;
UICoord& operator= (UICoord &&) = default;
/* === Builder API === */
class Builder;
/** shortcut to allow init from builder expression */
UICoord (Builder&& builder);
/** Builder: start definition of UI-Coordinates rooted in the `firstWindow` */
static Builder firstWindow();
/** Builder: start definition of UI-Coordinates rooted in the `currentWindow` */
static Builder currentWindow();
/** Builder: start definition of UI-Coordinates rooted in given window */
static Builder window (Literal windowID);
//----- convenience shortcuts to start a copy-builder....
Builder persp (Literal perspectiveID) const;
Builder panel (Literal panelID)const;
Builder view (Literal viewID) const;
Builder tab (Literal tabID) const;
Builder tab (uint tabIdx) const;
Builder noTab () const;
//----- convenience shortcuts to start mutation on a copy...
Builder path (Literal pathDefinition) const;
Builder append (Literal elmID) const;
Builder prepend (Literal elmID) const;
/* === named component access === */
Literal getWindow() const { return accesComponent (UIC_WINDOW);}
Literal getPersp() const { return accesComponent (UIC_PERSP); }
Literal getPanel() const { return accesComponent (UIC_PANEL); }
Literal getView() const { return accesComponent (UIC_VIEW); }
Literal getTab() const { return accesComponent (UIC_TAB); }
/* === query functions === */
/**
* @remark _incomplete_ UI-Coordinates have some fragment of the path
* defined, but lacks the definition of an anchor point,
* i.e. it has no window ID
*/
bool
isIncomplete() const
{
return not empty()
and isnil (getWindow());
}
bool
isComplete() const
{
return not empty()
and not isnil (getWindow());
}
/**
* @remark an _explicit_ coordinate spec does not use wildcards
* and is anchored in a window spec
*/
bool
isExplicit() const
{
return isComplete()
and not util::contains (*this, Symbol::ANY);
}
bool
isPresent (size_t idx) const
{
Literal* elm = unConst(this)->getPosition(idx);
return not isnil(elm)
and *elm != Symbol::ANY;
}
bool
isWildcard (size_t idx) const
{
Literal* elm = unConst(this)->getPosition(idx);
return elm
and *elm == Symbol::ANY;
}
/**
* Check if this coordinate spec can be seen as an extension
* of the given parent coordinates and thus reaches further down
* towards specific UI elements in comparison to the parent path
* This constitutes a _partial order_, since some paths might just
* be totally unrelated to each other and thus not comparable.
* @note we tolerate (but not demand) expansion/interpolation
* of the given parent, i.e. parent may be incomplete
* or contain `'*'` placeholders.
* @todo 10/2017 have to verify suitability of this definition
*/
bool
isExtendedBelow (UICoord const& parent) const
{
size_t subSiz = this->size(),
parSiz = parent.size(),
idx = 0;
if (parSiz >= subSiz)
return false;
while (idx < parSiz
and ( (*this)[idx]== parent[idx]
or Symbol::ANY == parent[idx]
or isnil (parent[idx])))
++idx;
ENSURE (idx < subSiz);
return idx == parSiz;
} // meaning: this goes further down
/* === String representation === */
operator string() const
{
if (isnil (*this))
return "UI:?";
string component = getComp();
string path = getPath();
if (isnil (component))
return "UI:?/" + path;
if (isnil (path))
return "UI:" + component;
else
return "UI:" + component + "/" + path;
}
string
getComp() const
{
if (empty()) return "";
size_t end = min (size(), UIC_PATH);
size_t pos = indexOf (*begin());
if (pos >= end)
return ""; // empty or path information only
string buff;
buff.reserve(80);
if (0 < pos) // incomplete UI-Coordinates (not anchored)
buff += "?";
for ( ; pos<end; ++pos )
switch (pos) {
case UIC_WINDOW:
buff += getWindow();
break;
case UIC_PERSP:
buff += "["+getPersp()+"]";
break;
case UIC_PANEL:
buff += "-"+getPanel();
break;
case UIC_VIEW:
buff += "."+getView();
break;
case UIC_TAB:
if (UIC_ELIDED != getTab())
buff += "."+getTab();
break;
default:
NOTREACHED ("component index numbering broken");
}
return buff;
}
string
getPath() const
{
size_t siz = size();
if (siz <= UIC_PATH)
return ""; // no path information
string buff; // heuristic pre-allocation
buff.reserve (10 * (siz - UIC_PATH));
iterator elm = pathSeq();
if (isnil (*elm))
{ // irregular case : only a path fragment
elm = this->begin();
buff += "?/";
}
for ( ; elm; ++elm )
buff += *elm + "/";
// chop off last delimiter
size_t len = buff.length();
ASSERT (len >= 1);
buff.resize(len-1);
return buff;
}
/** iterative access to the path sequence section */
iterator
pathSeq() const
{
return size()<= UIC_PATH? end()
: iterator{this, unConst(this)->getPosition(UIC_PATH)};
}
private:
/** @note Builder allowed to manipulate stored data */
friend class Builder;
size_t
findStartIdx() const
{
REQUIRE (not empty());
return indexOf (*begin());
}
Literal
accesComponent (UIPathElm idx) const
{
Literal* elm = unConst(this)->getPosition(idx);
return elm? *elm : Symbol::EMPTY;
}
void
setComponent (size_t idx, Literal newContent)
{
Literal* storage = expandPosition (idx);
setContent (storage, newContent);
}
/** replace / overwrite existing content starting at given index.
* @param idx where to start adding content; storage will be expanded to accommodate
* @param newContent either a single element, or several elements delimited by `'/'`
* @note - a path sequence will be split at `'/'` and the components _interned_
* - any excess elements will be cleared
* @warning need to invoke PathArray::normalise() afterwards
*/
void
setTailSequence (size_t idx, Literal newContent)
{
std::vector<Literal> elms;
if (not isnil (newContent))
{
if (not std::strchr (newContent, '/'))
{
// single element: just place it as-is
// and remove any further content behind
elms.emplace_back (newContent);
}
else
{ // it is actually a sequence of elements,
// which need to be split first, and then
// interned into the global symbol table
string sequence{newContent};
size_t pos = 0;
size_t last = 0;
while (string::npos != (last = sequence.find ('/', pos)))
{
elms.emplace_back (Symbol{sequence.substr(pos, last - pos)});
pos = last + 1; // delimiter stripped
}
sequence = sequence.substr(pos);
if (not isnil (sequence))
elms.emplace_back (Symbol{sequence});
} }
setTailSequence (idx, elms);
}
/** replace the existing path information with the given elements
* @note - storage will possibly be expanded to accommodate
* - the individual path elements are required to persist
* - any excess elements will be cleared
* - the pathElms can be _empty_ in which case just
* any content starting from `idx` will be cleared
* @warning need to invoke PathArray::normalise() afterwards
*/
void
setTailSequence (size_t idx, std::vector<Literal>& pathElms)
{
size_t cnt = pathElms.size();
expandPosition (idx + cnt); // preallocate
for (size_t i=0 ; i < cnt; ++i)
setContent (expandPosition(idx + i), pathElms[i]);
size_t end = size();
for (size_t i = idx+cnt; i<end; ++i)
setContent (expandPosition(i), nullptr);
}
public: /* ===== relational operators : equality and partial order ===== */
friend bool
operator== (UICoord const& l, UICoord const& r)
{
return static_cast<PathArray const&> (l) == static_cast<PathArray const&> (r);
}
friend bool
operator< (UICoord const& l, UICoord const& r)
{
return l.isExtendedBelow (r);
}
friend bool operator> (UICoord const& l, UICoord const& r) { return (r < l); }
friend bool operator<= (UICoord const& l, UICoord const& r) { return (l < r) or (l == r); }
friend bool operator>= (UICoord const& l, UICoord const& r) { return (r < l) or (l == r); }
friend bool operator!= (UICoord const& l, UICoord const& r) { return not (l == r); }
};
/* === Builder API === */
class UICoord::Builder
{
protected:
UICoord uic_;
template<typename...ARGS>
explicit
Builder (ARGS&& ...args) : uic_{std::forward<ARGS> (args)...} { }
Builder (UICoord && anonRef) : uic_{std::move(anonRef)} { }
Builder (UICoord const& base) : uic_{base} { }
Builder (Builder const&) = delete;
Builder& operator= (Builder const&) = delete;
Builder& operator= (Builder &&) = delete;
/** builder instances created by UICoord */
friend class UICoord;
public:
/** @remark moving a builder instance is acceptable */
Builder (Builder &&) = default;
/* == Builder functions == */
/** change UI coordinate spec to define it to be rooted within the given window
* @note this function allows to _undefine_ the window, thus creating an incomplete spec */
Builder&&
window (Literal windowID)
{
uic_.setComponent (UIC_WINDOW, windowID);
return std::move (*this);
}
/** augment UI coordinates to mandate a specific perspective to be active within the window */
Builder&&
persp (Literal perspectiveID)
{
uic_.setComponent (UIC_PERSP, perspectiveID);
return std::move (*this);
}
/** augment UI coordinates to indicate a specific view to be used */
Builder&&
panel (Literal panelID)
{
uic_.setComponent (UIC_PANEL, panelID);
return std::move (*this);
}
/** augment UI coordinates to indicate a specific view to be used */
Builder&&
view (Literal viewID)
{
uic_.setComponent (UIC_VIEW, viewID);
return std::move (*this);
}
/** augment UI coordinates to indicate a specific tab within the view" */
Builder&&
tab (Literal tabID)
{
uic_.setComponent (UIC_TAB, tabID);
return std::move (*this);
}
/** augment UI coordinates to indicate a tab specified by index number */
Builder&&
tab (uint tabIdx)
{
uic_.setComponent (UIC_TAB, Symbol{"#"+util::toString (tabIdx)});
return std::move (*this);
}
/** augment UI coordinates to indicate that no tab specification is necessary
* @remarks typically this happens when a panel just holds a simple view */
Builder&&
noTab()
{
uic_.setComponent (UIC_TAB, UIC_ELIDED);
return std::move (*this);
}
/** augment UI coordinates by appending a further component at the end.
* @note the element might define a sequence of components separated by `'/'`,
* in which case several elements will be appended.
*/
Builder&&
append (Literal elm)
{
if (not isnil(elm))
uic_.setTailSequence (uic_.size(), elm);
return std::move (*this);
}
/** augment partially defined UI coordinates by extending them towards the root */
Builder&&
prepend (Literal elmID)
{
if (not uic_.isIncomplete())
throw error::Logic ("Attempt to prepend "+elmID
+" to the complete rooted path "+string(uic_));
uic_.setComponent (uic_.findStartIdx() - 1, elmID);
return std::move (*this);
}
/**
* augment UI coordinates to define a complete local path
* @param pathDef a path, possibly with multiple components separated by `'/'`
* @note any existing path definition is completely replaced by the new path
*/
Builder&&
path (Literal pathDef)
{
uic_.setTailSequence (UIC_PATH, pathDef);
return std::move (*this);
}
/** possibly shorten this path specification to a limited depth */
Builder&&
truncateTo (size_t depth)
{
uic_.truncateTo (depth);
return std::move (*this);
}
};
/**
* @note this ctor is used to "fix" and normalise
* the contents established in the Builder thus far.
*/
inline
UICoord::UICoord (Builder&& builder)
: UICoord{std::move (builder.uic_)}
{
PathArray::normalise();
}
/** @return a minimally defined Builder, allowing to define further parts;
* to finish the definition, cast / store it into UICoord, which itself is immutable.
*/
inline UICoord::Builder
UICoord::currentWindow()
{
return window (UIC_CURRENT_WINDOW);
}
inline UICoord::Builder
UICoord::firstWindow()
{
return window (UIC_FIRST_WINDOW);
}
/** @return a Builder with just the windowID defined */
inline UICoord::Builder
UICoord::window (Literal windowID)
{
return Builder{windowID};
}
/** @return a Builder holding a clone copy of the original UICoord,
* with the perspective information set to a new value.
* @remarks This Builder can then be used do set further parts
* independently of the original. When done, store it
* as new UICoord object. To achieve real mutation,
* assign it to the original variable. */
inline UICoord::Builder
UICoord::persp (Literal perspectiveID) const
{
return Builder(*this).persp (perspectiveID);
}
inline UICoord::Builder
UICoord::panel (Literal panelID) const
{
return Builder(*this).panel (panelID);
}
inline UICoord::Builder
UICoord::view (Literal viewID) const
{
return Builder(*this).view (viewID);
}
inline UICoord::Builder
UICoord::tab (Literal tabID) const
{
return Builder(*this).tab (tabID);
}
inline UICoord::Builder
UICoord::tab (uint tabIdx) const
{
return Builder(*this).tab (tabIdx);
}
inline UICoord::Builder
UICoord::noTab () const
{
return Builder(*this).noTab();
}
/**
* convenience builder function so set a full path definition
* @note the given path string will be split at `'/'` and the
* resulting components will be stored/retrieved as Symbol
*/
inline UICoord::Builder
UICoord::path (Literal pathDefinition) const
{
return Builder(*this).path (pathDefinition);
}
inline UICoord::Builder
UICoord::append (Literal elmID) const
{
return Builder(*this).append (elmID);
}
inline UICoord::Builder
UICoord::prepend (Literal elmID) const
{
return Builder(*this).prepend (elmID);
}
}}// namespace gui::interact
#endif /*GUI_INTERACT_UI_COORD_H*/