LUMIERA.clone/src/gui/interact/ui-coord.hpp
Ichthyostega 7b2c98474f Navigator: initial draft of a UI coordinate resolver
...which in turn will drive the design of the LoactionQuery API
2017-10-16 02:39:22 +02:00

669 lines
20 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_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 `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 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 nut 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 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 will be _interned_ as Symbol
* - 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
{
UICoord uic_;
/** builder instances created by UICoord */
friend class UICoord;
protected:
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;
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
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);
}
};
/**
* @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 an empty 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);
}
/** @return aBuilder 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::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*/