diff --git a/data/config/setup.ini b/data/config/setup.ini index aaa0e2f2a..bffb29e92 100644 --- a/data/config/setup.ini +++ b/data/config/setup.ini @@ -18,6 +18,7 @@ authors = Joel Holdsworth|Christian Thäter|Hermann Voßeler|Stefan Kangas| copyright = 2007 - 2015 [Gui] -stylesheet = gtk-lumiera.css +#stylesheet = lumiera.css +stylesheet = lumiera-light-theme-complement.css iconpath = $ORIGIN/../../share/lumiera/icons:$ORIGIN/gui/icons:~/.lumiera/icons resourcepath = $ORIGIN/../../share/lumiera/gui:$ORIGIN/gui diff --git a/doc/design/architecture/Subsystems.txt b/doc/design/architecture/Subsystems.txt index f57d2d85b..49e833dd7 100644 --- a/doc/design/architecture/Subsystems.txt +++ b/doc/design/architecture/Subsystems.txt @@ -164,7 +164,7 @@ test and better to reason about. If it really can not be avoided to use such uni static context, it should at least be packaged as a single object, plus we should ensure this logic can only be accessed through a regular (non static) object as front-end. Packaged this way, the most common and dangerous pitfalls with statics can be avoided.] And since Lumiera indeed allows -for link:{ldoc}/technical/library/Dependencies.html[[lazily initialised dependencies], we +for link:{ldoc}/technical/library/Dependencies.html[lazily initialised dependencies], we establish the policy that *destructors must not rely on dependencies*. In fact, they should not do any tangible work at all, beyond releasing other resources. @@ -223,7 +223,7 @@ the event `ON_GLOBAL_INIT` is emitted. validation or auto-correction of the session's contents. `ON_SESSION_CLOSE`:: - This event indicates to commence any activity relying on an opened and fully operative session. + This event indicates to cease any activity relying on an opened and fully operative session. When invoked, the session is still in fully operative state, all interfaces are open and the render engine is available. However, after issuing this event, the session shutdown sequence will be initiated, by detaching the engine interfaces and signalling the scheduler to cease running render jobs. diff --git a/doc/technical/code/codingGuidelines.txt b/doc/technical/code/codingGuidelines.txt index 8ec17e34e..36b6ea44b 100644 --- a/doc/technical/code/codingGuidelines.txt +++ b/doc/technical/code/codingGuidelines.txt @@ -84,6 +84,10 @@ General Code Arrangement and Layout The primary author(s) and the year of the initial copyright claim should be mentioned. - Each header should be focused on a specific purpose. Preferably it starts with a file-level doxygen comment explaining the intention and anything not obvious from reading the code. + At lest a `@file` tag with one line of classification in a doxygen comment at the top of every + file is mandatory.footnote:[this rule stands simply because, without such a file-level doxygen + comment, doxygen will _ignore all contents_ of this file (really, might be surprising, yet it is + the way it is...)] - when arranging headers and compilation units, please take care of the compilation times and the code size. Avoid unnecessary includes. Use forward declarations where applicable. Yet still, _all immediately required direct dependencies should be mentioned_, even if already @@ -91,11 +95,20 @@ General Code Arrangement and Layout link:{ldoc}/technical/code/linkingStructure.html#_imports_and_import_order[issues of code organisation] - The include block starts with our own dependencies, followed by a second block with the library dependencies. After that, optionally some symbols may be brought into scope (through +using+ clauses). - Avoid cluttering top-level namespaces. Never import full namespaces (no +using namespace boost;+ please!) + Avoid cluttering top-level namespaces. Never import full namespaces.footnote:[no `using namespace gtk;` + or `using namespace boost` please! Experience shows, in the end you'll be using 5 names or so, but + pull in all the others just for sake of laziness. Just type the f**g `using` clause for every + import individually, and we'll all be better off...] - the includes for our own dependencies shall be given relative to source-root (or test root). Don't use relative includes for headers located in the same directory, or -- worse still -- in the parent directory. -- sometimes, the actual translation units will combine several facilities for technical reasons, e.g. - when sharing an implementation-level class or even singleton instance. +- sometimes, the actual translation units will combine several facilities for technical reasons.footnote: + [this means, there can be ``headers'', which are actually only be intended for inclusion on one or two + distinct places. This should be mentioned in the file-level comment, but generally is an acceptable + practice, and better then lumping everything into a 1000 lines header. As a guideline, if you expect + a rather technical concern not to be of much interest for most readers of a header, then better + extract it into a separate self-contained header and include it. E.g., you might be sharing an + an implementation-level class or even singleton instance and some constant definitions. Just + be sure not to include definitions several times.] Anonymous namespaces should be used liberally to avoid unnecessary exports. - template code mostly needs to reside in headers. (same for metaprogramming code). We employ the simple inclusion model (``Borland model'') for template instantiation. diff --git a/doc/technical/gui/CodePolicy.txt b/doc/technical/gui/CodePolicy.txt new file mode 100644 index 000000000..dbf547ad9 --- /dev/null +++ b/doc/technical/gui/CodePolicy.txt @@ -0,0 +1,94 @@ +Coding Policies within UI-Layer +=============================== +:Toc: + +NOTE: for the time being, this is a loose collection of + Conventions and Policies deemed adequate for work + on the Lumiera UI-Layer + + +Architecture +------------ + +The UI is loaded as Plug-In, which instantiates a `GtkLumiera` object. +All of the UI activity happens within the blocking function `GtkLumiera::run()`. +This UI activity boils down to run the GTK event loop; we use a dedicated thread, +and all of the GUI thus performs single-threaded and need not be thread-safe. +All UI activities thus need to be short running and with deterministic outcome. +It is strictly prohibited to spawn other threads from within the event loop. + +We hold up a strict distinction between the _UI mechanics_ and the _core editing +concerns_. The latter _must not be implemented within the UI-Layer._ Rather, you need +to send an `act(CommandID)` message over the UI-Bus, which causes the corresponding +Proc-Layer command script to be dispatched within the Session thread. It is good +practice to give an immediate visual clue regarding the fact of sending such +a command (e.g. pressing a button). But be prepared that the actual feedback +of invoking a command happens asynchronously. + +UI-Model +~~~~~~~~ +In short: _there is no dedicated UI-Model._ Stateful working data goes directly +into the widgets. However, some widgets or controllers are special, insofar they +correspond to and reflect some entities within the _Session model._ These special +entities must be implemented as subclasses of `gui::model::Tangible`. These are +always connected to the UI-Bus, and they are populated and mutated by receiving +diff messages pushed up from the Proc-Layer. + +There is an _inner circle_ of UI backbone services. These are all mutually dependent +on each other and their start-up sequence is intricate. Implement global and cross-cutting +concerns here. In case some widget or controller needs access to such a service, then you +should prefer using `lib::Depend`, and install the corresponding service +via `lib::DependInject::ServiceInstance`. + + +Behaviour patterns +------------------ + +Parent Containers +~~~~~~~~~~~~~~~~~ +A _Parent Container_ is an object managing a collection of children, with respect to +the structure of _tangible UI elements._ Irrespective if this container is also a +`Gtk::Container` widget, or ``just'' a controller. The key point to turn something +into a _Parent Container_ is the fact that this entity creates the children +in response to a (population) diff message. + +.Responsibilities +- a _Parent Container_ has to wire each child properly, so to enable the adequate + use of the UI element protocol. This includes + + ** to ensure the child is attached to the UI-Bus (usually enforced by ctor call) + ** to install a suitable `Expander` functor if the child supports expand/collapse + ** to install a suitable `Revealer` when it is relevant for the child to be brought + into sight in response to some message (e.g. to indicate error state). + +- whenever a child is detached from the container, you need to _ensure it is destroyed_ + right away, before there is any chance of processing some other UI event.footnote:[ + Invoking the destructor triggers a lot of magic here, especially it causes the child + to be properly detached from signals. We want to ensure this happens as soon as the + child is taken out of service. Do not use an ``can collect garbage later'' approach.] + +Error handling +~~~~~~~~~~~~~~ +Be aware that GTK is written in C. And while GTKmm has some safeguards in place, +better be sure no exception can emanate from event handling code. + +WARNING: [red]#TODO#: probably we'll need a common wrapper to do so... + + +Code organisation +----------------- +GTK is massive and compilation times tend to be problematic in the UI-Layer. +Thus you should be diligent with the organisation of includes. Try to confine the +more heavyweight includes within the implementation translation units. Use forward +declarations and PImpl if possible. However, it is fine to write a mere implementation +widget in header-only fashion, in case this improves the locality and readability of code. + +NOTE: `using namespace Gtk` and similar wildcard includes are prohibited. + +We are forced to observe a tricky include sequence, due to NoBug's `ERROR` macro, +and also in order to get I18N right. +Thus any actual translation unit should ensure that effectively the first include +is that of 'gui/gtk-base.hpp'.footnote:[you need not include it literally. It is +perfectly fine if you can be sure the first included header somehow drags in +'gtk-base.hpp' before any other header.] + diff --git a/doc/technical/gui/GuiConnect.txt b/doc/technical/gui/GuiConnect.txt new file mode 100644 index 000000000..84458a8fc --- /dev/null +++ b/doc/technical/gui/GuiConnect.txt @@ -0,0 +1,80 @@ +GUI Connection +============== + +The Lumiera application is built from several segregated software layers, and the user interface, the GUI-Layer is +optional and loaded as a plug-in. This deliberate design decision has far reaching consequences in all parts of the +application: Anything of relevance for editing or rendering need to be _represented explicitly_ within a model, distinct +and abstracted from the presentation for the user. For example, many media handling applications let you ``snap'' some +entity to a ``grid'', which happens conveniently during a given user interaction, just resulting in a new position for +the element at hand. In Lumiera, we can not follow this obvious and pragmatic approach -- rather, both the time grid +and the fact an object as been attached and aligned to such a grid need to be represented in the session model proper, +resulting in a `Placement`. More specifically, we can not derive behaviour of such an object from circumstantial +state data present in the UI. Rather, we need to compute the new position based on rules within the session. +And then we need to _communicate_ the result back into the UI -- if and when an UI is present to reflect that change. + +So instead of having a _»Controller«_ mediate the interplay of a _»Model«_ and the _»View«_ presentation, we end up +with a complete _communication cycle,_ where messages are sent in both directions, and _sent asynchronously._ + +Principles of UI / Core interaction +----------------------------------- +By all means, we avoid a common shared data structure as foundation for interaction. Such a _data centric_ approach +may lean towards two extremes, both of which are problematic. A _naively written application_ starts with building +the user interface and hinges any further internal structures directly below. For a tiny application, which is +basically ``just some interface controls'', such an approach might be adequate -- yet maintaining the ``code behind'' +the control surface quickly becomes a nightmare. We may avoid those immediate problems by settling down on a strictly +codified central _core data model._ And while this shift towards the other extreme of a data centric architecture +indeed resolves the immediately pressing maintenance problems, this settlement comes with an substantial price tag. +Since interface and core functionality are now torn apart, the code conducting even the simplest interaction is +scattered over various parts of the system, and thus everything need to be very well defined and documented. +Such a system becomes rigid as soon as it is built. And additionally it tends to suffer from structure duplication +and multiplication, since cross interactions involving several parts of the core and the surface tend to be had +to express: Interaction state need to be materialised into the core data, to allow other, distinct parts of the +interface to collaborate. + +We may escape from that dichotomy when we cease to _rely on data as the fundamental substrate of our system._ +To collaborate, both parts still need to share a structure, yet it suffices for this structure to be a _common +understanding:_ both the UI-Layer and the Proc-Layer need to share a common understanding how the parts of a editing +session can be arranged. Yet they do not need to share a _common data representation_ of the session. + +The consequence is that both sides, ``the core'' and ``the UI'' remain autonomous within their realm. For some concerns, namely _the core concerns,_ that is editing, arranging, processing, the core is in charge and has absolute authority. +On the other hand, when it comes to user interaction, especially the _mechanics and materiality of interaction,_ the UI +is the authority; it is free to decide about what is exposed and in which way. Yet the common structural understanding +is never fully, totally cast in concrete data structures. + +Rather, the core sends *diff messages* up to the UI, indicating how it sees this virtual structure to be changing. +The UI reflects these changes into _its own understanding and representation,_ which is here a structure of display +widgets. When the user interacts with these structures of the presentation layer, *action command messages* are generated, +using the element-IDs to designate the arguments of the intended operation. Acting on these messages again causes reaction and change in the core, which is reflected back in the form of further diff messages. + +Anatomy of the Proc/GUI interface +--------------------------------- +- the `GuiFacade` is used as a general lifecycle facade to start up the GUI and to set up the »Layer Separation + Interfaces«. The `GuiFacade` is implemented by a class _in Proc-Layer_ and loads the Lumiera GTK-UI as a plug-in. +- once the UI is running, it exposes the `GuiNotificationFacade`, to allow pushing state and structure updates up into + the user interface. +- in the opposite direction, for initiating actions from the UI, the Session Subsystem opens the `SessionCommandFacade`, + which can be considered __ ``the'' public session interface.__ + +Both these primary interfaces operate *message based.* As immediate consequence, these interfaces are comparatively +narrow, and interactions can easily be serialised, enqueued and dispatched. On the Proc-Layer side, the `ProcDispatcher` +coordinates command execution and the _Builder run_ to derive the consequences of those session command invocations. +And within the UI, the GTK-Event loop runs likewise single threaded, and the Notification facade automatically +dispatches any invocation into the UI thread. + +Within the UI-Layer, we do not rely on a central ``shadow model'', rather, the backbone of the UI is a communication +structure, the *UI-Bus*. Data within the GUI is kept very local, close to the presentation widgets. In fact, we +identify a selection of _really important_ UI elements, which we call ``the tangible elements'' (`gui::model::Tangible`). +Each of those primary elements within the UI _is connected to the UI-Bus,_ and can be addressed by its ID, and it +happens to correspond and reflect an entity within the session model, there denoted by the same ID. The core can +thus send a message towards the corresponding tangible UI element, without any knowledge as to how and where this +element is actually attached within the widget tree of the user interface. Even diff messages are dispatched over +the UI-Bus; it is up to the receiving tangible element to decide about the meaning of such a diff message, and +how to reshape its internal widget state to reflect that change. + +When it comes to the more elaborate parts of the model and the UI -- especially for the timeline -- those tangible +elements need not be widgets by themselves. Rather, they can be local presentation controllers, often also referred +to as *Presenter*. These manage and control a selection of slave widgets to form the visual presentation of the +corresponding session entity. For example, a `ClipPresenter` injects one or a conglomerate of several widgets +into the track canvas, depending on the current zoom level and the display state of the clip (collapsed, expanded, +abridged display). + diff --git a/doc/technical/gui/guiTheme.txt b/doc/technical/gui/GuiTheme.txt similarity index 78% rename from doc/technical/gui/guiTheme.txt rename to doc/technical/gui/GuiTheme.txt index 8a0c2cab5..2fa13d845 100644 --- a/doc/technical/gui/guiTheme.txt +++ b/doc/technical/gui/GuiTheme.txt @@ -1,8 +1,10 @@ Lumiera GUI style and theming ============================= +//Menu: label Theme/Style + +_this page collects some pieces of information regarding the visual appearance of the Lumiera GTK UI_ -'this page collects some pieces of information regarding the visual appearance of the Lumiera GTK GUI' GTK-3 styles ------------ @@ -15,7 +17,9 @@ introduce additional engine specific styles and settings. GTK-3 supports the powerful 'cascading' and 'contextual selectors' from CSS. Thus the nesting of elements in the GUI forms the base for creating styling rules. Hereby, widget class names translate into ``tag'' names -in the CSS selectors. Widgets may also expose CSS classes for styling -- the standard widgets define a generic set +in the CSS selectors.footnote:[The ``tag'' name is the widget's class name without the `Gtk::` prefix, and transformed +into all lower caps, single word, no underscores. E.g. `Gtk::TextView` -> `textview`] +Widgets may also expose CSS classes for styling -- the standard widgets define a generic set of https://developer.gnome.org/gtk3/3.4/GtkStyleContext.html#gtkstylecontext-classes[predefined CSS style classes], which can be used to establish the foundation for theming. Obviously it is preferable to keep styling rules as concise, generic and systematic as possible; yet we may still refer to individual gui elements by name (`#ID`) though. @@ -27,6 +31,8 @@ Recommended reading and https://developer.gnome.org/gtk3/3.4/GtkStyleContext.html#gtkstylecontext-classes[predefined style classes] in the GTK-3 reference manual. + * there is an https://developer.gnome.org/gtk3/stable/chap-css-overview.html[overview page in the developer manual], + and a https://developer.gnome.org/gtk3/stable/chap-css-properties.html[reference of supported properties]. * to start, look at this http://thegnomejournal.wordpress.com/2011/03/15/styling-gtk-with-css/[introductory text], or the more http://worldofgnome.org/making-gtk3-themes-part-1-basics/[hands-on series of articles from world of gnome] * this http://forums.fedoraforum.org/showthread.php?t=281568[post from fedora forum] features a conciese description @@ -49,6 +55,15 @@ A good recommendation is really to ``probe'' settings by changing them temporari (e.g. `background-color: red`). It is just too easy to learn wrong techniques based on false conclusions. +the GTK+ inspector +~~~~~~~~~~~~~~~~~~ +An essential tool when working with styles and Gtk widgets in general is the +https://wiki.gnome.org/Projects/GTK%2B/Inspector[GTK+ inspector], which is part of the standard GTK distribution. +It allows to inspect all GTK objects with their properties, and to see the actual tree of CSS nodes and the +corresponding selectors. You can even add a style class or state flag (like ``hover'') dynamically, and you +may add style rules and verify the effect on the application immediately. To use this ispector, launch the +application like `GTK_DEBUG=interactive target/lumiera` + binary themes ~~~~~~~~~~~~~ diff --git a/research/SConscript b/research/SConscript index 4df559f6a..5e4b2d481 100644 --- a/research/SConscript +++ b/research/SConscript @@ -4,7 +4,7 @@ ## Things defined here usuall won't be installed ## -Import('env core support_lib') +Import('env envGtk core support_lib') @@ -12,9 +12,14 @@ envR = env.Clone() envR.Append(CPPPATH='research') # envR.Append(CCFLAGS=' -O3 ') +envRGtk = envGtk.Clone() +envRGtk.Append(CPPPATH='research') + # build additional test and administrative tools.... experiments = [ envR.Program('try', ['try.cpp'] + core) #### to try out some feature... - , envR.Program('clang-static-init', ['clang-static-init-1.cpp', 'clang-static-init-2.cpp']) + , envR.Program('clang-static-init', ['clang-static-init-1.cpp', 'clang-static-init-2.cpp']) + + , envRGtk.Program('gtk-canvas-experiment', ['gtk-canvas-experiment.cpp', 'gtk-canvas-main.cpp'] + core) ] # diff --git a/research/gtk-canvas-experiment.cpp b/research/gtk-canvas-experiment.cpp new file mode 100644 index 000000000..af3132789 --- /dev/null +++ b/research/gtk-canvas-experiment.cpp @@ -0,0 +1,368 @@ +/* + GtkCanvasExperiment - Explore capabilities of the GTK canvas widget + + Copyright (C) Lumiera.org + 2016, 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 gtk-canvas-experiment.cpp + ** Implementation of gtk canvas experiments. + ** + ** @remark as of 10/2018 we start to build a new timeline widget, + ** based on this technology demo. + ** @see timeline-widget.hpp + ** + */ + + +#include "gtk-canvas-experiment.hpp" + +#include "lib/format-string.hpp" +#include "lib/format-cout.hpp" +#include "lib/util.hpp" + +#include +#include +#include + + + +using util::_Fmt; +using util::isnil; +using Gtk::Widget; +using sigc::mem_fun; +using sigc::ptr_fun; +using std::string; +using std::rand; +using std::max; + + +namespace demo { + + + CanvasDemoPanel::CanvasDemoPanel () + : Box{} + , twoParts_(Gtk::ORIENTATION_VERTICAL) + , buttons_() + , frame_("Gtk::Layout Experiments") + , scroller_() + , canvas_() + { + twoParts_.pack_start(buttons_, Gtk::PACK_SHRINK); + twoParts_.pack_start(frame_); + + buttons_.set_layout(Gtk::BUTTONBOX_START); + + // buttons to trigger experiments + button_1_.set_label("_place"); + button_1_.set_use_underline(); + button_1_.set_tooltip_markup("Experiment 1:\nplace new child widget\nat random position on the canvas"); + button_1_.signal_clicked().connect( + mem_fun(*this, &CanvasDemoPanel::experiment_1)); + buttons_.add(button_1_); + + button_2_.set_label("_move"); + button_2_.set_use_underline(); + button_2_.set_tooltip_markup("Experiment 2:\nmove all child widgets randomly"); + button_2_.signal_clicked().connect( + mem_fun(*this, &CanvasDemoPanel::experiment_2)); + buttons_.add(button_2_); + + button_3_.set_label("a_lign"); + button_3_.set_use_underline(); + button_3_.set_tooltip_markup("Experiment 3:\nalign all child widgets in a row\nwith silight random vertical offset"); + button_3_.signal_clicked().connect( + mem_fun(*this, &CanvasDemoPanel::experiment_3)); + buttons_.add(button_3_); + + button_4_.set_label("_grow"); + button_4_.set_use_underline(); + button_4_.set_tooltip_markup("Experiment 4:\nextend arbitrary child widget's text"); + button_4_.signal_clicked().connect( + mem_fun(*this, &CanvasDemoPanel::experiment_4)); + buttons_.add(button_4_); + + button_5_.set_label("_kill"); + button_5_.set_use_underline(); + button_5_.set_tooltip_markup("Experiment 5:\nkill arbitrary child widget"); + button_5_.signal_clicked().connect( + mem_fun(*this, &CanvasDemoPanel::experiment_5)); + buttons_.add(button_5_); + + toggleDraw_.set_label("draw"); + toggleDraw_.signal_clicked().connect( + [this]() { + canvas_.enableDraw (this->toggleDraw_.get_active()); + }); + buttons_.add(toggleDraw_); + //(End)buttons... + + frame_.add(scroller_); + frame_.set_border_width(5); + + scroller_.set_shadow_type(Gtk::SHADOW_IN); + scroller_.property_expand() = true; // dynamically grab any available additional space + scroller_.set_border_width(10); + scroller_.add(canvas_); + + canvas_.adjustSize(); + + // show everything.... + this->add(twoParts_); + this->show_all(); + } + + + void + CanvasDemoPanel::experiment_1() + { + frame_.set_label("Experiment 1... PLACE"); + + ChildEx* chld = makeChld(); + childz_.push_back(chld); + uint x = rand() % 1000; + uint y = rand() % 500; + canvas_.put(*chld, x, y); + chld->show(); + canvas_.adjustSize(); + } + + + void + CanvasDemoPanel::experiment_2() + { + frame_.set_label("Experiment 2... MOVE"); + for (Widget* chld : childz_) + { + uint x = canvas_.child_property_x(*chld); + uint y = canvas_.child_property_y(*chld); + int deltaX = -20 + rand() % 41; + int deltaY = -15 + rand() % 31; + x = uint(max (0, int(x) + deltaX)); + y = uint(max (0, int(y) + deltaY)); + + canvas_.move (*chld, x,y); + } + canvas_.adjustSize(); + } + + + void + CanvasDemoPanel::experiment_3() + { + frame_.set_label("Experiment 3... ALIGN"); + uint pos=0; + for (Widget* chld : childz_) + { + uint y = rand() % 30; + canvas_.move (*chld, pos, y); + + int width = chld->get_allocated_width(); + pos += 0.6 * width; + } + canvas_.adjustSize(); + } + + + void + CanvasDemoPanel::experiment_4() + { + frame_.set_label("Experiment 4... GROW"); + if (isnil (childz_)) + { + ERROR (test, "need to fabricate more childz before you can grow 'em..."); + return; + } + uint selector = rand() % childz_.size(); + ChildEx& toGrow = *childz_[selector]; + toGrow.set_label ("***"+toGrow.get_label()+"***"); + } + + + void + CanvasDemoPanel::experiment_5() + { + frame_.set_label("Experiment 5... KILL"); + if (isnil (childz_)) + { + WARN (test, "no children to kill. so sad."); + return; + } + uint killPos = rand() % childz_.size(); + ChildV::iterator killThat(&childz_[killPos]); + ChildEx* victim = *killThat; + childz_.erase (killThat); + canvas_.remove (*victim); + delete victim; + } + + + void + Canvas::enableDraw (bool yes) + { + shallDraw_ = yes; + + // force redrawing of the visible area... + auto win = get_window(); + if (win) + { + int w = get_allocation().get_width(); + int h = get_allocation().get_height(); + Gdk::Rectangle rect{0, 0, w, h}; + win->invalidate_rect(rect, false); + } + } + + + + void + Canvas::adjustSize() + { + recalcExtension_ = true; + } + + void + Canvas::determineExtension() + { + if (not recalcExtension_) return; + + uint extH=20, extV=20; + Gtk::Container::ForeachSlot callback + = [&](Gtk::Widget& chld) + { + auto alloc = chld.get_allocation(); + uint x = alloc.get_x(); + uint y = alloc.get_y(); + x += alloc.get_width(); + y += alloc.get_height(); + extH = max (extH, x); + extV = max (extV, y); + }; + foreach(callback); + recalcExtension_ = false; + set_size (extH, extV); + } + + + bool + Canvas::on_draw(Cairo::RefPtr const& cox) + { + if (shallDraw_) + { + uint extH, extV; + determineExtension(); + get_size (extH, extV); + + auto adjH = get_hadjustment(); + auto adjV = get_vadjustment(); + double offH = adjH->get_value(); + double offV = adjV->get_value(); + + cox->save(); + cox->translate(-offH, -offV); + + // draw red diagonal line + cox->set_source_rgb(0.8, 0.0, 0.0); + cox->set_line_width (10.0); + cox->move_to(0, 0); + cox->line_to(extH, extV); + cox->stroke(); + cox->restore(); + + // cause child widgets to be redrawn + bool event_is_handled = Gtk::Layout::on_draw(cox); + + // any drawing which follows happens on top of child widgets... + cox->save(); + cox->translate(-offH, -offV); + + cox->set_source_rgb(0.2, 0.4, 0.9); + cox->set_line_width (2.0); + cox->rectangle(0,0, extH, extV); + cox->stroke(); + cox->restore(); + + return event_is_handled; + } + else + return Gtk::Layout::on_draw(cox); + } + + + /* === Support for Investigation === */ + + namespace { + _Fmt childID("Chld-%02d"); + + int instanceCnt = 0; + } + + + uint ChildEx::childNo = 0; + + + ChildEx::ChildEx() + : Gtk::Button(string (childID % childNo++)) + { + ++instanceCnt; + } + + + void + ChildEx::on_clicked() + { + cout << "|=="< 0) + cout << " ↯↯ still "< + + 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 gtk-canvas-experiment.hpp + ** A custom widget using a canvas to draw and to place widgets. + ** @remark as of 10/2018 we start to build a new timeline widget, + ** based on this technology demo. + */ + + +#ifndef RESEARCH_GTK_CANVAS_EXPERIMENT_H +#define RESEARCH_GTK_CANVAS_EXPERIMENT_H + +#include "gui/gtk-base.hpp" + +#include + + +namespace demo { + + + /** + * "experimental" child widget for investigation of Gtk::Layout + */ + class ChildEx + : public Gtk::Button + { + static uint childNo; + + public: + ChildEx(); + ~ChildEx(); + + private: + void on_clicked() override; + }; + + void __verifyDeadChildren(); + + + + /** + * "experimental" custom canvas, based on Gtk::Layout. + * In addition this customised widget supports direct drawing + */ + class Canvas + : public Gtk::Layout + { + bool shallDraw_; + bool recalcExtension_ = false; + + public: + void enableDraw (bool); + void adjustSize(); + + private: + virtual bool on_draw (Cairo::RefPtr const&) override; + + void determineExtension(); + }; + + + + /** + * Dockable panel to hold timeline widget(s). + * @todo build the actual implementation, after finishing the investigation + * + * ## Investigation of gtk::GtkLayout + * As of 10/2016, we start this task with an exploration of GTK behaviour + * + * \par Plan of investigation + * 1. place some simple widgets (Buttons) ✔ + * 2. learn how to draw ✔ + * 3. place a huge number of widgets, to scrutinise scrolling and performance + * 4. place widgets overlapping and irregularly, beyond the scrollable area ✔ + * 5. bind signals to those widgets, to verify event dispatching ✔ + * 6. bind some further signal(s) to the ~GtkLayout container + * 7. hide and re-show a partially and a totally overlapped widget + * 8. find a way to move a widget ✔ and delete arbitrary widgets ✔ + * 9. expand an existing widget (text change) ✔ + * 10. build a custom "clip" widget ✘ + * 11. retrofit all preceding tests to use this "clip" widget ✘ + */ + class CanvasDemoPanel + : public Gtk::Box + { + public: + /** + * @param panel_manager The owner panel manager widget. + * @param dock_item The GdlDockItem that will host this panel. + */ + CanvasDemoPanel(); + + + + private: + Gtk::Box twoParts_; + Gtk::ButtonBox buttons_; + Gtk::Button button_1_; + Gtk::Button button_2_; + Gtk::Button button_3_; + Gtk::Button button_4_; + Gtk::Button button_5_; + Gtk::CheckButton toggleDraw_; + Gtk::Frame frame_; + Gtk::ScrolledWindow scroller_; + Canvas canvas_; + + ChildEx* makeChld(); + + using ChildV = std::vector; + ChildV childz_; + + void experiment_1(); + void experiment_2(); + void experiment_3(); + void experiment_4(); + void experiment_5(); + }; + + +}// namespace demo +#endif /*RESEARCH_GTK_CANVAS_EXPERIMENT_H*/ diff --git a/research/gtk-canvas-main.cpp b/research/gtk-canvas-main.cpp new file mode 100644 index 000000000..e949c675e --- /dev/null +++ b/research/gtk-canvas-main.cpp @@ -0,0 +1,64 @@ +/* + gtk-canvas-main.cpp - demo application to explore the abilities of Gtk::Canvas + + Copyright (C) Lumiera.org + 2018, 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 gtk-canvas-main.cpp + ** Simple GTK Application frame to explore canvas handling. + ** This demo application was extracted in 10/2018 to preserve + ** [exploration experiments](\ref demo::CanvasDemoPanel), carried + ** out in 2016 to decide upon the technology to support Lumiera's + ** gui::timeline::TimelineWidget. + */ + + +#include "gui/gtk-base.hpp" +#include "gtk-canvas-experiment.hpp" +#include "lib/error.hpp" + + +namespace demo { + + class Window + : public Gtk::Window + { + CanvasDemoPanel demoPanel_; + + public: + Window() + : demoPanel_{} + { + this->add (demoPanel_); + } + }; +}//namespace + + + +int +main (int argc, char* argv[]) +{ + NOTICE (main, "*** GTK-Canvas Experiment ***"); + + auto guiApp = Gtk::Application::create (argc, argv); + + demo::Window window; + return guiApp->run(window); +} diff --git a/research/try.cpp b/research/try.cpp index 9d2805acf..8f8108be1 100644 --- a/research/try.cpp +++ b/research/try.cpp @@ -39,11 +39,14 @@ // 03/18 - Dependency Injection / Singleton initialisation / double checked locking // 04/18 - investigate construction of static template members // 08/18 - Segfault when compiling some regular expressions for EventLog search +// 10/18 - investigate insidious reinterpret cast /** @file try.cpp - * Heisenbug hunt: random crashes in BusTerm_test, seemingly emerging from regular expression compilation. - * Not able to reproduce the crash, unfortunately. //////////////////////////////////////////////////////////TICKET #1158 + * Document an insidious wild cast, caused by the syntax `Type(arg)`. + * I was under the wrong assumption this would be handled equivalent to a constructor invocation. + * Seemingly it is rather handled as a C-style cast, i.e. equivalent to `(Type)arg`. + * @see [Question on Stackoverflow](https://stackoverflow.com/q/52782967/444796) */ typedef unsigned int uint; @@ -52,12 +55,10 @@ typedef unsigned int uint; #include "lib/test/test-helper.hpp" #include "lib/util.hpp" -#include #include -#include using std::string; -using VecS = std::vector; +using util::isSameObject; @@ -69,25 +70,32 @@ using VecS = std::vector; +class Wau + { + int i = -1; + }; + +class Miau + { + public: + uint u = 1; + }; + + + + + int main (int, char**) { - VecS rexs{{"after.+_ATTRIBS_.+ins.+1 of 62 ≺293.gen029≻.+mut.+1 of 62 ≺293.gen029≻.+ins.+borgID.+293.+emu.+1 of 62 ≺293.gen029≻" - ,"after.+_ATTRIBS_.+ins.+1 of 64 ≺251.gen019≻.+mut.+1 of 64 ≺251.gen019≻.+ins.+borgID.+251.+emu.+1 of 64 ≺251.gen019≻" - ,"after.+_ATTRIBS_.+ins.+1 of 8 ≺203.gen036≻.+mut.+1 of 8 ≺203.gen036≻.+ins.+borgID.+203.+emu.+1 of 8 ≺203.gen036≻" - ,"after.+?_ATTRIBS_.+?ins.+?53 of 57 ≺358.gen010≻.+?mut.+?53 of 57 ≺358.gen010≻.+?ins.+?borgID.+?358.+?emu.+?53 of 57 ≺358.gen010≻" - ,"after.+?_ATTRIBS_.+?ins.+?53 of 63 ≺178.gen028≻.+?mut.+?53 of 63 ≺178.gen028≻.+?ins.+?borgID.+?178.+?emu.+?53 of 63 ≺178.gen028≻" - ,"after.+?_ATTRIBS_.+?ins.+?53 of 59 ≺498.gen038≻.+?mut.+?53 of 59 ≺498.gen038≻.+?ins.+?borgID.+?498.+?emu.+?53 of 59 ≺498.gen038≻" - ,"after.+?_ATTRIBS_.+?ins.+?53 of 60 ≺223.gen003≻.+?mut.+?53 of 60 ≺223.gen003≻.+?ins.+?borgID.+?223.+?emu.+?53 of 60 ≺223.gen003≻" - ,"after.+?_ATTRIBS_.+?ins.+?53 of 78 ≺121.gen015≻.+?mut.+?53 of 78 ≺121.gen015≻.+?ins.+?borgID.+?121.+?emu.+?53 of 78 ≺121.gen015≻" - }}; - for (auto const& str : rexs) - { - auto rex = std::regex{str}; - SHOW_EXPR (std::regex_search(str, rex)); - } + Wau wau; + using ID = Miau &; + ID wuff = ID(wau); + cout << "Miau=" << wuff.u + << " ref to same object: " << isSameObject (wau, wuff) + << endl; cout << "\n.gulp.\n"; diff --git a/src/SConscript b/src/SConscript index b21a72d23..34cccabb0 100644 --- a/src/SConscript +++ b/src/SConscript @@ -38,13 +38,12 @@ plugins = [] # currently none # the Lumiera GTK GUI envGtk = env.Clone() envGtk.mergeConf(['gtkmm-3.0','sigc++-2.0','gthread-2.0','cairomm-1.0','gdl','xv','x11','xext','sm']) -envGtk.Append(LIBS=core) -guimodule = envGtk.LumieraPlugin('gtk_gui', srcSubtree('gui'), install=True) +guimodule = envGtk.LumieraPlugin('gtk_gui', srcSubtree('gui') + core, install=True) gui = ( guimodule + icons + [env.GuiResource(f) for f in env.Glob('gui/*.css')] ) -Export('lumiera core core_lib app_lib backend_lib support_lib plugins guimodule gui') +Export('lumiera core core_lib app_lib backend_lib support_lib plugins envGtk guimodule gui') diff --git a/src/common/configfacade.cpp b/src/common/configfacade.cpp index 3841f14eb..74d915aea 100644 --- a/src/common/configfacade.cpp +++ b/src/common/configfacade.cpp @@ -35,7 +35,7 @@ ** @todo as of 2016, this seems not to be used much, if at all. ** The GTK-UI, which in itself is very preliminary, retrieves ** some values from configuration, most notably the name of - ** the GTK stylesheet (`gtk-lumiera.css` is the default) + ** the GTK stylesheet (`lumiera.css` is the default) */ diff --git a/src/common/guifacade.cpp b/src/common/guifacade.cpp index bde729f1e..212c431a1 100644 --- a/src/common/guifacade.cpp +++ b/src/common/guifacade.cpp @@ -24,7 +24,7 @@ /** @file guifacade.cpp ** Implementation of the GUI loader. Code generated from this ** translation unit is linked into the core application, where it implements - ** the [Subsystem descriptor](subsys.hpp) for the _UI Subsystem._ When main() + ** the [Subsystem descriptor](\ref subsys.hpp) for the _UI Subsystem._ When main() ** activates and starts this subsystem, an instance of gui::GuiRunner will be ** created, which causes the Lumiera UI plug-in to be loaded and a new thread ** to be spawned, which launches the UI and performs the event loop. @@ -150,7 +150,7 @@ namespace gui { ERROR_LOG_AND_IGNORE (guifacade, "trigger shutdown of the GUI"); } - bool + bool checkRunningState () noexcept override { return bool(facade); @@ -210,15 +210,6 @@ namespace gui { { return bool(facade); } - - - + + } // namespace gui - -/* -// Local Variables: -// mode: C++ -// c-file-style: "gnu" -// indent-tabs-mode: nil -// End: -*/ diff --git a/src/common/notification-interface-proxy.cpp b/src/common/notification-interface-proxy.cpp index cb8951562..d767bdb31 100644 --- a/src/common/notification-interface-proxy.cpp +++ b/src/common/notification-interface-proxy.cpp @@ -42,6 +42,7 @@ namespace gui { namespace lumiera { namespace facade { using gui::ID; + using lib::diff::GenNode; using lib::diff::MutationMessage; @@ -62,11 +63,12 @@ namespace facade { //----Proxy-Implementation-of-GuiNotification-------- - void displayInfo (Level level, string const& text) override { _i_.displayInfo (level, cStr(text)); } - void markError (ID uiElement, string const& text) override { _i_.markError(uiElement.getHash().get(), cStr(text)); } - void markNote (ID uiElement, string const& text) override { _i_.markNote (uiElement.getHash().get(), cStr(text)); } - void mutate (ID uiElement, MutationMessage&& diff) override { _i_.mutate (uiElement.getHash().get(), &diff); } - void triggerGuiShutdown (string const& cause) override { _i_.triggerGuiShutdown (cStr(cause)); } + void displayInfo (Level level, string const& text) override { _i_.displayInfo (level, cStr(text)); } + void markError (ID uiElement, string const& text) override { _i_.markError(&uiElement, cStr(text)); } + void markNote (ID uiElement, string const& text) override { _i_.markNote (&uiElement, cStr(text)); } + void mark (ID uiElement, GenNode&& stateMark) override { _i_.mark (&uiElement, &stateMark); } + void mutate (ID uiElement, MutationMessage&& diff) override { _i_.mutate (&uiElement, &diff); } + void triggerGuiShutdown (string const& cause) override { _i_.triggerGuiShutdown (cStr(cause)); } public: diff --git a/src/common/option.cpp b/src/common/option.cpp index ce59d986b..268bfa63b 100644 --- a/src/common/option.cpp +++ b/src/common/option.cpp @@ -56,7 +56,7 @@ namespace lumiera { * @todo describe the actual options */ Option::Option (lib::Cmdline& cmdline) - : syntax("Lumiera, the non linear video editor. Supported parameters"), + : syntax("Lumiera, the non linear video editor.\nSupported parameters"), parameters() { syntax.add_options() @@ -64,7 +64,7 @@ namespace lumiera { ("session,f", op::value(), "session file to load (UNIMPLEMENTED)") ("script,s", op::value(), - "execute the given LUA script (UNIMPLEMENTED)") + "execute the given script (UNIMPLEMENTED)") ("headless", op::bool_switch(), "start without GUI") ("port,p", op::value(), diff --git a/src/common/query/query-resolver.hpp b/src/common/query/query-resolver.hpp index 52ea1f231..0a6f2759d 100644 --- a/src/common/query/query-resolver.hpp +++ b/src/common/query/query-resolver.hpp @@ -96,7 +96,7 @@ namespace lumiera { /** * Interface: a facility for resolving (some kind of) queries * A concrete subclass has the ability to create Resolution instances - * in response to specific queries of some kind, [if applicable](QueryResolver::canHandle). + * in response to specific queries of some kind, [if applicable](\ref QueryResolver::canHandle). * Every resolution mechanism is expected to enrol by calling #installResolutionCase. * Such a registration is considered permanent; a factory function gets stored, * assuming that the entity to implement this function remains available diff --git a/src/common/subsystem-runner.hpp b/src/common/subsystem-runner.hpp index e933737f2..fe9808d1d 100644 --- a/src/common/subsystem-runner.hpp +++ b/src/common/subsystem-runner.hpp @@ -22,12 +22,13 @@ /** @file subsystem-runner.hpp - ** Manage execution of the independend [Subsystems](\ref subsys.hpp) + ** Manage execution of the independent [Subsystems](\ref subsys.hpp) ** of the Lumiera application. The SubsystemRunner is used by ** lumiera::AppState::init() for actually "performing" the various subsystems, ** as defined by their [Subsystem descriptors](\ref lumiera::Subsys). Together ** these parts define the lifecycle protocol. ** + ** @todo as of 2018, a design rework seems indicated eventually /////////////////////////TICKET #1177 ** @remark While this facility is still pretty much the first implementation draft ** from 2008, it fulfilled our needs to express simple dependencies and to ** conduct a controlled shutdown in case of fatal problems. The operations diff --git a/src/common/ui-protocol.cpp b/src/common/ui-protocol.cpp new file mode 100644 index 000000000..f0041692e --- /dev/null +++ b/src/common/ui-protocol.cpp @@ -0,0 +1,63 @@ +/* + UiProtocol - magic keys used for communication with the Lumiera UI + + Copyright (C) Lumiera.org + 2018, 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-protocol.cpp + ** Storage for key constants and basic definitions used for interfacing with the GUI. + */ + +//#include "gui/gtk-base.hpp" /////////////////////TODO necessary? +#include "include/ui-protocol.hpp" + + +namespace gui { + + const Symbol META_kind{"kind"}; + + const Symbol TYPE_Fork{"Fork"}; + const Symbol TYPE_Clip{"Clip"}; + const Symbol TYPE_Marker{"Marker"}; + const Symbol TYPE_Channel{"Channel"}; + const Symbol TYPE_Effect{"Effect"}; + + const Symbol ATTR_name{"name"}; + const Symbol ATTR_fork{"fork"}; + const Symbol ATTR_assets{"assets"}; + + + /* ======== UI-Element protocol ======== */ + + const Symbol MARK_reset{"reset"}; + const Symbol MARK_clearErr{"clearErr"}; + const Symbol MARK_clearMsg{"clearMsg"}; + const Symbol MARK_expand{"expand"}; + const Symbol MARK_reveal{"reveal"}; + const Symbol MARK_Flash{"Flash"}; + const Symbol MARK_Error{"Error"}; + const Symbol MARK_Warning{"Warning"}; + const Symbol MARK_Message{"Message"}; + + /** */ + + + +}// namespace gui diff --git a/src/gui/ctrl/actions.hpp b/src/gui/ctrl/actions.hpp index 1cf4d3539..57d99d3ea 100644 --- a/src/gui/ctrl/actions.hpp +++ b/src/gui/ctrl/actions.hpp @@ -70,8 +70,8 @@ namespace ctrl { /** - * A helper class which registers and handles - * user action events. + * A helper class which registers global user actions + * and populates the main menu and toolbar. */ class Actions : util::NonCopyable @@ -130,6 +130,7 @@ namespace ctrl { menu("HelpMenu", _("_Help")); entry ([&]() { globalCtx_.wizard_.show_HelpAbout(); } , "HelpAbout", Stock::ABOUT); + entry ([&]() { globalCtx_.wizard_.launchTestCtrl(); } , "HelpTest", _("Self _Tests...")); menu("WindowMenu", _("_Window")); @@ -202,6 +203,7 @@ namespace ctrl { + diff --git a/src/gui/ctrl/bus-term.hpp b/src/gui/ctrl/bus-term.hpp index b2deb406d..d4b6f918c 100644 --- a/src/gui/ctrl/bus-term.hpp +++ b/src/gui/ctrl/bus-term.hpp @@ -34,7 +34,7 @@ ** which it will be wired. Moreover, each BusTerm bears a distinct ** [identity](\ref BusTerm::endpointID_), which is used as _implicit subject_ ** for emanating messages, or as explicit destination for routing. - ** The whole [UI-Bus](ui-bus.hpp) is built to perform within the + ** The whole [UI-Bus](\ref ui-bus.hpp) is built to perform within the ** UI event thread and thus is _not threadsafe_. For that reason, ** the automatic detachment built into each BusTerm's dtor is ** sufficient to ensure sane connectivity. diff --git a/src/gui/ctrl/core-service.hpp b/src/gui/ctrl/core-service.hpp index 61cb7775a..72b87e16d 100644 --- a/src/gui/ctrl/core-service.hpp +++ b/src/gui/ctrl/core-service.hpp @@ -39,6 +39,11 @@ ** since both CoreService and Nexus are mutually interdependent from an ** operational perspective, since they exchange messages in both directions. ** + ** In fact, the CoreService even _holds and thus manages_ the Nexus as a + ** private member, while the latter controls and connects all nodes attached + ** to the bus at runtime, including CoreService. This crisscross arrangement + ** ensures sane start-up and shutdown of the whole UI-Bus compound. + ** ** ## Bus connection and topology ** The CoreService plays a central role within the UI, since it represents ** _»the application core«_ from the UI layer's viewpoint. But it is not @@ -47,8 +52,8 @@ ** routing table in Nexus, can be addressed as a _first class citizen,_ ** that is, we're able to direct messages towards such an element, knowing ** only it's ID. But there is a twist: all connections to the Bus are made - ** from [bus terminals](ctrl::BusTerm), and each _node_, i.e. each - ** [tangible model element](model::Tangible) has a BusTerm member and + ** from [bus terminals](\ref ctrl::BusTerm), and each _node_, i.e. each + ** [tangible model element](\ref model::Tangible) has a BusTerm member and ** thus inherits the ability to talk to the bus. But only when _actively_ ** connected to the bus, a full link and entry in the routing table is ** established. The constructor of model::Tangible indeed makes such @@ -65,8 +70,8 @@ ** for the Nexus, and thus gains the ability to respond to those few special ** messages, which can not be handled in a generic way on the Nexus: ** - *act* handles command invocation within the Session core, and - ** is treated by [forwarding](command-handler.hpp) it over the - ** SessionCommand facade to the [Proc-Dispatcher](proc-dispatcher.hpp) + ** is treated by [forwarding](\ref command-handler.hpp) it over the + ** SessionCommand facade to the [Proc-Dispatcher](\ref proc-dispatcher.hpp) ** - *note* observes and captures presentation state note messages, which ** are to be handled by a central presentation state manager (TODO 1/17). ** @@ -103,6 +108,8 @@ namespace ctrl{ * handles those messages to be processed by centralised services: * - commands need to be sent down to Proc-Layer * - presentation state messages need to be recorded and acted upon. + * As an object, CoreService encases the heart of the UI-Bus, the + * \ref Nexus, and acts as "PImpl" for the gui::UiBus front-end. */ class CoreService : public BusTerm diff --git a/src/gui/ctrl/facade.hpp b/src/gui/ctrl/facade.hpp index 6d6093d2d..ce4eafdf4 100644 --- a/src/gui/ctrl/facade.hpp +++ b/src/gui/ctrl/facade.hpp @@ -30,8 +30,8 @@ ** ** @note GTK operates single threaded by design. ** For this reason, any call from other parts of the application need to be explicitly - ** dispatched into the UI event loop. The external facade interfaces are defined in a way - ** to ensure this constraint is met. + ** dispatched into the UI event loop. The external façade interfaces are constructed + ** appropriately to ensure this constraint is regarded. ** ** @see notification-service.hpp ** @see ui-manager.hpp diff --git a/src/gui/ctrl/nexus.hpp b/src/gui/ctrl/nexus.hpp index 3d6a93a9f..205c88dd2 100644 --- a/src/gui/ctrl/nexus.hpp +++ b/src/gui/ctrl/nexus.hpp @@ -23,8 +23,8 @@ /** @file nexus.hpp ** Core hub and routing table of the UI-Bus. - ** Any relevant element within the Lumiera GTK UI is connected to the [UI-Bus](ui-bus.hpp) - ** through some [bus terminal](bus-term.hpp). Actually, there is one special BustTerm + ** Any relevant element within the Lumiera GTK UI is connected to the [UI-Bus](\ref ui-bus.hpp) + ** through some [bus terminal](\ref bus-term.hpp). Actually, there is one special BustTerm ** implementation, which acts as router and messaging hub. ** ** @note messages to unknown target elements are silently dropped. @@ -69,7 +69,7 @@ namespace ctrl{ * * Thus, the "uplink", which is in fact the CoreService, is left to handle * - the *`act`* messages (command invocation) - * - the *`mark`* messages (upstream state change notification) + * - the *`note`* messages (upstream state change notification) * * @todo write type comment */ diff --git a/src/gui/ctrl/notification-hub.hpp b/src/gui/ctrl/notification-hub.hpp new file mode 100644 index 000000000..84b897ff1 --- /dev/null +++ b/src/gui/ctrl/notification-hub.hpp @@ -0,0 +1,208 @@ +/* + NOTIFICATION-HUB.hpp - receive and reroute notification messages + + Copyright (C) Lumiera.org + 2018, 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 notification-hub.hpp + ** Controller to receive and handle notification messages for the user. + ** This component is addressed in order to show error, warning and notification messages + ** in the UI. It is responsible for maintaining a local log of these messages, and to + ** allocate an appropriate display widgets, to show such notifications asynchronously. + ** On first use, an InfoboxPanel is allocated to hold an ErrorLogDisplay widget for + ** presentation of those messages + ** + ** # Notification Controller Behaviour + ** + ** This controller serves the purpose to present information and error messages to the user. + ** For the actual display, it allocates an appropriate view, placed into a docking pane + ** reserved for information display, in accordance to the configured screen layout + ** @todo 8/2018 for now we just use and possibly create a hard wired display within the + ** first (primary) application window. Moreover, we do so directly, instead of + ** consulting the (not yet fully implemented) ViewSpec mechanism. (via ViewLocator). + ** + ** The actual widget for information display is prepared, but it is actually allocated when + ** the need for information display arises. Which means, the user may close this display + ** widget, thereby discarding its current information content -- but when the next notification + ** needs to be shown, the controller will ensure to allocate an appropriate widget again. + ** Moreover, the widget can be _expanded_ or _collapsed_, without affecting its content. + ** - information messages are just added to the buffer without much ado. No attempt is made + ** to reveal or expand the widget (but if necessary, a new widget is allocated) + ** - error messages also set an error marker state, and cause expansion of the display widget. + ** - the error state can be _cleared_, which also demotes all error messages to mere information. + ** - information content can also be _cleared_, which removes all mere information messages, + ** while retaining the error entries. + ** - the doReset() operation completely clears the log contents, collapses the widget and clears state. + ** - expanding of the display widget is state marked, irrespective if it happened by user interaction + ** or as result of some display. However, the actual message content is _not_ state marked; it needs + ** to be persisted elsewhere (in the session) and replayed from there if desired. + ** + ** @see error-log-display.hpp + ** @see notification-service.hpp + */ + + +#ifndef GUI_CTRL_NOTIFICATION_HUB_H +#define GUI_CTRL_NOTIFICATION_HUB_H + +#include "gui/widget/error-log-display.hpp" +#include "lib/diff/tree-mutator.hpp" +#include "gui/model/controller.hpp" +#include "gui/model/w-link.hpp" + +#include + + +namespace gui { +namespace ctrl { + + using lib::diff::TreeMutator; + using model::WLink; + + + + + /** + * Service to receive and display error, warning and notification messages. + * These are sent over the UI-Bus through the NotificationService; after receiving + * such a message, this controller ensures to display the message and alert the + * user, while not blocking the overall UI. + */ + class NotificationHub + : public model::Controller + { + + + /** population and manipulation of persistent content via UI-Bus */ + void + buildMutator (lib::diff::TreeMutator::Handle buffer) override + { + buffer.create ( + TreeMutator::build() + ); + UNIMPLEMENTED ("define and implement what need to be reflected from asset::ErrorLog"); + } + + + /* ==== Tangible interface ==== */ + + virtual bool + doReset() override + { + if (not widget_) return false; + + widget_->clearAll(); + widget_->expand (false); + return true; + } + + virtual bool + doMsg (string text) override + { + getWidget().addInfo (text); + return false; // logging is no persistent state + } + + virtual bool + doClearMsg () override + { + if (widget_) + widget_->clearInfoMsg(); + return false; // not persistent (sticky) + } + + virtual bool + doErr (string text) override + { + getWidget().addError (text); + return false; + } + + virtual bool + doClearErr () override + { + if (widget_) + widget_->turnError_into_InfoMsg(); + return false; // not persistent (sticky) + } + + /** adds special treatment for a state mark tagged as `"Warning"` */ + virtual void + doMark (GenNode const& stateMark) override + { + if (stateMark.idi.getSym() == MARK_Warning) + getWidget().addWarn (stateMark.data.get()); + else + // forward to default handler + Controller::doMark (stateMark); + } + + virtual void + doFlash() override + { + if (not widget_) return; + widget_->reveal(); + widget_->triggerFlash(); + } + + + public: + using WidgetAllocator = std::function; + + + NotificationHub (ID identity, ctrl::BusTerm& nexus, WidgetAllocator wa) + : model::Controller{identity, nexus} + , allocateWidget_{wa} + , widget_{} + { + installExpander([&](){ return widget_ and widget_->expand; } + ,[&](bool yes) + { + if (widget_ or yes) + getWidget().expand (yes); + } + ); + installRevealer([&](){ getWidget().reveal(); }); // implementation implies also expand + } + + ~NotificationHub() { }; + + + private: + /** external operation to find or allocate an log display widget */ + WidgetAllocator allocateWidget_; + + /** collaboration with a log display allocated elsewhere */ + WLink widget_; + + + widget::ErrorLogDisplay& + getWidget() + { + if (not widget_) + widget_.connect (allocateWidget_()); + return *widget_; + } + }; + + + +}}// namespace gui::ctrl +#endif /*GUI_CTRL_NOTIFICATION_HUB_H*/ diff --git a/src/gui/ctrl/panel-locator.hpp b/src/gui/ctrl/panel-locator.hpp index a618e4901..2331dc664 100644 --- a/src/gui/ctrl/panel-locator.hpp +++ b/src/gui/ctrl/panel-locator.hpp @@ -25,7 +25,21 @@ ** Access and query front-end to locate, access and place docking panels. ** This service is an implementation detail of the global WindowLocator and ** allows to abstract from the concrete top-level window when dealing with - ** docking panels on a conceptual level + ** docking panels on a conceptual level. + ** + ** @todo 8/2018 refactoring of docking panel handling is underway ///////////////////////////////////////TICKET # 1144 + ** The PanelLocator is planned to become the one central coordinating + ** manager, while the existing panel managers in each window will be + ** demoted to a mere DockArea, corresponding to a `gdl::Dock` + ** + ** @todo 8/2018 there is some overlap with the (not yet fully functional) + ** ViewLocator in the InteractionDirector. Right now, PanelLocator + ** fills some gaps here and offers preliminary solutions for some + ** lookup and allocation tasks ultimately to be performed on a more + ** abstract level through the ViewSpec-DSL. This corresponds to the + ** observation, that PanelLocator and WindowLocator are somewhat + ** tangled currently. The goal is to turn them into access services, + ** and shift the allocation logic to the aforementioned higher level. ** ** @see view-locator.hpp ** @see interaction-director.hpp @@ -36,17 +50,29 @@ #define GUI_CTRL_PANEL_LOCATOR_H #include "gui/gtk-base.hpp" +#include "gui/workspace/panel-manager.hpp" +#include "lib/format-string.hpp" +#include "lib/meta/util.hpp" #include "lib/nocopy.hpp" +#include #include #include namespace gui { -namespace workspace { class WorkspaceWindow; } +namespace workspace { + class WorkspaceWindow; +} +namespace panel { + class Panel; +} namespace ctrl { + namespace error = lumiera::error; + using panel::Panel; + using util::_Fmt; using std::list; @@ -68,13 +94,42 @@ namespace ctrl { public: PanelLocator (WindowList&); - private: + /** lookup or allocate "the" instance of the indicated Panel(subtype). + * While there can be multiple instances of some panel type in various + * windows, this function will look through a canonical search order + * to either retrieve the first instance found, or create a new one + * in the _primary window_. + * @return reference to the indicated Panel subclass (down-casted) + * @throw error::Invalid when requesting a target class not recognised + * as known docking panel entity. + * @deprecated 8/2018 the (planned) ViewLocator will perform essentially + * the same task, albeit on a more flexible and configurable level. + */ + template + PAN& find_or_create(); + + private: + panel::Panel& preliminary_impl_PanelLookup (int typeID); }; - /** */ + /** @remark delegating front-end for type safety and error handling. */ + template + inline PAN& + PanelLocator::find_or_create() + { + static_assert (std::is_base_of(), "Lumiera Panel subclass expected"); + + int typeID = workspace::PanelManager::findPanelID(); + if (-1 == typeID) + throw error::Invalid (_Fmt{"Requested type «%s» not usable as docking panel."} + % util::typeStr() + ,error::LERR_(WRONG_TYPE)); + + return dynamic_cast (preliminary_impl_PanelLookup (typeID)); + } diff --git a/src/gui/ctrl/state-recorder.hpp b/src/gui/ctrl/state-recorder.hpp index 47d15ee1e..b3b4ee8fc 100644 --- a/src/gui/ctrl/state-recorder.hpp +++ b/src/gui/ctrl/state-recorder.hpp @@ -59,6 +59,7 @@ #include "gui/ctrl/bus-term.hpp" #include "gui/ctrl/state-manager.hpp" #include "gui/ctrl/state-map-grouping-storage.hpp" +#include "include/ui-protocol.hpp" #include @@ -174,19 +175,27 @@ namespace ctrl { void recordState (ID uiElm, StateMark stateMark) { - if ("reset" == stateMark.idi.getSym()) + if (MARK_reset == stateMark.idi.getSym()) storage_.clearState (uiElm); else - if ("clearErr" == stateMark.idi.getSym()) - storage_.clearProperty (uiElm, "Error"); + if (MARK_clearErr == stateMark.idi.getSym()) + storage_.clearProperty (uiElm, string{MARK_Error}); else - if ("clearMsg" == stateMark.idi.getSym()) - storage_.clearProperty (uiElm, "Message"); + if (MARK_clearMsg == stateMark.idi.getSym()) + storage_.clearProperty (uiElm, string{MARK_Message}); else storage_.record (uiElm, stateMark); } private: + /** @remark depending on the response of the notified element, + * this might lead to a reentrant record() call, because the + * element might chose to send a _note_ message immediately, + * to record a state change, since it can not know this _is_ + * already the replay of a stored state note. However, this + * reentrant call is harmless, it just updates the existing + * entry with identical content. + */ void replayPropertiesOf (Record entry) { diff --git a/src/gui/ctrl/window-locator.cpp b/src/gui/ctrl/window-locator.cpp index 9dc8d7dda..0338b6b1e 100644 --- a/src/gui/ctrl/window-locator.cpp +++ b/src/gui/ctrl/window-locator.cpp @@ -116,7 +116,7 @@ namespace ctrl { } - /** similar to #findFocusWindow(), for the 'has_focus' property + /** similar to #findActiveWindow(), for the 'has_focus' property * @note likewise we return the first window in list, in case no window * has keyboard focus. This may very well be the case. */ @@ -176,6 +176,32 @@ namespace ctrl { } + /** + * @internal preliminary implementation of docking panel lookup and allocation. + * @todo 8/2018 the actual implementation shall eventually be provided by the ViewLocator + * on a higher abstraction level, and configurable through the ViewSpec-DSL. + * For now we use a braindead iterate-through-all-windows approach. + */ + panel::Panel& + PanelLocator::preliminary_impl_PanelLookup (int typeID) + { + REQUIRE (not isnil(windowList_)); + + for (auto window : windowList_) + if (window->getPanelManager().hasPanel (typeID)) + return window->getPanelManager().showPanel (typeID); + + // no instance of the desired panel type exists yet... + for (auto window : windowList_) + if (window->is_active()) + return window->getPanelManager().showPanel (typeID); + + // use the first window in list when none is active + return windowList_.front()->getPanelManager().showPanel (typeID); + } + + + }}// namespace gui::ctrl diff --git a/src/gui/demo-gui-roundtrip.cpp b/src/gui/demo-gui-roundtrip.cpp deleted file mode 100644 index f22834533..000000000 --- a/src/gui/demo-gui-roundtrip.cpp +++ /dev/null @@ -1,112 +0,0 @@ -/* - DemoGuiRoundtrip - placeholder to further progress of the GUI-Session connection - - 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 demo-gui-roundtrip.cpp - ** Dummy and scaffolding to help development of the UI - Session connection. - ** The Goal is to establish a full messaging round-trip between UI and Proc-Layer - ** - a menu entry - ** - which issues a Proc-Command - ** - which in turn accesses the Notification-facade - ** - and sends a "mark" message towards some UI demo widget - ** - which in turn displays the mark text - ** - ** @todo WIP as of 8/2017 ////////////////////////////////////////////////////////////////////////TICKET #1099 - ** - ** @see CallQueue_test - ** @see notification-service.hpp - ** - */ - -#include "gui/gtk-base.hpp" - -#include "lib/error.hpp" -#include "lib/depend.hpp" -#include "gui/ui-bus.hpp" -#include "gui/ctrl/ui-manager.hpp" -#include "lib/nocopy.hpp" - -#include - - - -//using lumiera::error::LUMIERA_ERROR_STATE; - -using std::string; - - -namespace gui { - - namespace { // implementation details - -//using util::isnil; - using std::string; - - } // (End) impl details - - - - - - /** - * Quick-n-dirty hack to establish a prototypic connection between Application Core and UI... - * - * @todo be commendable - * @todo don't misbehave - * - * @see SomeSystem - * @see CallQueue_test - */ - class DemoGuiRoundtrip - : util::NonCopyable - { - string nothing_; - - DemoGuiRoundtrip(); - ~DemoGuiRoundtrip(); - - friend class lib::DependencyFactory; - - public: - /** access point to set up the scaffolding. - * @internal this is a development helper and will be remoulded all the time. - */ - static lib::Depend instance; - - - /* == X interface for == */ - - /* == Adapter interface for == */ - -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////UNIMPLEMENTED :: TICKET #1099 - void setSolution (string const& solution = "") - { - UNIMPLEMENTED ("tbw"); - if (isDeaf()) - this->transmogrify (solution); - } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////UNIMPLEMENTED :: TICKET #1099 - }; - - - -} // namespace gui diff --git a/src/gui/dialog/test-control.hpp b/src/gui/dialog/test-control.hpp new file mode 100644 index 000000000..6934bf789 --- /dev/null +++ b/src/gui/dialog/test-control.hpp @@ -0,0 +1,362 @@ +/* + TEST-CONTROL.hpp - child window to trigger self tests and diagnostics + + Copyright (C) Lumiera.org + 2018, 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 test-control.hpp + ** Non-modal dialog window to trigger diagnostics and self test routines. + ** This window can be launched from the help menu as child window of the current + ** top-level workspace window and stays outside of the regular window and perspective + ** management of the Lumiera UI. This window allows for quick-n-dirty development + ** helper routines to be hooked up easily within the UI. Actions launched from here + ** perform within the same environment as regular user interactions. + ** + ** @todo as of 9/2018, this is a first rough draft, relevant for #1099 ////////////////////////////TICKET #1140 gradually augment the self-diagnostics controls in the UI + ** @todo this header also features a design draft how to simplify building notebook widgets. + ** Which could be polished and moved into a separate utility header eventually. + */ + + +#ifndef GUI_DIALOG_TEST_CONTROL_H +#define GUI_DIALOG_TEST_CONTROL_H + + +#include "gui/gtk-base.hpp" +#include "gui/dialog/dialog.hpp" +#include "gui/ctrl/bus-term.hpp" +#include "gui/model/tangible.hpp" +#include "include/ui-protocol.hpp" + +#include "proc/cmd.hpp" +#include "include/gui-notification-facade.h" +#include "proc/mobject/session/dummy-session-connection.hpp" + +#include "lib/test/test-helper.hpp" +#include "lib/diff/gen-node.hpp" +#include "lib/scoped-ptrvect.hpp" +#include "lib/format-string.hpp" +#include "lib/format-cout.hpp" +#include "lib/nocopy.hpp" +#include "lib/util.hpp" + +#include +#include + +namespace gui { +namespace dialog { + + using std::string; + using std::forward; + using lib::diff::GenNode; + using util::sanitise; + using util::_Fmt; + + + /** + * Building block for a segment within a dialog page. + * This helper widget provides the typical sub section of a dialog + * with several child widgets stacked vertically and enclosed within + * a frame with a label. The frame serves as the parent widget, as far + * as the widget hierarchy is concerned. Both parts are publicly accessible + * as members, while providing a shortcut for publishing the box. + */ + struct FrameBox + { + Gtk::Frame frame; + Gtk::Box box; + + FrameBox (cuString label, + Gtk::Orientation orientation =Gtk::ORIENTATION_VERTICAL) + : frame{label} + , box{orientation} + { + frame.add (box); + } + + operator Gtk::Widget&() { return frame; } + + void + pack_start(Gtk::Widget& child, Gtk::PackOptions options =Gtk::PACK_EXPAND_WIDGET, guint padding =0) + { + box.pack_start (child, options, padding); + } + }; + + /** explicitly named shortcut for the typical dialog page content holder */ + class Page + : public Gtk::Box + , util::NonCopyable + { + public: + Page() + : Gtk::Box{Gtk::ORIENTATION_VERTICAL} + { } + }; + + /** + * Helper widget to simplify construction and wiring of a [Notebook] widget. + * Gtk::Notebook is quite powerful container foundation to build complex dialog widgets with + * multiple pages on tabs. However, the construction, wiring and setup is notoriously tedious, + * due to the repetitiveness and the sheer amount of child widgets spread over various pages. + * + * This design draft is an attempt to mitigate the required boilerplate, without overly much + * obscuring the structure. The basic idea is to package each page into a locally defined child + * struct, which is actually heap allocated and managed automatically. This way, each child page + * gets its own namespace, and wiring to other components is made explicit by passing named ctor + * arguments -- while the overall structure of building and wiring of widgets stays close to the + * habits of [programming with GTKmm](https://developer.gnome.org/gtkmm-tutorial/stable/). + * - define the pages as custom widgets, typically just as locally known struct types + * - invoke #buildPage passing the type and tab label for each page + * - define the wiring of the components within a page in the page's ctor + * - possibly pass external dependencies for wiring into that ctor + * @note the page widgets are actually heap allocated and managed automatically + * @see gui::dialog::TestControl as a usage example + * + * [Notebook]: https://developer.gnome.org/gtkmm-tutorial/stable/sec-multi-item-containers.html.en#sec-notebook + */ + class Notebook + : public Gtk::Notebook + , lib::ScopedPtrVect + { + public: + template + Notebook& + buildPage (cuString pageLabel, ARGS&& ...ctorArgs) + { + Gtk::Notebook::append_page (this->manage (new PAG(forward (ctorArgs)...)) + ,pageLabel); + return *this; + } + }; + + + /** + * A complex, tabbed-notebook-style non-modal dialog window, + * dedicated to development, diagnostics and experimentation. + * The TestControl can be launched from Lumiera's "Help" menu, + * offers an (passive, up-link) [UI-Bus connection](\ref ui-bus.hpp) + * and simplifies adding pages for occasional experiments and diagnostics. + */ + class TestControl + : public Gtk::Dialog + { + using Bus = ctrl::BusTerm&; + + Bus uiBus_; + Notebook notebook_; + + + /** + * Ticket #1099 : perform a dummy round-trip to verify Proc-GUI integration. + * This routine invokes the command `test_meta_displayInfo` and friends down in Proc-Layer, + * passing the settings from the radio buttons to select the flavour of feedback, and the text + * for feedback content. The expected behaviour is for the invoked command to send a feedback + * via UI-Bus towards the ErrorLogDisplay within the InfoboxPanel. + */ + struct Page1 : Page + { + Gtk::Entry content_; + FrameBox seg_1_{_("log notification"), Gtk::ORIENTATION_HORIZONTAL}, + seg_2_{_("mark via UI-Bus")}; + Gtk::Button trig_1_, trig_2_, trig_3_, trig_4_; + Gtk::RadioButton level_info_{"Info"}, + level_warn_{"Warn"}, + level_erro_{"Error"}; + Gtk::Box markParam_; + Gtk::ComboBoxText actionID_{true}; // has free-text entry field + + int + getLogLevel() + { + return level_info_.get_active()? NOTE_INFO : + level_warn_.get_active()? NOTE_WARN : NOTE_ERROR; + } + + string + getContent() + { + return string{content_.get_text()}; + } + + string + getActionID() + { + return string{actionID_.get_entry_text()}; + } + + + Page1 (Bus bus) + { + content_.set_tooltip_markup (_("Ticket #1099:\n" + "text message content\n" + "when invoking a suitable action,\n" + "it will be passed down and sent back")); + trig_1_.set_use_underline(); + trig_1_.set_label ("_display text"); + trig_1_.property_xalign() = 0; + trig_1_.set_tooltip_markup (_("Trigger Proc-GUI roundtrip\n" + "Proc invokes GuiNotification::displayInfo")); + + level_warn_.join_group(level_info_); + level_erro_.join_group(level_info_); + level_warn_.set_active(); + + trig_2_.set_use_underline(); + trig_2_.property_xalign() = 0; + trig_2_.set_label ("mark _error"); + trig_2_.set_tooltip_markup (_("trigger Proc-command, which in turn\n" + "sends an error state mark via UI-Bus")); + + trig_3_.set_use_underline(); + trig_3_.property_xalign() = 0; + trig_3_.set_label ("mark _info"); + trig_3_.set_tooltip_markup (_("trigger Proc-command, which in turn\n" + "sends an info state mark via UI-Bus")); + + trig_4_.set_use_underline(); + trig_4_.set_label ("_mark"); + trig_4_.property_xalign() = 0; + trig_4_.set_tooltip_markup (_("trigger Proc-command, which in turn\n" + "sends an state mark message, using\n" + "the message action-ID from the combobox")); + actionID_.append (cuString{MARK_Flash}); + actionID_.append (cuString{MARK_reveal}); + actionID_.append (cuString{MARK_clearErr}); + actionID_.append (cuString{MARK_clearMsg}); + actionID_.append (cuString{MARK_expand}); + actionID_.append (cuString{MARK_reset}); + actionID_.set_active(1); + actionID_.set_tooltip_markup("select the specific action-ID\n" + "when sending a mark message.\n" + "note: can enter arbitrary ID"); + + markParam_.pack_start(trig_4_); + markParam_.pack_start(actionID_, Gtk::PACK_SHRINK); + + seg_1_.pack_start (trig_1_, Gtk::PACK_EXPAND_WIDGET); + seg_1_.pack_start (level_info_, Gtk::PACK_SHRINK); + seg_1_.pack_start (level_warn_, Gtk::PACK_SHRINK); + seg_1_.pack_start (level_erro_, Gtk::PACK_SHRINK); + + seg_2_.pack_start (trig_2_); + seg_2_.pack_start (trig_3_); + seg_2_.pack_start (markParam_); + + pack_start (content_, Gtk::PACK_SHRINK); + pack_start (seg_1_, Gtk::PACK_SHRINK); + pack_start (seg_2_, Gtk::PACK_SHRINK); + + // define the action triggers... + trig_1_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_meta_displayInfo, getLogLevel(), getContent())); }); + trig_2_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_meta_markError, getContent())); }); + trig_3_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_meta_markNote, getContent())); }); + trig_4_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_meta_markAction, getActionID(), getContent())); }); + } + }; + + + /** + * Ticket #1014 : populate the Timeline in the GUI. + * This page allows to send various diff messages up into the UI, to provide timeline content. + */ + struct Page2 : Page + { + Gtk::Entry dummy_; + FrameBox part_1_{_("populate"), Gtk::ORIENTATION_HORIZONTAL}, + part_2_{_("modify content")}; + Gtk::Button seq_1_, seq_2_; + Gtk::Button mut_1_; + + string + pickDummyID() + { + string dummyID = sanitise (dummy_.get_text()); + dummy_.set_text (string{_Fmt{"d%s%02d"} % lib::test::randStr(2) % (1 + rand() % 99)}); + return dummyID; + } + + + Page2 (Bus bus) + { + seq_1_.set_label ("Sequence 1"); + seq_1_.set_tooltip_markup (_("Push population diff up into the UI\n" + "provides the typical simple default timeline structure")); + + seq_2_.set_label ("Sequence 2"); + seq_2_.set_tooltip_markup (_("Push population diff up into the UI\n" + "provides a complex nested timeline structure")); + + dummy_.set_tooltip_markup (_("dummy ID value\n" + "used to build names in the generated content")); + dummy_.set_max_width_chars(12); + pickDummyID(); + + part_1_.pack_start (seq_1_, Gtk::PACK_SHRINK); + part_1_.pack_start (seq_2_, Gtk::PACK_SHRINK); + part_1_.pack_start (dummy_, Gtk::PACK_EXPAND_WIDGET); + + + mut_1_.set_label ("move elements"); + mut_1_.set_tooltip_markup (_("randomly manipulate temporal position of dummy elements")); + + part_2_.pack_start (mut_1_); + + pack_start (part_1_, Gtk::PACK_SHRINK); + pack_start (part_2_, Gtk::PACK_SHRINK); + + // define the action triggers... + seq_1_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_fake_injectSequence_1, pickDummyID())); }); + seq_2_.signal_clicked().connect( + [&]{ bus.act (model::commandMessage (proc::cmd::test_fake_injectSequence_2, pickDummyID())); }); + + mut_1_.signal_clicked().connect( + [&]{ cerr << "gelldaschaugst..." <pack_start (notebook_); + + // construct and wire the pages... + notebook_.buildPage (_("#1099"), uiBus_); + notebook_.buildPage (_("Populate"), uiBus_); + + show_all(); + notebook_.set_current_page(-1); + } + }; + + +}} // namespace gui::dialog +#endif /*GUI_DIALOG_TEST_CONTROL_H*/ diff --git a/src/gui/gtk-lumiera.cpp b/src/gui/gtk-lumiera.cpp index 674ef7f27..887824d89 100644 --- a/src/gui/gtk-lumiera.cpp +++ b/src/gui/gtk-lumiera.cpp @@ -99,8 +99,8 @@ namespace gui { { } - void - run (Subsys::SigTerm reportOnTermination) + string + run() { string errorMsgBuff; try @@ -122,18 +122,21 @@ namespace gui { if (lumiera_error_peek()) errorMsgBuff = string{lumiera_error()}; - reportOnTermination(&errorMsgBuff); // inform main thread that the GUI has been shut down. + return errorMsgBuff; } }; void - runGUI (Subsys::SigTerm& reportOnTermination) + runGUI (Subsys::SigTerm reportOnTermination) { - GtkLumiera{}.run (reportOnTermination); - } + string shutdownLog = GtkLumiera{}.run(); + + // inform main thread that the GUI has been shut down... + reportOnTermination (&shutdownLog); + }//(End) GUI-Thread. - } // (End) impl details + }//(End) impl details diff --git a/src/gui/id-scheme.hpp b/src/gui/id-scheme.hpp index 94bb0ca48..ff10add1b 100644 --- a/src/gui/id-scheme.hpp +++ b/src/gui/id-scheme.hpp @@ -63,6 +63,10 @@ namespace proc { namespace asset { class Timeline; + + namespace meta { + class ErrorLog; + } } } @@ -71,7 +75,7 @@ namespace gui { class TimelineController; } namespace widget { - class ErrorLogWidget; + class ErrorLogDisplay; } namespace idi { @@ -84,7 +88,7 @@ namespace gui { /* === primary component view types === */ using TimelineView = timeline::TimelineController; - using ErrorLogView = widget::ErrorLogWidget; + using ErrorLogView = widget::ErrorLogDisplay; /** @@ -103,7 +107,7 @@ namespace gui { template<> struct Descriptor { - ///////////////////////////////////////////////////////////////////////////////////////////TICKET #1105 : need a model placeholder to represent UI specific global entities + using Model = proc::asset::meta::ErrorLog; ///////////////////////////////////////////////////////////////////////////////////////////TICKET #1105 : consider use of a DSL to configure component view access // diff --git a/src/gui/interact/interaction-director.cpp b/src/gui/interact/interaction-director.cpp index 01e083c10..d4dcff6ff 100644 --- a/src/gui/interact/interaction-director.cpp +++ b/src/gui/interact/interaction-director.cpp @@ -41,13 +41,17 @@ #include "gui/workspace/workspace-window.hpp" #include "gui/ctrl/ui-state.hpp" #include "gui/setting/asset-controller.hpp" -#include "gui/timeline/timeline-controller.hpp" +#include "gui/panel/timeline-panel.hpp" +//#include "gui/timeline/timeline-widget.hpp" +//#include "gui/timeline/timeline-controller.hpp" /////////////////TODO still required? +#include "include/ui-protocol.hpp" #include "proc/mobject/session/root.hpp" #include "proc/asset/sequence.hpp" ///////////////////////////////////////////////////////////TICKET #1096 : avoid direct inclusion to reduce compile times #include "proc/mobject/session/fork.hpp" ///////////////////////////////////////////////////////////TICKET #1096 : avoid direct inclusion to reduce compile times #include "proc/cmd.hpp" #include "backend/real-clock.hpp" #include "lib/diff/tree-mutator.hpp" +#include "lib/format-string.hpp" #include "lib/format-obj.hpp" //#include "gui/ui-bus.hpp" //#include "lib/util.hpp" @@ -56,11 +60,16 @@ //#include //using util::isnil; +using util::_Fmt; //using std::list; //using std::shared_ptr; using lib::idi::EntryID; using lib::hash::LuidH; +using lib::diff::Rec; using lib::diff::TreeMutator; +using lib::diff::collection; +using lib::diff::LUMIERA_ERROR_DIFF_STRUCTURE; +using std::make_unique; using util::toString; @@ -126,13 +135,32 @@ namespace interact { void InteractionDirector::buildMutator (TreeMutator::Handle buffer) { -// using Attrib = std::pair; -// using lib::diff::collection; - buffer.create ( TreeMutator::build() - ); - unimplemented ("create a sensible binding between root-controller and root-model element"); + .attach (collection(timelines_) + .isApplicableIf ([&](GenNode const& spec) -> bool + { // »Selector« : require object-like sub scope + return spec.data.isNested(); + }) + .matchElement ([&](GenNode const& spec, TimelineGui const& elm) -> bool + { // »Matcher« : how to know we're dealing with the right timeline object + return spec.idi == ID{elm}; + }) + .constructFrom ([&](GenNode const& spec) -> TimelineGui + { // »Constructor« : what to do when the diff mentions a new entity + return injectTimeline (spec); + }) + .buildChildMutator ([&](TimelineGui& targetTimeline, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool + { // »Mutator« : how to apply the diff recursively to a nested scope + if (ID{targetTimeline} != subID) return false; + targetTimeline.buildMutator (buff); // - delegate to child(Timeline) to build nested TreeMutator + return true; + })) + .mutateAttrib(ATTR_fork, [&](TreeMutator::Handle buff) + { // »Attribute Mutator« : how enter an object field as nested scope + REQUIRE (assets_); + assets_->buildMutator(buff); + })); } @@ -283,6 +311,62 @@ namespace interact { { return globalCtx_.windowLoc_.findActiveWindow(); } + + + namespace { + /** + * The timeline is actually a front-end to a binding to a root track. + * For that reason, we always create the root-track representation alongside + * the timeline, and thus we need a very special INS message to create a timeline: + * - it must be a record (an "object") + * - a nested attribute with key ATTR_fork is mandatory + * - this nested attribute likewise needs to be a record + * - and must be tagged with TYPE_Fork + * More specifically, the usual way to deliver such a structure is not allowed here, + * which would be first to send an empty record and then to open and populate it. + * All the elements mentioned above need to be present right within the payload + * value of the INS message creating the timeline. This non-standard format is + * perfectly legal in our tree diff language (just requires a heap allocation + * behind the scenes to hold the nested data, but who cares?) + */ + ID + verifyDiffStructure_and_extract_RootTrack (GenNode const& spec) + { + if (not (spec.data.isNested() + and spec.data.get().hasAttribute(string{ATTR_fork}) + and TYPE_Fork == spec.data.get().get(string{ATTR_fork}).data.recordType() + ) ) + throw error::State (_Fmt{"When populating a new Timeline, a root track must be given immediately" + "nested into INS message. We got the following initialisation payload: %s"} + % spec + , LERR_(DIFF_STRUCTURE)); + + return spec.data.get().get(string{ATTR_fork}).idi; + } + } + + /** + * @internal allocate a new TimelineWidget and attach it as child. + * @remarks assuming the structure of the diff is adequate, we'll first create a proxy + * to manage this timeline. Then we use some service to find a suitable location to house + * a TimelineWidget. And finally we trigger creation of the widget, which in turn creates + * a TimelineController attached to the UI-Bus + */ + TimelineGui + InteractionDirector::injectTimeline (GenNode const& spec) + { + ID rootTrack = verifyDiffStructure_and_extract_RootTrack (spec); + TimelineGui anchorProxy{spec.idi, rootTrack}; + + globalCtx_.windowLoc_ + .locatePanel() + .find_or_create() + .addTimeline ( + anchorProxy.buildTimelineWidget (this->uiBus_)); + + return anchorProxy; + } + diff --git a/src/gui/interact/interaction-director.hpp b/src/gui/interact/interaction-director.hpp index d6d22d076..676bd05ff 100644 --- a/src/gui/interact/interaction-director.hpp +++ b/src/gui/interact/interaction-director.hpp @@ -54,6 +54,7 @@ #define GUI_INTERACT_INTERACTION_DIRECTOR_H #include "gui/model/controller.hpp" +#include "gui/timeline/timeline-gui.hpp" #include "lib/depend-inject.hpp" //#include @@ -80,6 +81,7 @@ namespace workspace { } namespace interact { + using lib::diff::GenNode; //using std::string; using std::unique_ptr; @@ -91,6 +93,8 @@ namespace interact { class FocusTracker; class ViewLocator; + using timeline::TimelineGui; + /** @@ -113,7 +117,7 @@ namespace interact { unique_ptr tracker_; // == Model globals == - using Timelines = std::vector>; + using Timelines = std::vector; using Assets = unique_ptr; using State = unique_ptr; @@ -150,6 +154,7 @@ namespace interact { private: workspace::WorkspaceWindow& getWorkspaceWindow(); + TimelineGui injectTimeline (GenNode const&); }; diff --git a/src/gui/interact/ui-coord-resolver.hpp b/src/gui/interact/ui-coord-resolver.hpp index 36686f954..27635c58f 100644 --- a/src/gui/interact/ui-coord-resolver.hpp +++ b/src/gui/interact/ui-coord-resolver.hpp @@ -53,7 +53,7 @@ ** followed by traversal of the coordinate spec and matching against a navigation path within the actual UI window ** configuration. This process might involve interpretation of some meta-symbols and interpolation of wildcards. ** - ** As indicated above, the coordinate resolver internally relies on a [context query interface](\ref LocationQuery), + ** As indicated above, the coordinate resolver internally relies on a [context query interface](\ref gui::interact::LocationQuery), ** to find out about existing windows, panels, views and tabs and to navigate the real UI structure. The actual ** implementation of this context query interface is backed by the \ref Navigator component exposed through the ** \ref InteractionDirector. diff --git a/src/gui/interact/view-locator.cpp b/src/gui/interact/view-locator.cpp index ecfb2e848..350864201 100644 --- a/src/gui/interact/view-locator.cpp +++ b/src/gui/interact/view-locator.cpp @@ -23,8 +23,8 @@ /** @file view-locator.cpp ** Implementation details of a machinery to allocate UI global component views. - ** Essentially this compilation unit hosts the implementation backing the [ViewSpec-DSL](view-spec-dsl.hpp) - ** and the [Location Solver](ui-location-solver.hpp) to drive evaluation of the standard location defaults, + ** Essentially this compilation unit hosts the implementation backing the [ViewSpec-DSL](\ref view-spec-dsl.hpp) + ** and the [Location Solver](\ref ui-location-solver.hpp) to drive evaluation of the standard location defaults, ** which are [hard-wired into the UI](\ref id-scheme.hpp). For this integration of various facilities and ** frameworks we rely on actual definitions for the DSL-tokens, which themselves are functors and tied by ** lambda-binding into the implementation side of ViewLocator; which both acts as a front-end to access diff --git a/src/gui/interact/view-spec-dsl.hpp b/src/gui/interact/view-spec-dsl.hpp index 120be8581..1a0dd6b2e 100644 --- a/src/gui/interact/view-spec-dsl.hpp +++ b/src/gui/interact/view-spec-dsl.hpp @@ -84,7 +84,7 @@ ** This results in calling a dedicated _view allocator_ to create a new view or re-use an existing one ** as appropriate. ** - ** @note the actual binding for the DSL tokens can be found in the [ViewLocator implementation](view-locator.cpp) + ** @note the actual binding for the DSL tokens can be found in the [ViewLocator implementation](\ref view-locator.cpp) ** ** @see ViewSpecDSL_test ** @see id-scheme.hpp diff --git a/src/gui/interact/wizard.cpp b/src/gui/interact/wizard.cpp index 8585092fa..accf9ccac 100644 --- a/src/gui/interact/wizard.cpp +++ b/src/gui/interact/wizard.cpp @@ -33,6 +33,9 @@ #include "gui/interact/wizard.hpp" #include "gui/interact/spot-locator.hpp" #include "gui/workspace/workspace-window.hpp" +#include "gui/panel/infobox-panel.hpp" +#include "gui/dialog/test-control.hpp" +#include "gui/ctrl/notification-hub.hpp" #include "gui/ctrl/global-ctx.hpp" #include "lib/format-string.hpp" //#include "lib/util.hpp" @@ -55,6 +58,7 @@ namespace interact { using boost::algorithm::is_any_of; using boost::algorithm::split; using workspace::WorkspaceWindow; + using ctrl::NotificationHub; using ctrl::GlobalCtx; using Gtk::AboutDialog; @@ -66,6 +70,14 @@ namespace interact { Wizard::Wizard (GlobalCtx& globals) : globalCtx_{globals} + , notificationHub_{new NotificationHub{getErrorLogID() + ,globals.uiBus_.getAccessPoint() + ,[&]() -> widget::ErrorLogDisplay& + { + return globalCtx_.windowLoc_.locatePanel() + .find_or_create() + .getLog(); + }} } { } @@ -104,4 +116,22 @@ namespace interact { } + /** + * Launch a non modal child window to trigger self-test actions. + * This is a tool for diagnostics and development. The operations exposed here + * allow to launch some hard wired actions and test routines, performing within + * the regular UI environment and on equal footing with user operated controls. + */ + void + Wizard::launchTestCtrl() + { + if (testControlWindow_) + testControlWindow_->present(); // just (re)show the existing window + else + testControlWindow_ = std::make_unique (globalCtx_.uiBus_.getAccessPoint(), + globalCtx_.windowLoc_.findActiveWindow()); + } + + + }}// namespace gui::interact diff --git a/src/gui/interact/wizard.hpp b/src/gui/interact/wizard.hpp index 0d541616b..525ef30b7 100644 --- a/src/gui/interact/wizard.hpp +++ b/src/gui/interact/wizard.hpp @@ -42,23 +42,36 @@ #ifndef GUI_INTERACT_WIZARD_H #define GUI_INTERACT_WIZARD_H -#include "gui/gtk-base.hpp" #include "lib/nocopy.hpp" +#include "lib/idi/entry-id.hpp" //#include -//#include +#include +namespace proc { + namespace asset { + namespace meta { + class ErrorLog; + + extern lib::idi::EntryID theErrorLog_ID; +} } } + namespace gui { + using ID = lib::idi::BareEntryID const&; + namespace ctrl { class GlobalCtx; + class NotificationHub; +} +namespace dialog { + class TestControl; } namespace interact { -// using std::unique_ptr; + using std::unique_ptr; // using std::string; -// class GlobalCtx; // class SpotLocator; @@ -73,14 +86,19 @@ namespace interact { : util::NonCopyable { ctrl::GlobalCtx& globalCtx_; + unique_ptr notificationHub_; + unique_ptr testControlWindow_; public: Wizard (ctrl::GlobalCtx&); ~Wizard (); void show_HelpAbout(); - private: + void launchTestCtrl(); + static ID getErrorLogID() { return proc::asset::meta::theErrorLog_ID; } + + private: }; diff --git a/src/gui/lumiera-light-theme-complement.css b/src/gui/lumiera-light-theme-complement.css new file mode 100644 index 000000000..1539e5609 --- /dev/null +++ b/src/gui/lumiera-light-theme-complement.css @@ -0,0 +1,33 @@ +/* + lumiera-light-theme-complement.css - minimal supplement for GTK-3 light style sheet + + Copyright (C) Lumiera.org + 2018, Hermann Vosseler + + Styles and Graphics of the Lumiera GUI can be used and redistributed + under the the terms of the GNU General Public License version 2 or + above, or (at your option) under Creative Commons CC-By-SA. + +* ********************************************************/ + + +/* CSS stylesheet to provide the additional definitions + * required to run the Lumiera GTK UI with a typical + * "light" standard system theme + */ + + + + +/* ---------- Styles for Lumiera Widgets ---------- */ + + +/* ---------- Styles for special markup ---------- */ + + .indication_flash, + .indication_flash * { + background-color: LightYellow; +} + .indication_flash text { + border: 4px solid red; +} diff --git a/src/gui/gtk-lumiera.css b/src/gui/lumiera.css similarity index 95% rename from src/gui/gtk-lumiera.css rename to src/gui/lumiera.css index 20ffe9a52..38b117352 100644 --- a/src/gui/gtk-lumiera.css +++ b/src/gui/lumiera.css @@ -1,5 +1,5 @@ /* - gtk-lumiera.css - GTK-3 style sheet for the Lumiera GUI + lumiera.css - GTK-3 style sheet for the Lumiera GUI Copyright (C) Lumiera.org 2014, Hermann Vosseler @@ -35,13 +35,12 @@ * { - engine: adwaita; - border-width: 1px; border-style: outset; border-color: shade(@bg_color, 0.80); - color: inherit; /* by default inherit content colour from parent widget */ + color: inherit; /* by default inherit content colour from parent widget */ + background-color: inherit; } GtkWindow { @@ -276,3 +275,14 @@ class "gtkmm__CustomObject_TimelineRuler" style:highest "timeline_ruler" class "gtkmm__CustomObject_TimelineHeaderWidget" style:highest "timeline_header_widget" */ + + +/* ---------- Styles for special markup ---------- */ + + .indication_flash, + .indication_flash * { + background-color: shade(@hover_bg_color, 1.6); +} + .indication_flash text { + border: 4px solid red; +} diff --git a/src/gui/model/controller.hpp b/src/gui/model/controller.hpp index 8ebdfea29..a6df8f52d 100644 --- a/src/gui/model/controller.hpp +++ b/src/gui/model/controller.hpp @@ -25,6 +25,8 @@ ** Common Abstraction of all sub-controller, coordinated by the UI-Bus. ** ** @todo as of 1/2015 this is complete WIP-WIP-WIP + ** @todo as of 8/2018 we should consider to distill a common default implementation usable for most controllers. + ** especially, there might be a default (NOP) implementation of the diff mutator builder ** ** @see model::Tangible ** @see gui::UIBus @@ -68,24 +70,6 @@ namespace model { UNIMPLEMENTED ("Controller reset"); } - virtual bool - doExpand (bool yes) override - { - UNIMPLEMENTED ("Controller doExpand"); - } - - virtual void - doReveal (ID child) override - { - UNIMPLEMENTED ("Controller doReveal"); - } - - virtual void - doRevealYourself() override - { - UNIMPLEMENTED ("Controller doRevealYourself"); - } - virtual bool doMsg (string text) override { @@ -116,10 +100,16 @@ namespace model { UNIMPLEMENTED ("Controller doFlash"); } + protected: + /** default handler for all generic mark messages. Forwards to Tangible::doMark + * @todo is there any default implementation for special messages, + * which might be eligible as a base class implementation?? + */ virtual void doMark (GenNode const& mark) override { - UNIMPLEMENTED ("Controller doMark"); + // forward to default handler + Tangible::doMark (mark); } diff --git a/src/gui/model/expander-revealer.hpp b/src/gui/model/expander-revealer.hpp new file mode 100644 index 000000000..6bbe689dc --- /dev/null +++ b/src/gui/model/expander-revealer.hpp @@ -0,0 +1,189 @@ +/* + EXPANDER-REVEALER.hpp - functor components for standard UI element actions + + Copyright (C) Lumiera.org + 2018, 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 expander-revealer.hpp + ** Helper components to implement some standard UI-element actions by installing a functor. + ** The protocol for operating UI elements connected to the [UI-Bus](\ref ui-bus.hpp) includes + ** some generic operations, which need to be concretised for each actual usage situation. + ** Moreover, there are many situations where implementing those actions does not make + ** much sense, and thus they need to be treated as optional. + ** - some widgets or elements can be expanded or collapsed, to show more information + ** or to save screen real estate. Such widgets also expose an _expansion_ state. + ** - sometimes it is necessary to bring a given widget into sight, be it to show + ** the effect of some processing, or be it to indicate an error situation. + ** + ** ## Usage in the default implementation + ** + ** The base class of all [tangible UI elements](\ref Tangible) provides a default implementation + ** for these generic interaction mechanisms: It offers slots to connect UI signals against, and + ** it understands the \em `mark` messages `"expand"` and `"reveal"`. These are implemented + ** by delegating to the \ref Expander and \ref Revealer functors respectively. Moreover, this + ** default implementation automatically detects a resulting state change and emits an appropriate + ** \em `note` message on the UI-Bus, so to make those state changes persistent. However, in order + ** to supply an concrete implementation, depending on the circumstances, either the widget itself + ** or a parent container holding the element needs to install lambdas into those functor holders, + ** to detail what actually needs to be done. E.g. it is quite common to implement the "expand" + ** functionality by wrapping the widget into a `Gtk::Expander`. Which effectively means that + ** the widget itself is not able to expand itself, since this `Gtk::Expander` container widget + ** lives within the parent widget to hold the element. So this parent needs to install a + ** lambda when establishing the child element, and bind it to the `Gtk::Expander::set_expanded` + ** property, and the corresponding `Gtk::Expander::get_expanded` to retrieve the current + ** _expansion state_. However, to the contrary, some widgets do implement their _expansion state_ + ** all by themselves, e.g. by switching the presentation layout. Those widgets will have to + ** install an appropriate Expander functor within their constructor. + ** + ** @see [UI-Element protocol](\ref tangible.hpp) + ** @see [simple test case](\ref AbstractTangible_test::revealer()) + ** @see [usage example](\ref error-log-display.hpp) + ** + */ + + +#ifndef GUI_MODEL_EXPANDER_REVEALER_H +#define GUI_MODEL_EXPANDER_REVEALER_H + + +#include "lib/error.hpp" + +#include +#include + + +namespace gui { +namespace model { + + using std::move; + + + /** + * Functor component to support the default implementation of expanding/collapsing. + * It is built based on closures (lambdas) to explicate how to detect the expansion + * state of the widget in question and how actively to expand or collapse it. + * A default constructed Expander disables this feature -- which can be detected + * through the #canExpand() function. + */ + class Expander + { + public: + using ProbeFun = std::function; + using ChangeFun = std::function; + + private: + ProbeFun probeState_; + ChangeFun changeState_; + + public: + Expander() { } + Expander(ProbeFun detectCurrExpansionState, ChangeFun expand_collapse) + : probeState_{move (detectCurrExpansionState)} + , changeState_{move (expand_collapse)} + { + ENSURE (canExpand()); + } + + bool + canExpand() const + { + return bool{probeState_} + and bool{changeState_}; + } + + explicit + operator bool() const + { + REQUIRE (canExpand()); + return probeState_(); + } + + bool + operator() (bool shallExpand) + { + REQUIRE (canExpand()); + bool currState = probeState_(); + if (currState != shallExpand) + changeState_(shallExpand); + return currState; + } + + /* === alternate "expressive" API === */ + bool + expand (bool yes =true) + { + return this->operator() (yes); + } + + bool + collapse() + { + return expand (false); + } + + bool + isExpanded() const + { + return bool{*this}; + } + }; + + + + /** + * Functor component to support the default implementation of revealing an UI-Element. + * It is built based on a closure (lambda) detailing what actually to do in order + * to bring a given UI element into sight. A default constructed Revealer disables + * this feature, which can be detected through the #canReveal() function. + */ + class Revealer + { + public: + using RevealeItFun = std::function; + + private: + RevealeItFun revealIt_; + + public: + Revealer() { } + Revealer(RevealeItFun how_to_uncover_the_element) + : revealIt_{move (how_to_uncover_the_element)} + { + ENSURE (canReveal()); + } + + bool + canReveal() const + { + return bool{revealIt_}; + } + + void + operator()() + { + REQUIRE (canReveal()); + revealIt_(); + } + }; + + + +}} // namespace gui::model +#endif /*GUI_MODEL_EXPANDER_REVEALER_H*/ diff --git a/src/gui/model/flash-deco.hpp b/src/gui/model/flash-deco.hpp new file mode 100644 index 000000000..15889a76c --- /dev/null +++ b/src/gui/model/flash-deco.hpp @@ -0,0 +1,90 @@ +/* + FLASH-DECO.hpp - widget decorator to add a visual flash action + + Copyright (C) Lumiera.org + 2018, 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 flash-deco.hpp + ** Decorator to add the ability to display a visual flash action to a GTK widget. + ** The protocol for operating UI elements connected to the [UI-Bus](\ref ui-bus.hpp) includes + ** a message to make the receiving widget flash for a short time, to mandate the user's attention. + ** The visual change itself is effected by adding a CSS class, while adding a timeout callback + ** to revert to normal display after a short timespan. In itself, this modification sequence + ** is entirely generic, and can thus be added by decorating the widget to affect; moreover, + ** adding such a callback need to be done properly, to avoid a crash in case the widget + ** is destroyed during the timeout period. + ** + ** @see [UI-Element protocol](\ref tangible.hpp) + ** @see [usage example](\ref error-log-display.hpp) + ** + */ + + +#ifndef GUI_MODEL_FLASH_DECO_H +#define GUI_MODEL_FLASH_DECO_H + + +#include "gui/gtk-base.hpp" +#include "gui/style-scheme.hpp" + + +namespace gui { +namespace model { + + + /** + * Decorator for a Gtk::Widget to add a visual flash action. + * @tparam WIT type of the target widget to decorate. All ctors are passed through. + * @remark invoke the #flash function to trigger + * @see \ref gui::CSS_CLASS_FLASH + */ + template + class FlashDeco + : public WIT + { + static_assert (std::is_base_of() + ,"wrapped target type required to be a Gtk::Widget"); + public: + using WIT::WIT; + + void + flash() + { + auto styleContext = this->get_style_context(); + styleContext->add_class (CSS_CLASS_FLASH); + + Glib::signal_timeout() + .connect_once (sigc::mem_fun(*this, &FlashDeco::flashback) + ,STYLE_FLASH_DURATION_in_ms + ,Glib::PRIORITY_LOW); // after all pending drawing tasks + } + + private: + void + flashback() + { + auto styleContext = this->get_style_context(); + styleContext->remove_class (CSS_CLASS_FLASH); + } + }; + + +}} // namespace gui::model +#endif /*GUI_MODEL_FLASH_DECO_H*/ diff --git a/src/gui/model/tangible.cpp b/src/gui/model/tangible.cpp index 1c5f4de5e..0070bc029 100644 --- a/src/gui/model/tangible.cpp +++ b/src/gui/model/tangible.cpp @@ -25,7 +25,7 @@ ** Common base implementation of all tangible and connected interface elements. ** ** @see abstract-tangible-test.cpp - ** @see [explanation of the fundamental interactions](tangible.hpp) + ** @see [explanation of the fundamental interactions](\ref tangible.hpp) ** */ @@ -33,6 +33,7 @@ #include "gui/model/tangible.hpp" #include "gui/model/widget.hpp" #include "gui/model/controller.hpp" +#include "include/ui-protocol.hpp" #include "lib/diff/gen-node.hpp" @@ -60,7 +61,7 @@ namespace model { Tangible::reset() { if (this->doReset()) - uiBus_.note (GenNode{"reset", true}); + uiBus_.note (GenNode{string{MARK_reset}, true}); } @@ -82,7 +83,7 @@ namespace model { Tangible::clearErr() { if (this->doClearErr()) - uiBus_.note (GenNode{"clearErr", true}); + uiBus_.note (GenNode{string{MARK_clearErr}, true}); } @@ -93,7 +94,7 @@ namespace model { Tangible::clearMsg() { if (this->doClearMsg()) - uiBus_.note (GenNode{"clearMsg", true}); + uiBus_.note (GenNode{string{MARK_clearMsg}, true}); } @@ -125,7 +126,7 @@ namespace model { Tangible::markMsg (string message) { if (this->doMsg (message)) - uiBus_.note (GenNode{"Message", message}); + uiBus_.note (GenNode{string{MARK_Message}, message}); } @@ -136,7 +137,7 @@ namespace model { Tangible::markErr (string error) { if (this->doErr (error)) - uiBus_.note (GenNode("Error", error)); + uiBus_.note (GenNode{string{MARK_Error}, error}); } @@ -148,12 +149,15 @@ namespace model { * behaviour. If this virtual method returns `true`, the * state change is deemed relevant and persistent, and * thus a "state mark" is sent on the UI-Bus. + * @remark a default implementation of ::doExpand() is provided, + * based on installing an \ref Expander functor through + * the [configuration function](\ref #installExpander). */ void Tangible::slotExpand() { if (this->doExpand(true)) - uiBus_.note (GenNode("expand", true)); + uiBus_.note (GenNode{string{MARK_expand}, true}); } @@ -165,24 +169,59 @@ namespace model { Tangible::slotCollapse() { if (this->doExpand(false)) - uiBus_.note (GenNode("expand", false)); + uiBus_.note (GenNode{string{MARK_expand}, false}); } /** - * @todo 12/2015 not clear yet what needs to be done - * @remarks the intention is to request the given child - * to be brought into sight. We need to set up some kind - * of children registration, but better not do this in - * a completely generic fashion, for danger of overengineering. - * Moreover, it is not clear yet, who will issue this request - * and at which element the initial request can/will be targeted. + * generic default implementation of the expand/collapse functionality. + * Based on the #expand_ functor, which needs to be [configured](\ref installExpander()) + * explicitly to enable this functionality. + * @return `true` if the actual expansion state has been changed. + */ + bool + Tangible::doExpand (bool yes) + { + if (not expand_.canExpand()) + return false; + bool oldState = expand_(yes); + return oldState != yes; // actually changed + } + + + /** + * Cause the element to be brought into sight. + * This is a generic Slot to connect UI signals against; + * the same action can also be triggered by sending a **mark** + * message over the UI-Bus with the symbol "`reveal`". + * @note this is an optional feature and requires the actual widget or controller + * either to override the ::doReveal() extension point, or to + * [install a suitable closure](\ref installRevealer()). Typically this + * is not in itself a persistent state change; however, it might incur + * expanding some widgets, which is recorded as persistent UI state. + * @remarks the intention is to make a specific element visible, e.g. to reveal + * the effect of some operation, or to mark a serious error condition. + * Implementing this is by no means trivial, since it involves the + * possibly recursive collaboration with enclosing container widgets, + * and maybe even to scroll to a given canvas position. */ void - Tangible::slotReveal(ID child) + Tangible::slotReveal() { - this->doReveal(child); - this->doRevealYourself(); + this->doReveal(); + } + + + /** + * generic default implementation of the "reveal" functionality. + * Based on the #reveal_ functor, which needs to be [configured](\ref installRevealer()) + * explicitly to enable this functionality. + */ + void + Tangible::doReveal() + { + if (reveal_.canReveal()) + reveal_(); } @@ -207,13 +246,13 @@ namespace model { void Tangible::mark (GenNode const& stateMark) { - if (stateMark.idi.getSym() == "Flash") + if (stateMark.idi.getSym() == MARK_Flash) this->doFlash(); else - if (stateMark.idi.getSym() == "Error") + if (stateMark.idi.getSym() == MARK_Error) this->markErr (stateMark.data.get()); else - if (stateMark.idi.getSym() == "Message") + if (stateMark.idi.getSym() == MARK_Message) this->markMsg (stateMark.data.get()); else this->doMark(stateMark); @@ -229,10 +268,10 @@ namespace model { * It is up to the concrete element to give this a tangible meaning, e.g. a track * might switch to detail view and a clip might reveal attached effects. * - *reset* restores the element to the hard wired default, by invoking `doReset()` - * - *revealYourself* prompts the element to take the necessary actions to bring - * itself into view. There is no requirement for an element to even implement - * this call, but those which do typically know some kind of _"parent object"_ - * to forward this request, by invoking `doReveal(myID)` on this parent. + * - *reveal* prompts the element to take the necessary actions to bring itself + * into view. There is no requirement for an element to even implement this call, + * but those which do typically know some kind of _"parent object"_ to forward + * this request, by invoking `doReveal(myID)` on this parent. * For instance, a clip might ask the enclosing track, which in turn might * call the enclosing timeline display for help, resulting in a scroll action * to bring the clip into sight. @@ -242,20 +281,23 @@ namespace model { void Tangible::doMark (GenNode const& stateMark) { - if (stateMark.idi.getSym() == "expand") - this->doExpand (stateMark.data.get()); + if (stateMark.idi.getSym() == MARK_expand) + { + if (this->doExpand (stateMark.data.get())) + uiBus_.note (GenNode(string{MARK_expand}, true)); // possibly reentrant (yet harmless) + } else - if (stateMark.idi.getSym() == "reset") + if (stateMark.idi.getSym() == MARK_reset) this->reset(); else - if (stateMark.idi.getSym() == "clearMsg") + if (stateMark.idi.getSym() == MARK_clearMsg) this->clearMsg(); else - if (stateMark.idi.getSym() == "clearErr") + if (stateMark.idi.getSym() == MARK_clearErr) this->clearErr(); else - if (stateMark.idi.getSym() == "revealYourself") - this->doRevealYourself(); + if (stateMark.idi.getSym() == MARK_reveal) + this->doReveal(); } diff --git a/src/gui/model/tangible.hpp b/src/gui/model/tangible.hpp index fcaf2f1f9..f8e06fe99 100644 --- a/src/gui/model/tangible.hpp +++ b/src/gui/model/tangible.hpp @@ -24,7 +24,7 @@ /** @file tangible.hpp ** Abstraction: a tangible element of the User Interface. ** This is a generic foundation for any elements of more than local relevance - ** within the Lumiera UI.
Any such element is connected to the [UI-Bus](ui-bus.hpp). + ** within the Lumiera UI.
Any such element is connected to the [UI-Bus](\ref ui-bus.hpp). ** ** ## rationale ** @@ -96,7 +96,7 @@ ** ** And finally, there are the _essential updates_ -- any changes in the model _for real._ ** These are sent as notifications just to some relevant top level element, expecting this element - ** to request a [diff](tree-diff.hpp) and to mutate contents into shape recursively. + ** to request a [diff](\ref tree-diff.hpp) and to mutate contents into shape recursively. ** ** ## Interactions ** @@ -105,7 +105,7 @@ ** - **act**: send a \ref GenNode representing the action ** - **note**: _send_ a GenNode representing the _state mark_ ** - **mark**: _receive_ a GenNode representing the _feedback,_ a replayed _state mark_ or _generic message._ - ** - **diff**: ask to retrieve a diff, which + ** - **diff**: prompt the element to retrieve a diff, which ** - either is an incremental status update ** - or is a from-scratch reconfiguration ** @@ -133,6 +133,7 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" #include "gui/ctrl/bus-term.hpp" +#include "gui/model/expander-revealer.hpp" #include "lib/diff/diff-mutable.hpp" #include "lib/idi/entry-id.hpp" #include "lib/symbol.hpp" @@ -153,9 +154,9 @@ namespace model { * Interface common to all UI elements of relevance for the Lumiera application. * Any non-local and tangible interface interaction will at some point pass through * this foundation element, which forms the joint and attachment to the UI backbone, - * which is the [UI-Bus](ui-bus.hpp). Any tangible element acquires a distinct identity + * which is the [UI-Bus](\ref ui-bus.hpp). Any tangible element acquires a distinct identity * and has to be formed starting from an already existing bus nexus. - * @see [explanation of the basic interactions](tangible.hpp) + * @see [explanation of the basic interactions](\ref tangible.hpp) * @warning Tangible is `NonCopyable` for good reason: the UI-Bus Nexus adds a direct * reference into the routing table, tied to the given Tangible's ID (identity. * Consequently you must not store tangibles in STL containers, since these @@ -175,9 +176,14 @@ namespace model { ctrl::BusTerm uiBus_; + Expander expand_; + Revealer reveal_; + Tangible(ID identity, ctrl::BusTerm& nexus) : uiBus_{nexus.attach(identity, *this)} + , expand_{} + , reveal_{} { } public: @@ -196,21 +202,22 @@ namespace model { void slotExpand(); void slotCollapse(); - - void slotReveal(ID child); + void slotReveal(); void markFlash(); void markMsg (string message); void markErr (string error); void mark(GenNode const&); + void installExpander (Expander::ProbeFun, Expander::ChangeFun); + void installRevealer (Revealer::RevealeItFun); + protected: virtual bool doReset() =0; virtual bool doClearMsg() =0; virtual bool doClearErr() =0; - virtual bool doExpand (bool yes) =0; - virtual void doReveal (ID child) =0; - virtual void doRevealYourself () =0; + virtual bool doExpand (bool yes); + virtual void doReveal (); virtual bool doMsg (string) =0; virtual bool doErr (string) =0; @@ -222,30 +229,74 @@ namespace model { /** override default size traits for diff application. - * @remarks this value here is hard coded, base on what + * @remarks this value here is hard coded, based on what * can be expected for diff application to UI elements. */ friend constexpr size_t treeMutatorSize (const Tangible*) { - return 256; + return 512; } }; + /** convenience shortcut to build a message suitable for command invocation + * @param args... sequence of arguments to be packaged into a lib::diff::Rec for invocation + */ + template + inline lib::diff::GenNode + commandMessage (Symbol cmdID, ARGS&&... args) + { + using lib::diff::Rec; + using lib::diff::GenNode; + using GenNodeIL = std::initializer_list; + + + return GenNode (string{cmdID}, + Rec(Rec::TYPE_NIL_SYM + ,GenNodeIL{} + ,GenNodeIL {std::forward (args)...})); + } // not typed, no attributes, all arguments as children + + /** convenience shortcut to issue a command with several arguments */ template inline void Tangible::invoke (Symbol cmdID, ARGS&&... args) { - using GenNodeIL = std::initializer_list; - - invoke (cmdID, - Rec(Rec::TYPE_NIL_SYM - ,GenNodeIL{} - ,GenNodeIL {std::forward (args)...})); - } // not typed, no attributes, all arguments as children + uiBus_.act (commandMessage (cmdID, std::forward (args)...)); + } + + + /** + * Configure the (optional) functionality to expand or collapse the UI-Element. + * @param detectCurrExpansionState a lambda or function to retrieve if the element is currently expanded + * @param howto_expand_collapse a lambda or function to switch the element's expansion state + * @note unless this setup function is invoked, the expand/collapse functionality remains disabled; + * invoking the #slotExpand() or sending **mark** "`expand`" messages via UI-Bus has no effect then. + */ + inline void + Tangible::installExpander (Expander::ProbeFun detectCurrExpansionState, + Expander::ChangeFun howto_expand_collapse) + { + expand_ = Expander{move (detectCurrExpansionState), move (howto_expand_collapse)}; + } + + + /** + * Configure the (optional) functionality to bring the UI-Element into sight. + * @param how_to_uncover_the_element a lambda or function to actually cause the necessary actions. + * @note unless this setup function is invoked, the "`reveal`" functionality remains disabled. + * Typically this setup will be done by an owning parent container, binding to some internals + * and also recursively invoking the "`reveal`" action on the container. + */ + inline void + Tangible::installRevealer (Revealer::RevealeItFun how_to_uncover_the_element) + { + reveal_ = Revealer{move (how_to_uncover_the_element)}; + } + diff --git a/src/gui/model/w-link.hpp b/src/gui/model/w-link.hpp new file mode 100644 index 000000000..a71e9b6f5 --- /dev/null +++ b/src/gui/model/w-link.hpp @@ -0,0 +1,244 @@ +/* + W-LINK.hpp - smart-link to GTK widget + + Copyright (C) Lumiera.org + 2018, 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 w-link.hpp + ** A smart link to an GTK widget with automatic disconnection. + ** Allows to hold a connection to a [trackable] without taking ownership. + ** The link is statefull, can be reconnected, and automatically transitions + ** into disconnected state when the target dies. The link state can be tested + ** by `bool` conversion. WLink objects are fully copyable, and each copy has + ** its own attachment state and can be reconnected independently. + ** + ** The purpose of WLink is to support collaborations between controllers and + ** widgets or between widgets. Whenever some logic works with or relies on + ** some other UI entity -- which it might even _"allocate"_ -- without taking + ** ownership, the relation can be implemented with a WLink. + ** + ** @warning this class is not threadsafe, because lib SigC++ is not either, + ** and it can only be used reliably from within the GUI thread. + ** @remarks the lib SigC++ is used in GTKmm for handling "Signals" and invoking + ** "Slots" (i.e. functors) as result of such a signal. Such is dangerous + ** within the context of an interactive UI, since widgets might go away + ** and be destroyed due to user interaction. To deal with this problem, + ** GTKmm derives all widgets from [trackable], which offers an auto + ** deregistration callback, when the object is destroyed, so any + ** remaining signal connections can be dropped to avoid calling + ** a dead functor. We make use of the same mechanism here to + ** install a callback to invalidate this smart-handle. + ** + ** [trackable]: https://developer.gnome.org/libsigc++/stable/structsigc_1_1trackable.html#details "`sigc::trackable`" + ** + ** @see \ref WLink_test + ** @see \ref NotificationHub (usage example) + ** + */ + + +#ifndef GUI_MODEL_W_LINK_H +#define GUI_MODEL_W_LINK_H + + +#include "lib/error.hpp" +#include "lib/format-string.hpp" + +#include +#include + + +namespace gui { +namespace model { + + namespace error = lumiera::error; + using error::LUMIERA_ERROR_BOTTOM_VALUE; + + using util::_Fmt; + + + /** + * Managed link to a `sigc::trackable` UI widget, without taking ownership. + * Automatically installs a callback to switch this link into detached state + * when the target (widget) is destroyed. + * @tparam TAR the actual target widget type, which can be forward declared, + * until the point where the widget is actually constructed or attached. + * This target type must derive from `sigc::trackable` + * @warning _not_ threadsafe + * @note only EX_SANE, since attaching, detaching and swapping might throw. + */ + template + class WLink + { + + TAR* widget_; + + + public: + ~WLink() + { + try { + this->clear(); + } + ERROR_LOG_AND_IGNORE (gui, "Detaching managed WLink from Widget") + } + WLink() noexcept + : widget_{nullptr} + { } + + explicit + WLink (TAR& targetWidget) + : widget_{attachTo (targetWidget)} + { } + + WLink (WLink const& r) + : widget_{r.widget_? attachTo(*r.widget_) : nullptr} + { } + + WLink (WLink && rr) + : widget_{rr.widget_? attachTo(*rr.widget_) : nullptr} + { + rr.clear(); + } + + WLink& + operator= (WLink other) ///< @warning only EX_SANE + { + swap (*this, other); + return *this; + } + + /** swap the pointees, including callback registration. + * @warning only EX_SANE. + * Might leave the following intermediary states + * - only r was detached + * - both r and l are detached + * - both detached, but only l attached to the former target of r, + * while the former target of l is now completely detached. + */ + friend void + swap (WLink& l, WLink& r) + { + TAR* tl = l.widget_; + TAR* tr = r.widget_; + if (tl == tr) return; + r.clear(); + l.clear(); + if (tr) l.widget_ = l.attachTo (*tr); + if (tl) r.widget_ = r.attachTo (*tl); + } + + + explicit + operator bool() const + { + return bool{widget_}; + } + + TAR& + operator* () const + { + __ensureAlive(); + return *widget_; + } + + TAR* + operator-> () const + { + __ensureAlive(); + return widget_; + } + + + bool isValid() const { return bool{*this}; } + bool isActive() const { return bool{*this}; } + + + /** detach and deactivate this link + * @note EX_STRONG (but only under the assumption that also + * sigc::trackable::remove_destroy_notify_cllback is EX_STRONG) + */ + void + clear() + { + if (widget_) + widget_->remove_destroy_notify_callback (&widget_); + widget_ = nullptr; + } + + /** (re)connect this smart link to the given target. + * Any previously existing link is detached beforehand + * @note EX_SANE only (assuming `sigc::trackable` is sane) + * Might leave this WLink in disconnected state when throwing + * @throws error::External when registration with `sigc::trackable` fails + */ + void + connect (TAR& otherTarget) + { + if (widget_ == &otherTarget) return; + clear(); + widget_ = attachTo (otherTarget); + } + + private: + void + __ensureAlive() const + { + if (not widget_) + throw error::State ("zombie widget encountered" + , LERR_(BOTTOM_VALUE)); + } + + /** @internal installs the necessary callback + * to detach this link in case the target is destroyed + * @note EX_STRONG + */ + TAR* + attachTo (TAR& target) + { + static_assert (std::is_base_of() + ,"target type required to be sigc::trackable"); + + try { + target.add_destroy_notify_callback (&widget_ + ,[](void* p) + { + TAR* & widgetPtr = *static_cast(p); + ASSERT (widgetPtr); + widgetPtr = nullptr; + return p; + }); + return ⌖ + } + catch (std::exception& problem) + { + throw error::External (problem, _Fmt{"WLink could not attach to %s. Problem is %s"} % target % problem); + } + catch (...) + { + ERROR (gui, "Unknown exception while attaching WLink"); + throw error::External (_Fmt{"WLink could not attach to %s due to unidentified Problems"} % target); + } + } + }; + + +}} // namespace gui::model +#endif /*GUI_MODEL_W_LINK_H*/ diff --git a/src/gui/model/widget.hpp b/src/gui/model/widget.hpp index 5302cfbd5..2abfa875e 100644 --- a/src/gui/model/widget.hpp +++ b/src/gui/model/widget.hpp @@ -67,24 +67,6 @@ namespace model { UNIMPLEMENTED ("Widget reset"); } - virtual bool - doExpand (bool yes) override - { - UNIMPLEMENTED ("Widget doExpand"); - } - - virtual void - doReveal (ID child) override - { - UNIMPLEMENTED ("Widget doReveal"); - } - - virtual void - doRevealYourself() override - { - UNIMPLEMENTED ("Widget doRevealYourself"); - } - virtual bool doMsg (string text) override { diff --git a/src/gui/notification-service.cpp b/src/gui/notification-service.cpp index 0807df973..41cff9361 100644 --- a/src/gui/notification-service.cpp +++ b/src/gui/notification-service.cpp @@ -32,13 +32,20 @@ ** lifespan of this service instance must exceed the running of the event loop, ** since otherwise the event loop might invoke a lambda bound to the `this` ** pointer of a NotificationService already decommissioned. The setup of the - ** standard Lumiera UI top-level context ensures this is the case, since the + ** standard Lumiera UI top-level context ensures these requirements, since the ** UiManager::performMainLoop() maintains the NotificationService instance ** and also performs the blocking `gtk_main()` call. Consequently, any ** invocation added from other threads after leaving the GTK main loop ** but before closing the GuiNotification facade will just be enqueued, ** but then dropped on destruction of the UiDispatcher PImpl. ** + ** Beyond that dispatching functionality, the NotificationService + ** just serves as entry point to send messages through the [UI-Bus] + ** (\ref ui-bus.hpp) towards [UI elements](\ref tangible.hpp) + ** identified by EntryID. Even notifications and error messages + ** are handled this way, redirecting them toward a dedicated + ** [Log display](\ref error-log-display.hpp) + ** ** @see ui-dispatcher.hpp ** */ @@ -48,9 +55,13 @@ #include "gui/ctrl/ui-manager.hpp" #include "gui/ctrl/ui-dispatcher.hpp" #include "gui/notification-service.hpp" +#include "gui/interact/wizard.hpp" +#include "include/ui-protocol.hpp" + #include "lib/diff/mutation-message.hpp" #include "lib/diff/gen-node.hpp" #include "include/logging.h" +#include "lib/format-string.hpp" #include "lib/depend.hpp" #include "lib/util.hpp" @@ -58,6 +69,7 @@ extern "C" { #include "common/interface-descriptor.h" } +#include #include @@ -66,8 +78,11 @@ using lib::diff::TreeMutator; using lib::diff::MutationMessage; using gui::ctrl::UiDispatcher; using gui::ctrl::BusTerm; -using std::string; using util::cStr; +using util::_Fmt; + +using std::string; +using std::move; namespace gui { @@ -82,40 +97,57 @@ namespace gui { { dispatch_->event ([=]() { - this->mark (uiElement, uiMessage); + ctrl::BusTerm::mark (uiElement, uiMessage); }); } - void + void NotificationService::displayInfo (NotifyLevel severity, string const& text) - { - INFO (gui, "@GUI: display '%s' as notification message.", cStr(text)); - ////////////////////////TODO actually push the information to the GUI ///////////////////////////////////TICKET #1102 : build a message display box in the UI - ////////////////////////////////////////////////TICKET #1047 : as a temporary solution, use the InfoBox panel... - } + { ///////////////////////////////////TICKET #1102 : build a dedicated message display box in the UI + ID errorLogID = interact::Wizard::getErrorLogID(); ////////////////////////////////////TICKET #1047 : as a temporary solution, use the InfoBox panel... + switch (severity) { + case NOTE_ERROR: + markError (errorLogID, text); + break; + case NOTE_INFO: + markNote (errorLogID, text); + break; + case NOTE_WARN: + mark (errorLogID, GenNode{string{MARK_Warning}, text}); + break; + default: + throw lumiera::error::Logic (_Fmt{"UI Notification with invalid severity %d encountered. " + "Given message text was '%s'"} % severity % text); + } } void NotificationService::markError (ID uiElement, string const& text) { - dispatchMsg (uiElement, GenNode{"Error", text}); + dispatchMsg (uiElement, GenNode{string{MARK_Error}, text}); } void - NotificationService::markNote (ID uiElement, string const& text) + NotificationService::markNote (ID uiElement, string const& text) { - dispatchMsg (uiElement, GenNode{"Message", text}); + dispatchMsg (uiElement, GenNode{string{MARK_Message}, text}); + } + + + void + NotificationService::mark (ID uiElement, GenNode&& stateMarkMsg) + { + dispatchMsg (uiElement, move (stateMarkMsg)); } void NotificationService::mutate (ID uiElement, MutationMessage&& diff) { - dispatch_->event ([=]() //////////////////////////////////TODO care for error handling!!! - { - // apply and consume diff message stored within closure + dispatch_->event ([=]() + { // apply and consume diff message stored within closure this->change (uiElement, move(unConst(diff))); }); } @@ -229,27 +261,35 @@ namespace gui { } ) , LUMIERA_INTERFACE_INLINE (markError, - void, (LumieraUid element, const char* text), + void, (const void* element, const char* text), { if (!_instance) lumiera_error_set (LUMIERA_ERROR_LIFECYCLE, text); else - _instance().markError (reinterpret_cast (*element), text); + _instance().markError (*static_cast (element), text); } ) , LUMIERA_INTERFACE_INLINE (markNote, - void, (LumieraUid element, const char* text), + void, (const void* element, const char* text), { if (!_instance) lumiera_error_set (LUMIERA_ERROR_LIFECYCLE, text); else - _instance().markNote (reinterpret_cast (*element), text); + _instance().markNote (*static_cast (element), text); + } + ) + , LUMIERA_INTERFACE_INLINE (mark, + void, (const void* element, void* stateMark), + { + if (!_instance) lumiera_error_set (LUMIERA_ERROR_LIFECYCLE, "passing state mark"); + else + _instance().mark (*static_cast (element), move(*reinterpret_cast (stateMark))); } ) , LUMIERA_INTERFACE_INLINE (mutate, - void, (LumieraUid element, void* diff), + void, (const void* element, void* diff), { if (!_instance) lumiera_error_set (LUMIERA_ERROR_LIFECYCLE, "passing diff message"); else - _instance().mutate (reinterpret_cast (*element), move(*reinterpret_cast (diff))); + _instance().mutate (*static_cast (element), move(*reinterpret_cast (diff))); } ) , LUMIERA_INTERFACE_INLINE (triggerGuiShutdown, @@ -262,15 +302,13 @@ namespace gui { ) ); - - - } // (END) facade implementation details + } //(END) facade implementation details /** - * When started, NotificationService connects to the [UI-Bus](ui-bus.hpp) via the provided connection. + * When started, NotificationService connects to the [UI-Bus](\ref ui-bus.hpp) via the provided connection. * This is a simple, unidirectional up-link connection, without actively adding NotificationService * into the routing tables in [Nexus]. Yet this simple connection is sufficient to implement this * service by talking to other facilities within the UI layer. diff --git a/src/gui/notification-service.hpp b/src/gui/notification-service.hpp index 93dd82666..b3390fa5c 100644 --- a/src/gui/notification-service.hpp +++ b/src/gui/notification-service.hpp @@ -27,11 +27,12 @@ ** events within the lower layers, or as result of invoking commands on the session. ** ** This service is the implementation of a layer separation facade interface. Clients should use - ** gui::GuiNotification#facade to access this service. This header defines the interface used - ** to \em provide this service, not to access it. - ** - ** @see gui::GuiFacade - ** @see core-sevice.hpp starting this service + ** gui::GuiNotification#facade to access this service. This header here defines the interface + ** used to _provide_ this service, not to access it. + ** + ** @see gui::GuiFacade launching the Lumiera UI + ** @see facade.hpp RAII holder to start this service and open the interface + ** @see gui::ctrl::UiManager::performMainLoop() exposes all UI façade interfaces */ @@ -68,7 +69,7 @@ namespace gui { * route calls through this interface. * * @note the ctor of this class establishes an "up-link" - * connection to the [UI-Bus](ui-bus.hpp), which + * connection to the [UI-Bus](\ref ui-bus.hpp), which * enables the service implementation to talk to * other facilities within the UI. */ @@ -100,10 +101,11 @@ namespace gui { void displayInfo (NotifyLevel,string const& text) override; void markError (ID uiElement, string const& text) override; void markNote (ID uiElement, string const& text) override; + void mark (ID uiElement, GenNode&&) override; void mutate (ID uiElement, MutationMessage&&) override; void triggerGuiShutdown (string const& cause) override; }; - + } // namespace gui diff --git a/src/gui/panel/infobox-panel.cpp b/src/gui/panel/infobox-panel.cpp index 0f4330e50..24f1bcd5c 100644 --- a/src/gui/panel/infobox-panel.cpp +++ b/src/gui/panel/infobox-panel.cpp @@ -27,43 +27,63 @@ #include "gui/gtk-base.hpp" #include "gui/panel/infobox-panel.hpp" -#include "lib/format-string.hpp" +#include "gui/widget/error-log-display.hpp" -using util::_Fmt; -using Glib::ustring; namespace gui { namespace panel{ + using widget::ErrorLogDisplay; + + InfoBoxPanel::InfoBoxPanel (workspace::PanelManager& panelManager, Gdl::DockItem& dockItem) : Panel(panelManager, dockItem, getTitle(), getStockID()) , twoParts_{Gtk::ORIENTATION_VERTICAL} , buttons_{} - , frame_{"UI Integration Experiments"} - , errorLog_{} + , frame_{_("System Information")} + , logExpander_{_("Error Log")} + , theLog_{} { twoParts_.pack_start(frame_); twoParts_.pack_start(buttons_, Gtk::PACK_SHRINK); buttons_.set_layout (Gtk::BUTTONBOX_START); - // buttons to trigger experiments - button_1_.set_label ("_bang"); - button_1_.set_use_underline(); - button_1_.set_tooltip_markup ("Experiment 1:\ntrigger Proc-GUI roundtrip"); - button_1_.signal_clicked().connect( - mem_fun(*this, &InfoBoxPanel::experiment_1)); - buttons_.add (button_1_); + // buttons to control the error log + buttonClear_.set_label (_("_clear Log")); + buttonClear_.set_use_underline(); + buttonClear_.set_tooltip_markup (_("Discard all contents of the error log.")); + buttonClear_.signal_clicked().connect( + [this](){ if (theLog_) theLog_->clearAll(); }); + buttonClearErr_.set_label (_("_Error OK")); + buttonClearErr_.set_use_underline(); + buttonClearErr_.set_tooltip_markup (_("Clear the error state and turn errors in to information entries.")); + buttonClearErr_.signal_clicked().connect( + [this](){ if (theLog_) theLog_->turnError_into_InfoMsg(); }); + buttonClearInfo_.set_label (_("drop _Info")); + buttonClearInfo_.set_use_underline(); + buttonClearInfo_.set_tooltip_markup (_("Discard all mere info message, retain error entries only.")); + buttonClearInfo_.signal_clicked().connect( + [this](){ if (theLog_) theLog_->clearInfoMsg(); }); + + buttons_.add (buttonClear_); + buttons_.add (buttonClearErr_); + buttons_.add (buttonClearInfo_); //(End)buttons... - frame_.set_border_width (5); - frame_.add (errorLog_); - - // show everything.... + // show initial configuration.... this->add (twoParts_); this->show_all(); + + // schedule state update to hide the error related buttons + // after the UI is actually mapped to screen. + Glib::signal_idle() + .connect_once ( sigc::bind( + sigc::mem_fun (*this, &InfoBoxPanel::reflect_LogErrorState), false) + ); } + const char* InfoBoxPanel::getTitle() { @@ -77,16 +97,44 @@ namespace panel{ } + /** on demand allocate display of information / error log + * @note we assume it stays alive until the panel itself is closed. + * @remark the ErrorLogDisplay widget exposes a \ref Expander functor, + * which is wired here with the Gtk::Expander container holding the Log. + * This setup allows to trigger the expand/collapse functionality and + * query the expansion state directly on the widget. Especially + * \ref NotificationHub relies on this configuration. + */ + ErrorLogDisplay& + InfoBoxPanel::getLog() + { + if (not theLog_) + { + theLog_.reset (new ErrorLogDisplay{}); + logExpander_.set_expanded (false); + logExpander_.add (*theLog_); + theLog_->expand = model::Expander{[&]() -> bool { return logExpander_.get_expanded(); } + ,[&](bool yes) { logExpander_.set_expanded (yes); } + }; + theLog_->reveal = model::Revealer{[&]() { Panel::show(true); + theLog_->expand(true); + } }; + frame_.set_border_width (5); + frame_.add (logExpander_); + frame_.show_all(); + + theLog_->signalErrorChanged().connect( + mem_fun(*this, &InfoBoxPanel::reflect_LogErrorState)); + } + return *theLog_; + } + void - InfoBoxPanel::experiment_1() + InfoBoxPanel::reflect_LogErrorState (bool isError) { - frame_.set_label("Experiment 1... BANG"); - - static uint bangNo{0}; - static _Fmt msgTemplate{"Bang #%d\n"}; - - errorLog_.showMsg(NOTE_WARN, msgTemplate % ++bangNo); + buttonClearErr_.set_visible (isError); + buttonClearInfo_.set_visible (isError); } diff --git a/src/gui/panel/infobox-panel.hpp b/src/gui/panel/infobox-panel.hpp index 3b508bcba..4ef8c1b0f 100644 --- a/src/gui/panel/infobox-panel.hpp +++ b/src/gui/panel/infobox-panel.hpp @@ -26,7 +26,7 @@ ** Such an »Info Box« typically exposes detail settings from some other component ** currently selected, and allows to access those in a non modal fashion. ** - ** @todo as of 8/2017 this is (ab)used as space for UI / Proc-Layer integration experiments + ** @todo as of 10/2017 this is used as space to host the information and error log */ @@ -36,9 +36,13 @@ #include "gui/panel/panel.hpp" -#include "gui/widget/error-log-widget.hpp" + +#include namespace gui { +namespace widget { + class ErrorLogDisplay; +} namespace panel{ class InfoBoxPanel @@ -49,7 +53,8 @@ namespace panel{ * @param PanelManager The owner panel manager widget. * @param DockItem The GdlDockItem that will host this panel. * - * @todo as of 8/2017 this is placeholder code for UI experiments... + * @todo just used as place for the error log as of 10/2017. + * More to come... */ InfoBoxPanel (workspace::PanelManager&, Gdl::DockItem&); @@ -57,14 +62,18 @@ namespace panel{ static const char* getTitle(); ///< @deprecated need better design of the PanelManager /////TICKET #1026 static const gchar* getStockID(); + widget::ErrorLogDisplay& getLog(); + private: Gtk::Box twoParts_; Gtk::ButtonBox buttons_; - Gtk::Button button_1_; + Gtk::Button buttonClear_, buttonClearInfo_, buttonClearErr_; Gtk::Frame frame_; - widget::ErrorLogWidget errorLog_; + Gtk::Expander logExpander_; - void experiment_1(); + std::unique_ptr theLog_; + + void reflect_LogErrorState (bool); }; diff --git a/src/gui/panel/panel.cpp b/src/gui/panel/panel.cpp index a663db660..1b740d2af 100644 --- a/src/gui/panel/panel.cpp +++ b/src/gui/panel/panel.cpp @@ -42,7 +42,8 @@ namespace panel{ ,Gdl::DockItem& dockItem ,const gchar* longName ,const gchar* stockID) - : panelManager_(panelManager) + : Gtk::Box{Gtk::ORIENTATION_VERTICAL} + , panelManager_(panelManager) , dockItem_(dockItem) , panelBar_(*this, stockID) { diff --git a/src/gui/panel/panel.hpp b/src/gui/panel/panel.hpp index d79c24305..8b34e9bdc 100644 --- a/src/gui/panel/panel.hpp +++ b/src/gui/panel/panel.hpp @@ -36,18 +36,18 @@ #include namespace gui { - namespace workspace { - class PanelManager; - class WorkspaceWindow; - } - namespace panel { +namespace workspace { + class PanelManager; + class WorkspaceWindow; +} +namespace panel { /** * The base class for all dockable panels. */ class Panel - : public Gtk::VBox + : public Gtk::Box { protected: diff --git a/src/gui/panel/timeline-panel.cpp b/src/gui/panel/timeline-panel.cpp index 698e8b344..7bd68d0b7 100644 --- a/src/gui/panel/timeline-panel.cpp +++ b/src/gui/panel/timeline-panel.cpp @@ -25,7 +25,7 @@ ** Implementation of the dockable panel to hold the main timeline display. ** ** @todo as of 10/2016 this is WIP-WIP-WIP : canvas widgets experiment - ** @todo build a new timeline widget, connected to the UI-Bus + ** @todo as of 10/2018 we start to build a new timeline widget, connected to the UI-Bus ** @see timeline-widget.hpp ** */ @@ -33,107 +33,30 @@ #include "gui/gtk-base.hpp" #include "gui/panel/timeline-panel.hpp" +#include "gui/timeline/timeline-widget.hpp" +#include "gui/timeline/timeline-widget-empty.hpp" -//#include "gui/workspace/workspace-window.hpp" -#include "gui/ui-bus.hpp" -#include "lib/format-string.hpp" -#include "lib/format-cout.hpp" - -//#include "lib/util.hpp" -#include -#include -#include - - - -using util::_Fmt; -//using std::shared_ptr; -//using std::weak_ptr; -//using util::contains; -using Gtk::Widget; -using sigc::mem_fun; -using sigc::ptr_fun; -using std::string; -using std::cout; -using std::endl; -using std::rand; -using std::max; +using std::make_unique; namespace gui { namespace panel { - + + using timeline::TimelinePage; + using timeline::TimelineWidget; + using timeline::TimelineWidgetEmpty; TimelinePanel::TimelinePanel (workspace::PanelManager& panelManager, Gdl::DockItem& dockItem) : Panel(panelManager, dockItem, getTitle(), getStockID()) - , twoParts_(Gtk::ORIENTATION_VERTICAL) - , buttons_() - , frame_("Gtk::Layout Experiments") - , scroller_() - , canvas_() + , tabs_{} + , pages_{} { - twoParts_.pack_start(buttons_, Gtk::PACK_SHRINK); - twoParts_.pack_start(frame_); - - buttons_.set_layout(Gtk::BUTTONBOX_START); - - // buttons to trigger experiments - button_1_.set_label("_place"); - button_1_.set_use_underline(); - button_1_.set_tooltip_markup("Experiment 1:\nplace new child widget\nat random position on the canvas"); - button_1_.signal_clicked().connect( - mem_fun(*this, &TimelinePanel::experiment_1)); - buttons_.add(button_1_); - - button_2_.set_label("_move"); - button_2_.set_use_underline(); - button_2_.set_tooltip_markup("Experiment 2:\nmove all child widgets randomly"); - button_2_.signal_clicked().connect( - mem_fun(*this, &TimelinePanel::experiment_2)); - buttons_.add(button_2_); - - button_3_.set_label("a_lign"); - button_3_.set_use_underline(); - button_3_.set_tooltip_markup("Experiment 3:\nalign all child widgets in a row\nwith silight random vertical offset"); - button_3_.signal_clicked().connect( - mem_fun(*this, &TimelinePanel::experiment_3)); - buttons_.add(button_3_); - - button_4_.set_label("_grow"); - button_4_.set_use_underline(); - button_4_.set_tooltip_markup("Experiment 4:\nextend arbitrary child widget's text"); - button_4_.signal_clicked().connect( - mem_fun(*this, &TimelinePanel::experiment_4)); - buttons_.add(button_4_); - - button_5_.set_label("_kill"); - button_5_.set_use_underline(); - button_5_.set_tooltip_markup("Experiment 5:\nkill arbitrary child widget"); - button_5_.signal_clicked().connect( - mem_fun(*this, &TimelinePanel::experiment_5)); - buttons_.add(button_5_); - - toggleDraw_.set_label("draw"); - toggleDraw_.signal_clicked().connect( - [this]() { - canvas_.enableDraw (this->toggleDraw_.get_active()); - }); - buttons_.add(toggleDraw_); - //(End)buttons... - - frame_.add(scroller_); - frame_.set_border_width(5); - - scroller_.set_shadow_type(Gtk::SHADOW_NONE); - scroller_.set_border_width(10); - scroller_.add(canvas_); - - canvas_.adjustSize(); + addTimeline (PageHandle{new TimelineWidgetEmpty{}}); // show everything.... - this->add(twoParts_); + this->add(tabs_); this->show_all(); } @@ -150,231 +73,27 @@ namespace panel { } - - void - TimelinePanel::experiment_1() - { - frame_.set_label("Experiment 1... PLACE"); - - ChildEx* chld = makeChld(); - childz_.push_back(chld); - uint x = rand() % 1000; - uint y = rand() % 500; - canvas_.put(*chld, x, y); - chld->show(); - canvas_.adjustSize(); - } - - - void - TimelinePanel::experiment_2() - { - frame_.set_label("Experiment 2... MOVE"); - for (Widget* chld : childz_) - { - uint x = canvas_.child_property_x(*chld); - uint y = canvas_.child_property_y(*chld); - int deltaX = -20 + rand() % 41; - int deltaY = -15 + rand() % 31; - x = uint(max (0, int(x) + deltaX)); - y = uint(max (0, int(y) + deltaY)); - - canvas_.move (*chld, x,y); - } - canvas_.adjustSize(); - } - - - void - TimelinePanel::experiment_3() - { - frame_.set_label("Experiment 3... ALIGN"); - uint pos=0; - for (Widget* chld : childz_) - { - uint y = rand() % 30; - canvas_.move (*chld, pos, y); - - int width = chld->get_allocated_width(); - pos += 0.6 * width; - } - canvas_.adjustSize(); - } - - - void - TimelinePanel::experiment_4() - { - frame_.set_label("Experiment 4... GROW"); - uint selector = rand() % childz_.size(); - ChildEx& toGrow = *childz_[selector]; - toGrow.set_label ("***"+toGrow.get_label()+"***"); - } - - - void - TimelinePanel::experiment_5() - { - frame_.set_label("Experiment 5... KILL"); - uint killPos = rand() % childz_.size(); - ChildV::iterator killThat(&childz_[killPos]); - ChildEx* victim = *killThat; - childz_.erase (killThat); - canvas_.remove (*victim); - delete victim; - } - - - void - Canvas::enableDraw (bool yes) - { - shallDraw_ = yes; - - // force redrawing of the visible area... - auto win = get_window(); - if (win) - { - int w = get_allocation().get_width(); - int h = get_allocation().get_height(); - Gdk::Rectangle rect{0, 0, w, h}; - win->invalidate_rect(rect, false); - } - } - - - - void - Canvas::adjustSize() - { - recalcExtension_ = true; - } - - void - Canvas::determineExtension() - { - if (not recalcExtension_) return; - - uint extH=20, extV=20; - Gtk::Container::ForeachSlot callback - = [&](Gtk::Widget& chld) - { - auto alloc = chld.get_allocation(); - uint x = alloc.get_x(); - uint y = alloc.get_y(); - x += alloc.get_width(); - y += alloc.get_height(); - extH = max (extH, x); - extV = max (extV, y); - }; - foreach(callback); - recalcExtension_ = false; - set_size (extH, extV); - } - - - bool - Canvas::on_draw(Cairo::RefPtr const& cox) - { - if (shallDraw_) - { - uint extH, extV; - determineExtension(); - get_size (extH, extV); - - auto adjH = get_hadjustment(); - auto adjV = get_vadjustment(); - double offH = adjH->get_value(); - double offV = adjV->get_value(); - - cox->save(); - cox->translate(-offH, -offV); - - // draw red diagonal line - cox->set_source_rgb(0.8, 0.0, 0.0); - cox->set_line_width (10.0); - cox->move_to(0, 0); - cox->line_to(extH, extV); - cox->stroke(); - cox->restore(); - - // cause child widgets to be redrawn - bool event_is_handled = Gtk::Layout::on_draw(cox); - - // any drawing which follows happens on top of child widgets... - cox->save(); - cox->translate(-offH, -offV); - - cox->set_source_rgb(0.2, 0.4, 0.9); - cox->set_line_width (2.0); - cox->rectangle(0,0, extH, extV); - cox->stroke(); - cox->restore(); - - return event_is_handled; - } - else - return Gtk::Layout::on_draw(cox); - } - - - /* === Support for Investigation === */ - namespace { - _Fmt childID("Chld-%02d"); - - int instanceCnt = 0; - } - - - uint ChildEx::childNo = 0; - - - ChildEx::ChildEx() - : Gtk::Button(string (childID % childNo++)) + bool + isEmptyTimeline (auto& pages) { - ++instanceCnt; + return 1 == pages.size() + and dynamic_cast (pages[0].get()); } - + } void - ChildEx::on_clicked() + TimelinePanel::addTimeline (PageHandle&& pTimelineWidget) { - cout << "|=="< 0) - cout << " ↯↯ still "< +#include +#include #include namespace gui { -namespace model{ - class Sequence; - } -namespace panel { - -//using std::shared_ptr; - - /** - * "experimental" child widget for investigation of Gtk::Layout - */ - class ChildEx - : public Gtk::Button - { - static uint childNo; - - public: - ChildEx(); - ~ChildEx(); - - private: - void on_clicked() override; - }; - - void __verifyDeadChildren(); - - - - /** - * "experimental" custom canvas, based on Gtk::Layout. - * In addition this customised widget supports direct drawing - */ - class Canvas - : public Gtk::Layout - { - bool shallDraw_; - bool recalcExtension_ = false; - - public: - void enableDraw (bool); - void adjustSize(); - - private: - virtual bool on_draw (Cairo::RefPtr const&) override; - - void determineExtension(); - }; - +namespace panel{ /** * Dockable panel to hold timeline widget(s). - * @todo build the actual implementation, after finishing the investigation - * - * ## Investigation of gtk::GtkLayout - * As of 10/2016, we start this task with an exploration of GTK behaviour - * - * \par Plan of investigation - * 1. place some simple widgets (Buttons) ✔ - * 2. learn how to draw ✔ - * 3. place a huge number of widgets, to scrutinise scrolling and performance - * 4. place widgets overlapping and irregularily, beyond the scrollable area ✔ - * 5. bind signals to those widgets, to verify event dispatching ✔ - * 6. bind some further signal(s) to the ~GtkLayout container - * 7. hide and re-show a partially and a totally overlapped widget - * 8. find a way to move a widget ✔ and delete arbitrary widgets ✔ - * 9. expand an existing widget (text change) ✔ - * 10. build a custom "clip" widget - * 11. retrofit all preceding tests to use this "clip" widget + * @todo WIP 10/2018 the actual re-implementation of Lumiera's Timeline display */ class TimelinePanel : public Panel { + using PageHandle = std::unique_ptr; + using Timelines = std::vector; + + Gtk::Notebook tabs_; + Timelines pages_; + public: /** * @param panel_manager The owner panel manager widget. * @param dock_item The GdlDockItem that will host this panel. */ - TimelinePanel(workspace::PanelManager&, Gdl::DockItem&); + TimelinePanel (workspace::PanelManager&, Gdl::DockItem&); static const char* getTitle(); static const gchar* getStockID(); + /** take ownership of the widget and place it into a new tab */ + void addTimeline (PageHandle &&); private: - Gtk::Box twoParts_; - Gtk::ButtonBox buttons_; - Gtk::Button button_1_; - Gtk::Button button_2_; - Gtk::Button button_3_; - Gtk::Button button_4_; - Gtk::Button button_5_; - Gtk::CheckButton toggleDraw_; - Gtk::Frame frame_; - Gtk::ScrolledWindow scroller_; - Canvas canvas_; - - ChildEx* makeChld(); - - using ChildV = std::vector; - ChildV childz_; - - void experiment_1(); - void experiment_2(); - void experiment_3(); - void experiment_4(); - void experiment_5(); }; diff --git a/src/gui/setting/asset-controller.cpp b/src/gui/setting/asset-controller.cpp index 352bd751d..bda517cdd 100644 --- a/src/gui/setting/asset-controller.cpp +++ b/src/gui/setting/asset-controller.cpp @@ -63,9 +63,6 @@ namespace setting { void AssetController::buildMutator (TreeMutator::Handle buffer) { -// using Attrib = std::pair; -// using lib::diff::collection; - buffer.create ( TreeMutator::build() ); diff --git a/src/gui/setting/asset-controller.hpp b/src/gui/setting/asset-controller.hpp index fce280526..d4e6ad67e 100644 --- a/src/gui/setting/asset-controller.hpp +++ b/src/gui/setting/asset-controller.hpp @@ -77,14 +77,14 @@ namespace setting { : public model::Controller { ///////TODO create UI representation for the asset subsections - - /** content population and manipulation via UI-Bus */ - void buildMutator (lib::diff::TreeMutator::Handle) override; public: AssetController (ID identity, ctrl::BusTerm& nexus); ~AssetController(); + /** content population and manipulation via UI-Bus */ + void buildMutator (lib::diff::TreeMutator::Handle) override; + private: }; diff --git a/src/gui/timeline/header-pane-widget.cpp b/src/gui/style-scheme.cpp similarity index 51% rename from src/gui/timeline/header-pane-widget.cpp rename to src/gui/style-scheme.cpp index 6590e3e4e..1dd8cf8c6 100644 --- a/src/gui/timeline/header-pane-widget.cpp +++ b/src/gui/style-scheme.cpp @@ -1,8 +1,8 @@ /* - HeaderPaneWidget - display pane for track headers within the timeline + StyleScheme - magic keys to access uniform styling scheme Copyright (C) Lumiera.org - 2016, Hermann Vosseler + 2018, 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 @@ -21,54 +21,27 @@ * *****************************************************/ -/** @file header-pane-widget.cpp - ** Implementation of the track header pane within the timeline UI. +/** @file style-scheme.hpp + ** Definition of access keys for uniform UI styling. ** - ** @todo WIP-WIP-WIP as of 12/2016 + ** @see gui::workspace::UiStyle ** */ - #include "gui/gtk-base.hpp" -#include "gui/timeline/header-pane-widget.hpp" - -//#include "gui/ui-bus.hpp" -//#include "lib/format-string.hpp" -//#include "lib/format-cout.hpp" - -//#include "lib/util.hpp" - -//#include -//#include - - - -//using util::_Fmt; -//using util::contains; -//using Gtk::Widget; -//using sigc::mem_fun; -//using sigc::ptr_fun; -//using std::cout; -//using std::endl; - +#include "gui/style-scheme.hpp" namespace gui { -namespace timeline { + + const Literal TAG_ERROR{"ERROR"}; ////////////////////////////////////////////////////////////TICKET #1168 : find a way to manage style of custom extended UI elements + const Literal TAG_WARN{"WARN"}; + + /** CSS class added temporarily to make a widget "flash" + * in response to the corresponding UI-Bus message + * @see flash-deco.hpp */ + cuString CSS_CLASS_FLASH{"indication_flash"}; + const uint STYLE_FLASH_DURATION_in_ms = 1100; - - - HeaderPaneWidget::HeaderPaneWidget () - { - } - - - HeaderPaneWidget::~HeaderPaneWidget() - { - } - - - - -}}// namespace gui::timeline +}// namespace gui diff --git a/src/gui/style-scheme.hpp b/src/gui/style-scheme.hpp new file mode 100644 index 000000000..383ae8987 --- /dev/null +++ b/src/gui/style-scheme.hpp @@ -0,0 +1,58 @@ +/* + STYLE_SCHEME.hpp - magic keys to access uniform styling scheme + + Copyright (C) Lumiera.org + 2018, 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 style-scheme.hpp + ** Hard wired key constants to access a global uniform styling scheme. + ** + ** @todo as or 10/2018 this is mere placeholder code. The StyleManager needs rework + ** + ** @see ui-style.hpp + ** + */ + +#ifndef GUI_STYLE_SCHEME_H +#define GUI_STYLE_SCHEME_H + + +#include "lib/symbol.hpp" + + +namespace Glib { + class ustring; +} +namespace gui { + + using lib::Literal; + using cuString = const Glib::ustring; + + + extern const Literal TAG_ERROR; ////////////////////////////////////////////////////////TICKET #1168 : find a way to manage style of custom extended UI elements + extern const Literal TAG_WARN; + + extern cuString CSS_CLASS_FLASH; + extern const uint STYLE_FLASH_DURATION_in_ms; + + + +}// namespace gui +#endif /*GUI_STYLE_SCHEME_H*/ diff --git a/src/gui/timeline/body-canvas-widget.cpp b/src/gui/timeline/body-canvas-widget.cpp index 52d4e8383..284688036 100644 --- a/src/gui/timeline/body-canvas-widget.cpp +++ b/src/gui/timeline/body-canvas-widget.cpp @@ -31,6 +31,7 @@ #include "gui/gtk-base.hpp" #include "gui/timeline/body-canvas-widget.hpp" +#include "gui/timeline/track-body.hpp" //#include "gui/ui-bus.hpp" //#include "lib/format-string.hpp" @@ -57,17 +58,47 @@ namespace timeline { + TimelineCanvas::TimelineCanvas() + : Gtk::Layout{} + , rootBody_{nullptr} + { } + + + + BodyCanvasWidget::~BodyCanvasWidget() { } BodyCanvasWidget::BodyCanvasWidget () + : Gtk::ScrolledWindow{} + , canvas_{} { + this->set_shadow_type(Gtk::SHADOW_IN); + this->set_policy (Gtk::POLICY_ALWAYS, Gtk::POLICY_AUTOMATIC); // always need a horizontal scrollbar + this->property_expand() = true; // dynamically grab any available additional space + this->add(canvas_); + +// canvas_.adjustSize(); + + // show everything.... + this->show_all(); } - BodyCanvasWidget::~BodyCanvasWidget() - { - } + /** + * The Lumiera Timeline model does not rely on a list of tracks, as most conventional video editing software does -- + * rather, each sequence holds a _fork of nested scopes._ This recursively nested structure is reflected in the way + * we organise and draw the timeline representation onto the TimelineCanvas: we use an intermediary entity, the TrackBody + * as an organisational grouping device, even while we draw _all of the timeline representation_ onto a single global + * ::canvas_ within the (scrollable) BodyCanvasWidget. Thus, adding the first TrackBody to represent the root track + * of a Timeline, will also prepare the grounding for any other nested entities to be drawn on top. + */ + void + BodyCanvasWidget::installForkRoot (TrackBody& rootTrackBody) + { + canvas_.rootBody_ = &rootTrackBody; + } + diff --git a/src/gui/timeline/body-canvas-widget.hpp b/src/gui/timeline/body-canvas-widget.hpp index faf4ca6d4..47b1743e6 100644 --- a/src/gui/timeline/body-canvas-widget.hpp +++ b/src/gui/timeline/body-canvas-widget.hpp @@ -69,18 +69,36 @@ namespace gui { namespace timeline { + class TrackBody; + + + class TimelineCanvas + : public Gtk::Layout + { + public: + TrackBody* rootBody_; + + TimelineCanvas(); + }; + /** * @todo WIP-WIP as of 12/2016 */ class BodyCanvasWidget + : public Gtk::ScrolledWindow { + TimelineCanvas canvas_; + public: BodyCanvasWidget(); ~BodyCanvasWidget(); - + + /** @internal Initially install the contents corresponding to the root track fork */ + void installForkRoot (TrackBody& rootTrackBody); + private:/* ===== Internals ===== */ - + }; diff --git a/src/gui/timeline/clip-presenter.cpp b/src/gui/timeline/clip-presenter.cpp index 8a749412e..19e30f7c7 100644 --- a/src/gui/timeline/clip-presenter.cpp +++ b/src/gui/timeline/clip-presenter.cpp @@ -25,11 +25,13 @@ ** Implementation details of clip presentation management. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ #include "gui/gtk-base.hpp" +#include "include/ui-protocol.hpp" #include "gui/timeline/clip-presenter.hpp" #include "gui/timeline/marker-widget.hpp" @@ -93,57 +95,57 @@ namespace timeline { .attach (collection(markers_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Marker" - return "Marker" == spec.data.recordType(); + return TYPE_Marker == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PMarker const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PMarker { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi, this->uiBus_); }) .buildChildMutator ([&](PMarker& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; })) .attach (collection(effects_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Effect" - return "Effect" == spec.data.recordType(); + return TYPE_Effect == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PEffect const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PEffect { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi, this->uiBus_); }) .buildChildMutator ([&](PEffect& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; })) .attach (collection(channels_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Channel" - return "Channel" == spec.data.recordType(); + return TYPE_Channel == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PChannel const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PChannel { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi, this->uiBus_); }) .buildChildMutator ([&](PChannel& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; }))); diff --git a/src/gui/timeline/clip-presenter.hpp b/src/gui/timeline/clip-presenter.hpp index 86876bce1..5ecd0df1d 100644 --- a/src/gui/timeline/clip-presenter.hpp +++ b/src/gui/timeline/clip-presenter.hpp @@ -44,6 +44,7 @@ ** clips, just to indicate some content is actually present in this part of the timeline. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ diff --git a/src/gui/timeline/header-pane-widget.hpp b/src/gui/timeline/header-pane-widget.hpp index fdc44d0a0..e18cbc2fa 100644 --- a/src/gui/timeline/header-pane-widget.hpp +++ b/src/gui/timeline/header-pane-widget.hpp @@ -33,7 +33,13 @@ ** The HeaderPaneWidget aggregates those patchbay elements into a nested, collapsable tree ** structure in accordance with the nesting of scopes. For the actual layout, it has to collaborate ** with the timeline::LayoutManager to work out the space available for each individual track head - ** and to keep these parts aligned with the tracks in the [timeline body](BodyCanvasWidget). + ** and to keep these parts aligned with the tracks in the [timeline body](\ref BodyCanvasWidget). + ** + ** The header area is shown at the left side of the timeline display, always visible. + ** At the top of the header area, corresponding to the time ruler within the body at the right, + ** we place a compact navigation control, including also the timecode display. The space below + ** is kept in sync with the respective track entries of the timeline, and is itself built as + ** a nested structure of PatchbayWidget entries. ** ** @todo WIP-WIP-WIP as of 12/2016 ** @@ -44,6 +50,8 @@ #define GUI_TIMELINE_HEADER_PANE_WIDGET_H #include "gui/gtk-base.hpp" +#include "gui/timeline/navigator-widget.hpp" +#include "gui/timeline/patchbay-widget.hpp" //#include "lib/util.hpp" @@ -55,16 +63,44 @@ namespace gui { namespace timeline { + using PAdjustment = Glib::RefPtr; + + + class TrackHeadWidget; + /** * @todo WIP-WIP as of 12/2016 */ class HeaderPaneWidget + : public Gtk::Box { + NavigatorWidget navigator_; + PatchbayWidget patchbay_; + public: - HeaderPaneWidget(); - ~HeaderPaneWidget(); - + ~HeaderPaneWidget() { } + + HeaderPaneWidget (PAdjustment const& vScroll) + : Gtk::Box{Gtk::ORIENTATION_VERTICAL} + , navigator_{} + , patchbay_{vScroll} + { + this->pack_start (navigator_, Gtk::PACK_SHRINK); + this->pack_start (patchbay_, Gtk::PACK_EXPAND_WIDGET); + } + + /** + * Initially install the root node of the track fork, + * which later can be extended recursively by adding nested + * sub-forks ("Sub-Tracks"). + */ + void + installForkRoot (TrackHeadWidget& rootTrackHead) + { + patchbay_.installFork (rootTrackHead); + } + private:/* ===== Internals ===== */ }; diff --git a/src/gui/timeline/marker-widget.cpp b/src/gui/timeline/marker-widget.cpp index 52d6760f0..530a30c02 100644 --- a/src/gui/timeline/marker-widget.cpp +++ b/src/gui/timeline/marker-widget.cpp @@ -25,11 +25,13 @@ ** Implementation of marker display. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ #include "gui/gtk-base.hpp" +#include "include/ui-protocol.hpp" #include "gui/timeline/marker-widget.hpp" //#include "gui/ui-bus.hpp" @@ -81,11 +83,11 @@ namespace timeline { { buffer.create ( TreeMutator::build() - .change("name", [&](string val) + .change(ATTR_name, [&](string val) { name_ = val; }) - .change("kind", [&](string val) + .change(META_kind, [&](string val) { if (val == "LOOP") kind_ = LOOP; else kind_ = MARK; diff --git a/src/gui/timeline/marker-widget.hpp b/src/gui/timeline/marker-widget.hpp index e1e5c9e84..45ee015e3 100644 --- a/src/gui/timeline/marker-widget.hpp +++ b/src/gui/timeline/marker-widget.hpp @@ -30,6 +30,7 @@ ** may receive messages over the UI-Bus. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ @@ -39,7 +40,7 @@ #include "gui/gtk-base.hpp" #include "gui/model/widget.hpp" -#include "gui/widget/label-widget.hpp" +#include "gui/widget/element-box-widget.hpp" //#include "lib/util.hpp" @@ -64,7 +65,7 @@ namespace timeline { */ class MarkerWidget : public model::Widget - , public widget::LabelWidget + , public widget::ElementBoxWidget { MarkerKind kind_; string name_; diff --git a/src/gui/timeline/navigator-widget.cpp b/src/gui/timeline/navigator-widget.cpp index 8559fbe65..eade9c30f 100644 --- a/src/gui/timeline/navigator-widget.cpp +++ b/src/gui/timeline/navigator-widget.cpp @@ -60,14 +60,13 @@ namespace timeline { + + NavigatorWidget::~NavigatorWidget() { } + + NavigatorWidget::NavigatorWidget () - { - } - - - NavigatorWidget::~NavigatorWidget() - { - } + : Gtk::Grid{} + { } diff --git a/src/gui/timeline/navigator-widget.hpp b/src/gui/timeline/navigator-widget.hpp index 4eb021125..791798a30 100644 --- a/src/gui/timeline/navigator-widget.hpp +++ b/src/gui/timeline/navigator-widget.hpp @@ -58,6 +58,7 @@ namespace timeline { * @todo WIP-WIP as of 12/2016 */ class NavigatorWidget + : public Gtk::Grid { public: NavigatorWidget (); diff --git a/src/gui/timeline/patchbay-widget.cpp b/src/gui/timeline/patchbay-widget.cpp index 32496d6b7..e40d049d8 100644 --- a/src/gui/timeline/patchbay-widget.cpp +++ b/src/gui/timeline/patchbay-widget.cpp @@ -32,7 +32,7 @@ #include "gui/gtk-base.hpp" #include "gui/timeline/patchbay-widget.hpp" - +#include "gui/timeline/track-head-widget.hpp" //#include "gui/ui-bus.hpp" //#include "lib/format-string.hpp" //#include "lib/format-cout.hpp" @@ -46,6 +46,7 @@ //using util::_Fmt; //using util::contains; +using Gtk::Adjustment; //using Gtk::Widget; //using sigc::mem_fun; //using sigc::ptr_fun; @@ -60,16 +61,34 @@ namespace timeline { - PatchbayWidget::PatchbayWidget () - { - } + PatchbayWidget::~PatchbayWidget() { } - PatchbayWidget::~PatchbayWidget() + /** + * Set up the patchbay area of the timeline UI. + * The patchbay is a container to hold the actual placement control widgets + * arranged alongside with each track, according to the nested track structure. + * The header pane and thus especially the patchbay needs to follow as a slave + * to the vertical scrolling adjustments of the Timeline display; if the users + * scrolls up or down over the more or less expanded tree of tracks, the header + * has to follow this scrolled display in sync. Thus the implementation of the + * patchbay is based on a Gtk::Viewport, which is attached to the vertical + * Gtk::Adjustment, as exposed by the Gtk::ScrolledWindow holding the + * timeline body canvas on the right side of the Timeline UI. + * @note the Patchbay is not scrollable in horizontal direction, + * thus we create a disabled Adjustment for this parameter. + */ + PatchbayWidget::PatchbayWidget (PAdjustment const& vScroll) + : Gtk::Viewport{Gtk::Adjustment::create (0,0,0,0,0,0), vScroll} + { } + + + void + PatchbayWidget::installFork (TrackHeadWidget& rootTrackHead) { + this->add (rootTrackHead); } - }}// namespace gui::timeline diff --git a/src/gui/timeline/patchbay-widget.hpp b/src/gui/timeline/patchbay-widget.hpp index f172d10be..03728a763 100644 --- a/src/gui/timeline/patchbay-widget.hpp +++ b/src/gui/timeline/patchbay-widget.hpp @@ -22,22 +22,14 @@ /** @file patchbay-widget.hpp - ** Widget to control the placement parameters of a fork within the timeline header pane UI. + ** Widget to hold the track head controls within the timeline header pane UI. ** [The fork](\ref session::Fork), a recursively nested system of scopes, is rendered within - ** the timeline display as a system of nested tracks. Each of these tracks possibly holds some - ** child tracks plus some actual media clips, which all inherit parameters of placement from - ** this fork ("track"). These parameters address various aspects of how content is attached - ** ("placed") into the model at large, examples being - ** - how to route the output - ** - how to "place" this content into an _output space_, like e.g. - ** + sound panning - ** + video overlay parameters (additive, opaque, transparent) - ** + video or audio _level_ (=fader) - ** - how to locate this content in time (e.g. relative to some marker) - ** For each track, we show a patchbay in the timeline header pane, which serves to control - ** such aspects relevant for all content contained within the scope of this track. + ** the timeline display as a system of nested tracks. For each of these tracks we get a header + ** section, allowing to control its placement parameters, including start time, output routing + ** level and panning. The _Patch Bay_ is the container holding all those track header controls, + ** arranged into a recursively nested structure. ** - ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo WIP-WIP-WIP as of 10/2018 ** */ @@ -57,16 +49,28 @@ namespace gui { namespace timeline { + using PAdjustment = Glib::RefPtr; + + class TrackHeadWidget; + /** + * Header pane control area corresponding to a Track with nested child Tracks. + * This structure is used recursively to build up the Fork of nested Tracks. + * - first row: Placement + Property pane + * - second row: content or nested tracks. * @todo WIP-WIP as of 12/2016 */ class PatchbayWidget + : public Gtk::Viewport { public: - PatchbayWidget (); + PatchbayWidget (PAdjustment const& vScroll); ~PatchbayWidget(); - + + /** @internal Initially install the contents corresponding to the track fork root */ + void installFork (TrackHeadWidget& rootTrackHead); + private:/* ===== Internals ===== */ }; diff --git a/src/gui/timeline/timeline-controller.cpp b/src/gui/timeline/timeline-controller.cpp index 80135b2c1..5634a301c 100644 --- a/src/gui/timeline/timeline-controller.cpp +++ b/src/gui/timeline/timeline-controller.cpp @@ -32,13 +32,15 @@ ** - thus we get a rather simple mapping, with some fixed attributes and no ** flexible child collection. The root track is implemented as TrackPresenter. ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** @see TimelineWidget ** */ #include "gui/gtk-base.hpp" +#include "include/ui-protocol.hpp" +#include "gui/timeline/timeline-layout.hpp" #include "gui/timeline/timeline-controller.hpp" #include "gui/timeline/track-presenter.hpp" #include "gui/timeline/marker-widget.hpp" @@ -74,18 +76,21 @@ namespace timeline { //const int TimelineWidget::TrackPadding = 1; //const int TimelineWidget::HeaderWidth = 150; //const int TimelineWidget::HeaderIndentWidth = 10; + class TrackHeadWidget; + class TrackBody; - TimelineController::TimelineController (ID identity, ID trackID, ctrl::BusTerm& nexus) + TimelineController::TimelineController (ID identity, ID trackID, ctrl::BusTerm& nexus, TimelineLayout& layoutManager) : Controller{identity, nexus} + , name_{identity.getSym()} // fallback initialise name from human-readable ID symbol , markers_{} - , fork_{new TrackPresenter{trackID, nexus}} - , name_{identity.getSym()} // fallback initialise name from human-readable ID symbol - { - UNIMPLEMENTED ("how to make the controller operative..."); - } + , fork_{new TrackPresenter{trackID, nexus, [&](TrackHeadWidget& head,TrackBody& body) + { + layoutManager.installRootTrack (head, body); + }}} + { } TimelineController::~TimelineController() @@ -133,6 +138,7 @@ namespace timeline { TimelineController::buildMutator (TreeMutator::Handle buffer) { using PMarker = unique_ptr; + auto rootForkID = ID{*fork_}; buffer.create ( TreeMutator::build() @@ -143,7 +149,7 @@ namespace timeline { }) .matchElement ([&](GenNode const& spec, PMarker const& elm) -> bool { // »Matcher« : how to know we're dealing with the right object - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PMarker { // »Constructor« : what to do when the diff mentions a new entity @@ -151,17 +157,17 @@ namespace timeline { }) .buildChildMutator ([&](PMarker& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { // »Mutator« : how to apply the diff recursively to a nested scope - if (ID(target) != subID) return false; // - require match on already existing child object + if (ID{*target} != subID) return false; // - require match on already existing child object target->buildMutator (buff); // - delegate to child to build nested TreeMutator return true; })) - .mutateAttrib("fork", [&](TreeMutator::Handle buff) - { // »Attribute Mutator« : how enter an object field as nested scope + .mutateAttrib(rootForkID, [&](TreeMutator::Handle buff) + { // »Attribute Mutator« : how to enter the track-fork-object field as nested scope REQUIRE (fork_); fork_->buildMutator(buff); }) - .change("name", [&](string val) - { // »Attribute Setter« : how assign a new value to some object field + .change(ATTR_name, [&](string val) + { // »Attribute Setter« : how to assign a new value to the name field (object member) name_ = val; })); } diff --git a/src/gui/timeline/timeline-controller.hpp b/src/gui/timeline/timeline-controller.hpp index 75cb0f854..eb157626e 100644 --- a/src/gui/timeline/timeline-controller.hpp +++ b/src/gui/timeline/timeline-controller.hpp @@ -45,7 +45,7 @@ ** - these in turn manage a set of ClipPresenter entities ** - and those presenters care for injecting suitable widgets into the TimelineWidget's parts. ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ @@ -72,12 +72,13 @@ namespace timeline { using std::string; class TrackPresenter; + class TimelineLayout; class MarkerWidget; /** * Controller to supervise the timeline display. - * As a [tangible element](model::Tangible), it is attached to the UI-Bus. + * As a [tangible element](\ref model::Tangible), it is attached to the UI-Bus. * @todo WIP-WIP-rewrite as of 12/2016 * @remarks a Timeline always has an attached Sequence, which in turn has * a single mandatory root track. This in turn might hold further child tracks, @@ -86,10 +87,11 @@ namespace timeline { class TimelineController : public model::Controller { + string name_; + vector> markers_; std::unique_ptr fork_; - string name_; public: /** @@ -97,9 +99,9 @@ namespace timeline { * @param trackID the mandatory root track used in the associated Sequence * @param nexus some established connection to the UI-Bus, used for registration. */ - TimelineController (ID identity, ID trackID, ctrl::BusTerm& nexus); + TimelineController (ID identity, ID trackID, ctrl::BusTerm& nexus, TimelineLayout&); - ~TimelineController(); + ~TimelineController(); /** set up a binding to respond to mutation messages via UiBus */ @@ -108,6 +110,12 @@ namespace timeline { public: /* ===== Control interface ===== */ + string const& + getName() const + { + return name_; + } + public: /* ===== Signals ===== */ private:/* ===== Events ===== */ diff --git a/src/gui/timeline/timeline-gui.cpp b/src/gui/timeline/timeline-gui.cpp new file mode 100644 index 000000000..f68a385e7 --- /dev/null +++ b/src/gui/timeline/timeline-gui.cpp @@ -0,0 +1,123 @@ +/* + TimelineGui - top-level anchor point and placeholder for "the timeline" + + Copyright (C) Lumiera.org + 2018, 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 timeline-gui.cpp + ** Implementation details a delegating anchor proxy for a TimelineWidget. + ** To reduce header include impact of global-ctx.hpp, all operations involving + ** actual object construction are defined within this dedicated translation unit. + ** + ** @see TimelineWidget + ** @see gui::interact::InteractionDirector::buildMutator + ** @see gui::timeline::TimelineController::buildMutator + ** + */ + + +#include "gui/gtk-base.hpp" +#include "include/ui-protocol.hpp" +#include "gui/timeline/timeline-gui.hpp" +#include "gui/timeline/timeline-widget.hpp" + +#include + +using lib::diff::TreeMutator; +using std::unique_ptr; + + +namespace gui { +namespace timeline { + + + /** + * Initially, TimelineGui is just an empty placeholder handle. + * Yet knowledge of the represented timeline's ID is required + */ + TimelineGui::TimelineGui (ID identity, ID trackID) + : timelineID_{identity} + , rootTrackID_{trackID} + { } + + /** + * @remark basically the default copy ctor, just redefined here explicitly + * to allow using TimelineGui without the need to include timeline-widget.hpp + */ + TimelineGui::TimelineGui (TimelineGui const& r) + : WLink{r}, util::Cloneable{} + , timelineID_{r.timelineID_} + , rootTrackID_{r.rootTrackID_} + { } + + TimelineGui::TimelineGui (TimelineGui && rr) + : WLink{std::move (rr)} + , timelineID_{rr.timelineID_} + , rootTrackID_{rr.rootTrackID_} + { } + + TimelineGui::~TimelineGui() + { + if (this->isActive()) + TODO ("cascading destruction of the TimelineWidget"); /////////////////////////////////////////////TICKET 1016 : implement sane unwinding + } + + + /** + * actually build a TimelineWidget to enact the role represented by this smart-handle. + * @remark some implementation twist involved here, since TimelinePanel manages a collection + * of `unique_ptr`, in order to be able to hold an empty placeholder page. + * Since C++ unfortunately does not support _Covariance_ proper, we need to fabricate a + * unique_ptr right here, which can than be handed over to TimelinePanel, + * yet still we _do inherit_ from WLink, i.e. expose the subclass. + * The `unque_ptr` owns and manages the TimelineWidget, which is itself non-copyable + * and stays at a fixed location in heap memory, as is required by gui::ctrl::Nexus + * to maintain a registration of the UI-Bus connection. WLink to the contrary + * just connects to the widget, and is automatically disconnected when it dies. + */ + unique_ptr + TimelineGui::buildTimelineWidget (BusTerm& nexus) + { + TimelineWidget* widget; + unique_ptr pageHandle {widget = new TimelineWidget{timelineID_, rootTrackID_, nexus}}; + this->connect (*widget); + return pageHandle; + } + + + /** + * forwarding implementation of the DiffMutable interface, + * delegating through the referred TimelineWidget if present, + * yet silently ignoring any diff messages otherwise. + * @see tree-mutator.hpp + */ + void + TimelineGui::buildMutator (lib::diff::TreeMutator::Handle buffer) + { + if (this->isActive()) + operator*().buildMutator (buffer); // delegate to TimelineController + else // else when no widget exists... + buffer.create( + TreeMutator::build() + .ignoreAllChanges()); // ...consume and ignore diff + } + + +}}// namespace gui::timeline diff --git a/src/gui/timeline/timeline-gui.hpp b/src/gui/timeline/timeline-gui.hpp new file mode 100644 index 000000000..495645cde --- /dev/null +++ b/src/gui/timeline/timeline-gui.hpp @@ -0,0 +1,106 @@ +/* + TIMELINE-GUI.hpp - top-level anchor point and placeholder for "the timeline" + + Copyright (C) Lumiera.org + 2018, 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 timeline/timeline-gui.hpp + ** Anchor point and placeholder for the UI representation of a Timeline from the session. + ** TimelineGui elements are what is managed by the InteractionDirector as children reflecting + ** the top-level collection of Timelines within the session. However, these elements are proxies, + ** implemented as [smart-handle for a widget](\ref model::WLink) -- because the fact _that_ a given + ** timeline _is indeed presented_ needs to remain at the UI's discretion, retaining the latter's total + ** authority in question of interaction materiality. Moreover, the actual TimelineWidget is not at all + ** attached within the InteractionDirector as a managing entity, it is rather installed as a child widget + ** within a notebook control at some [timeline docking panel](\ref TimelinePanel). There could be several + ** such timeline panels in various top-level windows, and it is a question within the user's workspace + ** arrangement where exactly it will be placed, if at all. So TimelineGui acts as smart-handle to access + ** such a widget, and it stands in as a proxy to receive diff messages for this given timeline. Yet when + ** this timeline is not present in the UI, such diff messages and other UI-Bus messages addressed for + ** this entity are to be silently ignored. + ** + ** \par how the timeline presentation structure is built + ** The actual UI representation of a timeline tends to be a highly complex tree of interconnected widgets. + ** There is no "master plan" knowledge within the UI how to build such a structure, rather this structure + ** evolves in response to _population diff messages_ from the core. Which in turn are either produced + ** initially, or in answer to a population request originating from within the UI, prompted by the + ** need to expose some timeline at a given place within the interface. + ** + ** @see gui::interact::InteractionDirector::buildMutator + ** @see gui::timeline::TimelineWidget + ** + */ + + +#ifndef GUI_TIMELINE_TIMELINE_GUI_H +#define GUI_TIMELINE_TIMELINE_GUI_H + +#include "gui/ctrl/bus-term.hpp" +#include "gui/model/w-link.hpp" +#include "lib/diff/diff-mutable.hpp" +#include "lib/nocopy.hpp" + +#include + + +namespace gui { +namespace timeline { + + class TimelinePage; + class TimelineWidget; + using ctrl::BusTerm; + + /** + * smart-Handle as anchor point for "the UI representation" of a timeline. + * A collection of TimelineGui elements is managed as children of the InteractionDirector. + * The corresponding TimelineWidget may or may not be present within the UI currently. + */ + class TimelineGui + : public model::WLink + , public lib::diff::DiffMutable + , util::Cloneable + { + using ID = BusTerm::ID; + + ID timelineID_; + ID rootTrackID_; + + public: + TimelineGui (ID identity, ID trackID); + virtual ~TimelineGui(); + + TimelineGui (TimelineGui const&); + TimelineGui (TimelineGui &&); + + operator ID() const { return timelineID_; } + + + /** actually build a TimelineWidget to enact the role + * represented by this smart-handle */ + std::unique_ptr buildTimelineWidget (BusTerm&); + + /** @internal this method is invoked by the UI-Bus when dispatching a MutationMessage... */ + void buildMutator (lib::diff::TreeMutator::Handle buffer) override; + + }; + + +}}// namespace gui::timeline +#endif /*GUI_TIMELINE_TIMELINE_GUI_H*/ diff --git a/src/gui/timeline/layout-manager.cpp b/src/gui/timeline/timeline-layout.cpp similarity index 53% rename from src/gui/timeline/layout-manager.cpp rename to src/gui/timeline/timeline-layout.cpp index 24d6b9b10..3028bce8f 100644 --- a/src/gui/timeline/layout-manager.cpp +++ b/src/gui/timeline/timeline-layout.cpp @@ -1,5 +1,5 @@ /* - LayoutManager - global timeline layout management and display control + TimelineLayout - global timeline layout management and display control Copyright (C) Lumiera.org 2016, Hermann Vosseler @@ -21,16 +21,17 @@ * *****************************************************/ -/** @file layout-manager.cpp +/** @file timeline-layout.cpp ** Implementation details of global timeline layout management. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ #include "gui/gtk-base.hpp" -#include "gui/timeline/layout-manager.hpp" +#include "gui/timeline/timeline-layout.hpp" //#include "gui/ui-bus.hpp" //#include "lib/format-string.hpp" @@ -58,16 +59,35 @@ namespace timeline { + TimelineLayout::~TimelineLayout() { } - LayoutManager::LayoutManager () + TimelineLayout::TimelineLayout (Gtk::Paned& topLevelContainer) + : paneSplitPosition_{topLevelContainer.property_position()} + , bodyCanvas_{} + , headerPane_{bodyCanvas_.get_vadjustment()} // wire the header pane (Gtk::Viewport) to follow the body vertical scroll movement { + topLevelContainer.add1 (headerPane_); + topLevelContainer.add2 (bodyCanvas_); } - LayoutManager::~LayoutManager() + /** + * This function is invoked once for each new TimelineWidget, in order to build the + * starting point for the track widget structure, which then can be extended recursively + * to add further nested tracks. The central problem for this widget hierarchy is that + * we have to build two matching structures in parallel... + * - the track header area ("patchbay") + * - the corresponding track body with actual content (clips) + */ + void + TimelineLayout::installRootTrack(TrackHeadWidget& head, TrackBody& body) { + headerPane_.installForkRoot (head); + bodyCanvas_.installForkRoot (body); } + + diff --git a/src/gui/timeline/layout-manager.hpp b/src/gui/timeline/timeline-layout.hpp similarity index 70% rename from src/gui/timeline/layout-manager.hpp rename to src/gui/timeline/timeline-layout.hpp index e73d52384..6a14b359d 100644 --- a/src/gui/timeline/layout-manager.hpp +++ b/src/gui/timeline/timeline-layout.hpp @@ -1,5 +1,5 @@ /* - LAYOUT-MANAGER.hpp - global timeline layout management and display control + TIMELINE-LAYOUT.hpp - global timeline layout management and display control Copyright (C) Lumiera.org 2016, Hermann Vosseler @@ -21,7 +21,7 @@ */ -/** @file layout-manager.hpp +/** @file timeline-layout.hpp ** A core service of the timeline UI to ensure consistent display and layout ** of all components within the timeline. The content of the timeline is organised ** into several nested collections, possibly holding several thousand individual elements. @@ -40,11 +40,12 @@ ** # Architecture ** ** A naive approach would have a global layout manager drill down into some model storage - ** and reach into the components to manipulate and adjust the layout to fit. Doing so can - ** be considered, since this links together details scattered all over the model into a - ** huge global process carried out at a single code location. Any further extension or - ** evolution of details of the UI presentation are bound to be worked into this core - ** global piece of code, which soon becomes brittle, hard to understand and generally + ** and reach into the components to manipulate and adjust the layout to fit. Yet however + ** straight forward and adequate this might seem, following this routine is a recipe for + ** disaster, since this procedure now ties and links together details scattered all over + ** the model into a huge global process, carried out at a single code location. Any further + ** extension or evolution of details of the UI presentation are bound to be worked into this + ** core global piece of code, which soon becomes brittle, hard to understand and generally ** a liability and maintenance burden. We have seen this happen in numerous existing ** code bases (and in fact even our own initial approach started to go down that route). ** Thus we strive to break up the whole process of controlling the layout into several @@ -57,13 +58,14 @@ ** ** Whenever the layout of timeline contents has to be (re)established, we trigger a recursive ** evaluation pass, which in fact is a tree walk. The layout manager creates a DisplayEvaluation - ** record, which is passed to the [Element's allocate function](Element::allocate). The element - ** in turn has the liability to walks its children and recursively initiate a nested evaluation + ** record, which is passed to the [Element's allocate function](\ref Element::allocate). The element + ** in turn has the liability to walk its children and recursively initiate a nested evaluation ** by invoking DisplayEvaluation::evaluateChild(Element), which in turn calls back to ** LayoutManager::evaluate() to initiate a recursive evaluation pass. Within the recursively ** created DisplayEvaluation elements, we are able to transport and aggregate information ** necessary to give each element it' screen allocation. And this in turn allows us to - ** decide upon a suitable display strategy for each individual element. + ** decide upon a suitable display strategy for each individual element, within a local + ** and self-contained context. ** ** For this to work, the _element_ can not be the actual widget, since the result of this whole ** process might be to create or retract an actual GTK widget. For this reason, the timeline @@ -72,14 +74,17 @@ ** [MVP pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) here. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ -#ifndef GUI_TIMELINE_LAYOUT_MANAGER_H -#define GUI_TIMELINE_LAYOUT_MANAGER_H +#ifndef GUI_TIMELINE_TIMELINE_LAYOUT_H +#define GUI_TIMELINE_TIMELINE_LAYOUT_H #include "gui/gtk-base.hpp" +#include "gui/timeline/header-pane-widget.hpp" +#include "gui/timeline/body-canvas-widget.hpp" //#include "lib/util.hpp" @@ -91,20 +96,35 @@ namespace gui { namespace timeline { + class TrackHeadWidget; + class TrackBody; + /** - * @todo WIP-WIP as of 12/2016 + * Top-level anchor point for the timeline display (widgets). + * The central entity to organise concerns relevant for the presentation of the + * Timeline as a whole, as opposed to rendering individual tracks as part of the Timeline. + * @todo WIP-WIP as of 10/2018 */ - class LayoutManager + class TimelineLayout { + Glib::PropertyProxy paneSplitPosition_; + + BodyCanvasWidget bodyCanvas_; + HeaderPaneWidget headerPane_; + + public: - LayoutManager (); - ~LayoutManager(); - + TimelineLayout (Gtk::Paned&); + ~TimelineLayout(); + + /** @internal anchor the display of the root track into the two display panes */ + void installRootTrack (TrackHeadWidget&,TrackBody&); + private:/* ===== Internals ===== */ }; }}// namespace gui::timeline -#endif /*GUI_TIMELINE_LAYOUT_MANAGER_H*/ +#endif /*GUI_TIMELINE_TIMELINE_LAYOUT_H*/ diff --git a/src/gui/timeline/timeline-widget-empty.cpp b/src/gui/timeline/timeline-widget-empty.cpp deleted file mode 100644 index 2874af081..000000000 --- a/src/gui/timeline/timeline-widget-empty.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/* - TimelineWidgetEmpty - placeholder widget for a missing timeline - - Copyright (C) Lumiera.org - 2016, 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 timeline-widget-empty.cpp - ** Implementation of an empty timeline placeholder. - ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway - ** - */ - - -#include "gui/gtk-base.hpp" -#include "gui/timeline/timeline-widget-empty.hpp" - -//#include "gui/workspace/workspace-window.hpp" -//#include "gui/ui-bus.hpp" -//#include "lib/format-string.hpp" -//#include "lib/format-cout.hpp" - -//#include "lib/util.hpp" -//#include -//#include - - - -//using util::_Fmt; -//using std::shared_ptr; -//using std::weak_ptr; -//using util::contains; -//using Gtk::Widget; -//using sigc::mem_fun; -//using sigc::ptr_fun; -//using std::cout; -//using std::endl; - - -namespace gui { -namespace timeline { - -//const int TimelineWidget::TrackPadding = 1; -//const int TimelineWidget::HeaderWidth = 150; -//const int TimelineWidget::HeaderIndentWidth = 10; - - - - - TimelineWidgetEmpty::TimelineWidgetEmpty () - { - } - - - TimelineWidgetEmpty::~TimelineWidgetEmpty() - { - } - - - - - -}}// namespace gui::timeline diff --git a/src/gui/timeline/timeline-widget-empty.hpp b/src/gui/timeline/timeline-widget-empty.hpp index 4bb681a19..6d587427f 100644 --- a/src/gui/timeline/timeline-widget-empty.hpp +++ b/src/gui/timeline/timeline-widget-empty.hpp @@ -23,12 +23,12 @@ /** @file timeline-widget-empty.hpp ** Empty placeholder to be rendered when the UI starts without session. - ** The TimelinePannel represents the [model root](\ref mobject::session::Root) and thus - ** has the responsibility to manage the individual TimelineWidget instances. In case the - ** UI is started with no usable session loaded in Proc-Layer, this empty placeholder widget - ** can be inserted. - ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway + ** A TimelinePannel is able to expose several timelines in a tabbed notebook. + ** However, the actual [model root](\ref mobject::session::Root) is represented by the + ** InteractionDirector, which receives the timeline contents pushed up from the session + ** as diff messages and injects a TimelineWidget for each of them. But when not timeline + ** is present and thus no actual TimelineWidget was yet instantiated, the TimelinePanel + ** will install this TimelineWidgetEmpty placeholder instead to fill the void. ** */ @@ -37,12 +37,7 @@ #define GUI_TIMELINE_TIMELINE_WIDGET_EMPTY_H #include "gui/gtk-base.hpp" - -#include "lib/time/timevalue.hpp" - -//#include -//#include - +#include "gui/timeline/timeline-widget.hpp" namespace gui { @@ -50,31 +45,52 @@ namespace timeline { /** - * Core timeline display (custom widget). - * @todo WIP-WIP-rewrite as of 12/2016 - * @remarks At top level, this widget is split into a header pane (left) - * and a scrollable timeline body (right). The layout of both parts is aligned. + * Placeholder to show when no actual Timeline is present. + * Typically this happens when the application starts up and no session has been loaded (yet). + * @todo This widget could offer a list of last sessions to pick from etc... + * @remarks We just display some quote to fill the void... */ class TimelineWidgetEmpty + : public TimelinePage { + + Gtk::Frame frame_; + Gtk::Box content_; + Gtk::Label messageText_; + Gtk::Label quoteText_; + + Gtk::Image image_; + + cuString + getLabel() const override + { + return _("[virgin]"); + } + public: - /** - * @param source_state state to be used used as the - * data source (model) for this timeline widget. - */ - TimelineWidgetEmpty(); - ~TimelineWidgetEmpty(); - - - - public: /* ===== Control interface ===== */ - - public: /* ===== Signals ===== */ - - private:/* ===== Events ===== */ - - private:/* ===== Internals ===== */ - + TimelineWidgetEmpty() + : TimelinePage{} + , content_{Gtk::ORIENTATION_VERTICAL} + , image_{Gtk::StockID{"track_enabled"}, Gtk::ICON_SIZE_LARGE_TOOLBAR} + { + messageText_.set_markup("To be or not to be that is the question..."); + messageText_.set_max_width_chars(80); + messageText_.set_line_wrap(); + messageText_.set_halign(Gtk::ALIGN_FILL); + messageText_.set_valign(Gtk::ALIGN_FILL); + quoteText_.set_markup ("-- the great actor Joseph Tura"); + quoteText_.set_halign(Gtk::ALIGN_END); + quoteText_.set_valign(Gtk::ALIGN_START); + content_.pack_start (messageText_, Gtk::PACK_SHRINK); + content_.pack_start (quoteText_, Gtk::PACK_EXPAND_PADDING); + frame_.add(content_); + frame_.set_border_width(5); + frame_.set_halign(Gtk::ALIGN_END); + frame_.set_valign(Gtk::ALIGN_START); + + Gtk::Paned::add1 (image_); + Gtk::Paned::add2 (frame_); + } }; diff --git a/src/gui/timeline/timeline-widget.cpp b/src/gui/timeline/timeline-widget.cpp index 52a9bc824..6ae5c7e63 100644 --- a/src/gui/timeline/timeline-widget.cpp +++ b/src/gui/timeline/timeline-widget.cpp @@ -24,7 +24,7 @@ /** @file timeline/timeline-widget.cpp ** Implementation details of Lumiera's timeline display widget. ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway + ** @todo as of 10/2018 a complete rework of the timeline display is underway ** @see timeline-controller.cpp ** */ @@ -32,6 +32,8 @@ #include "gui/gtk-base.hpp" #include "gui/timeline/timeline-widget.hpp" +#include "gui/timeline/timeline-controller.hpp" +#include "gui/timeline/timeline-layout.hpp" //#include "gui/workspace/workspace-window.hpp" //#include "gui/ui-bus.hpp" @@ -66,19 +68,29 @@ namespace timeline { TimelineWidget::TimelineWidget (BusTerm::ID identity, BusTerm::ID trackID, BusTerm& nexus) - : Gtk::Paned{Gtk::ORIENTATION_VERTICAL} - , control_{new TimelineController{identity, trackID, nexus}} - , layout_{new LayoutManager} + : TimelinePage{} + , layout_{new TimelineLayout{*this}} + , control_{new TimelineController{identity, trackID, nexus, *layout_}} { - UNIMPLEMENTED ("build the timeline UI"); + show_all(); } + TimelineWidget::~TimelineWidget() { } - TimelineWidget::~TimelineWidget() + + void + TimelineWidget::buildMutator (lib::diff::TreeMutator::Handle buff) { + control_->buildMutator (buff); } + cuString + TimelineWidget::getLabel() const + { + return control_->getName(); + } + diff --git a/src/gui/timeline/timeline-widget.hpp b/src/gui/timeline/timeline-widget.hpp index 1128cf871..12f39757e 100644 --- a/src/gui/timeline/timeline-widget.hpp +++ b/src/gui/timeline/timeline-widget.hpp @@ -33,13 +33,13 @@ ** to a single session::Timeline, known by its ID. The widget creates a TimelineController ** right away, which takes initiative to populate the display with that Timeline's contents. ** - ** #Lifecycle + ** # Lifecycle ** The assumption is that any element creation and deletion is triggered through messages over ** the [UI-Bus](\ref ui-bus.hpp). So there will be a _parent element,_ corresponding to the - ** ["model root"](\ref session::Root), and this parent, in response to some mutation message, - ** will create a TimelineWidget, add it into the appropriate GTK display setup and manage it - ** as child element; the [construction parameters](TimelineWidget::TimelineWidget] ensure - ** it gets connected to the bus as well. Incidentally, this assumption also implies that + ** [model root](\ref proc::mobject::session::Root), and this parent, in response to some + ** mutation message, will create a TimelineWidget, add it into the appropriate GTK display setup + ** and manage it as child element; the [construction parameters](\ref TimelineWidget::TimelineWidget) + ** ensure it gets connected to the bus as well. Incidentally, this assumption also implies that ** this parent element has set up a _binding for diff mutation,_ typically by implementing ** model::Tangible::buildMutator. And further on this means that the parent will also ** destroy the TimelineWidget, prompted by a message to that end. All deregistration @@ -47,7 +47,7 @@ ** is `sigc::trackable`, which means after destruction any further signals ** will be silently ignored. ** - ** @todo as of 12/2016 a complete rework of the timeline display is underway + ** @todo as of 10/2018 a complete rework of the timeline display is underway ** */ @@ -56,12 +56,13 @@ #define GUI_TIMELINE_TIMELINE_WIDGET_H #include "gui/gtk-base.hpp" -#include "gui/timeline/timeline-controller.hpp" /////TODO possible to push that into the implementation? -#include "gui/timeline/layout-manager.hpp" +//#include "gui/timeline/timeline-controller.hpp" /////TODO possible to push that into the implementation? +//#include "gui/timeline/layout-manager.hpp" #include "gui/ctrl/bus-term.hpp" #include "lib/time/timevalue.hpp" #include "lib/diff/diff-mutable.hpp" +#include "lib/nocopy.hpp" //#include //#include @@ -77,6 +78,25 @@ namespace gui { namespace timeline { using ctrl::BusTerm; + class TimelineController; + class TimelineLayout; + + /** + * Interface: GUI page holding a timeline display + */ + class TimelinePage + : public Gtk::Paned + { + public: + virtual ~TimelinePage() { } ///< this is an interface + + virtual cuString getLabel() const =0; + + + TimelinePage() + : Gtk::Paned{Gtk::ORIENTATION_HORIZONTAL} + { } + }; /** * Core timeline display (custom widget). @@ -86,10 +106,11 @@ namespace timeline { * and a scrollable timeline body (right). The layout of both parts is aligned. */ class TimelineWidget - : public Gtk::Paned + : public TimelinePage + , util::NonCopyable { + std::unique_ptr layout_; std::unique_ptr control_; - std::unique_ptr layout_; public: /** build a new timeline display and attach it to the UI-Bus. @@ -113,12 +134,18 @@ namespace timeline { */ TimelineWidget (BusTerm::ID identity, BusTerm::ID trackID, BusTerm& nexus); - ~TimelineWidget(); + ~TimelineWidget(); public: /* ===== Control interface ===== */ + /** allow for diff mutation (delegated to TimelineController */ + void buildMutator (lib::diff::TreeMutator::Handle); + + cuString getLabel() const override; + + public: /* ===== Signals ===== */ private:/* ===== Events ===== */ diff --git a/src/gui/timeline/track-body.cpp b/src/gui/timeline/track-body.cpp index 081a85842..ac485de3a 100644 --- a/src/gui/timeline/track-body.cpp +++ b/src/gui/timeline/track-body.cpp @@ -56,20 +56,47 @@ namespace gui { namespace timeline { - - - - - TrackBody::TrackBody () - { - } - - - TrackBody::~TrackBody() - { + namespace { + const uint DEFAULT_OVERVIEW_HEIGHT_px = 20; + const uint DEFAULT_CONTENT_HEIGHT_px = 80; } + TrackBody::TrackBody() + : overviewHeight_{DEFAULT_OVERVIEW_HEIGHT_px} + , contentHeight_{DEFAULT_OVERVIEW_HEIGHT_px} + , subTracks_{} + { } + + + TrackBody::~TrackBody() + { + TODO ("detach from parent; store a functor or backreference"); + } + + + void + TrackBody::setTrackName (cuString& trackName) + { + TODO ("is the track name of any relevance for the TrackBody widget?"); + } + + + /** + * recursively calculate the height in pixels to display this track, + * including all nested sub-tracks + */ + uint + TrackBody::calcHeight() + { + uint heightSum = overviewHeight_ + contentHeight_; + for (TrackBody* subTrack : subTracks_) + heightSum += subTrack->calcHeight(); + return heightSum; + } + + + }}// namespace gui::timeline diff --git a/src/gui/timeline/track-body.hpp b/src/gui/timeline/track-body.hpp index 5e271115f..f9f60401c 100644 --- a/src/gui/timeline/track-body.hpp +++ b/src/gui/timeline/track-body.hpp @@ -29,7 +29,7 @@ ** yet for coordination of a globally consistent timeline layout, each ** track display is coordinated by a TrackPresenter, which corresponds ** to a session::Fork and directly controls the respective display elements - ** in the [header pane](timeline::HeaderPaneWidget) and the display of the + ** in the [header pane](\ref timeline::HeaderPaneWidget) and the display of the ** timeline body, which is actually a canvas for custom drawing. ** ** @todo WIP-WIP-WIP as of 12/2016 @@ -41,27 +41,50 @@ #define GUI_TIMELINE_TRACK_BODY_H #include "gui/gtk-base.hpp" +#include "gui/ctrl/bus-term.hpp" //#include "lib/util.hpp" //#include -//#include +#include namespace gui { namespace timeline { + using ID = ctrl::BusTerm::ID; + /** - * @todo WIP-WIP as of 12/2016 + * Helper to organise and draw the space allocated for a fork of sub-tracks. + * TrackBody units work together with the TimelineCanvas, which arranges all + * elements placed into the tracks and performs custom drawing to mark the + * working space available for placing those elements (Clips, Effects, Markers). + * A given TrackBody works with coordinates relative to its vertical starting point; + * coordinates on the TimelineCanvas operate from top downwards. The fundamental + * task of a TrackBody is to find out about its own overall height, including the + * overall height require by all its nesting children. Moreover, the height of + * the content area needs to be negotiated with the actual content elements. + * @todo WIP-WIP as of 10/2018 */ class TrackBody { + uint overviewHeight_; + uint contentHeight_; + + using SubTracks = std::vector; + + SubTracks subTracks_; + public: - TrackBody (); + TrackBody(); ~TrackBody(); - + + void setTrackName (cuString&); + + uint calcHeight(); + private:/* ===== Internals ===== */ }; diff --git a/src/gui/timeline/track-head-widget.cpp b/src/gui/timeline/track-head-widget.cpp index 3bb54282a..e4748e996 100644 --- a/src/gui/timeline/track-head-widget.cpp +++ b/src/gui/timeline/track-head-widget.cpp @@ -22,8 +22,7 @@ /** @file track-head-widget.cpp - ** Implementation details regarding display of the track heads - ** within the timeline UI. + ** Implementation details regarding display of the track heads within the timeline UI. ** ** @todo WIP-WIP-WIP as of 12/2016 ** @@ -59,17 +58,44 @@ namespace timeline { + TrackHeadWidget::~TrackHeadWidget() { } - TrackHeadWidget::TrackHeadWidget () + + + TrackHeadWidget::TrackHeadWidget() + : Gtk::Grid{} + , nameTODO_{"?"} + , treeTODO_{"↳"} { + this->attach (nameTODO_, 0,0, 2,1); + this->attach (treeTODO_, 0,1, 1,1); + + this->show_all(); } - TrackHeadWidget::~TrackHeadWidget() + void + TrackHeadWidget::setTrackName (cuString& trackName) + { + nameTODO_.set_label (trackName); + } + + /** + * @remark The Lumiera Timeline model does not rely on a list of tracks, as most conventional + * video editing software does -- rather, each sequence holds a _fork of nested scopes._ + * This recursively nested structure is reflected in the patchbay area corresponding to + * each track in the _header pane_ of the timeline display, located to the left. The + * patchbay for each track is a grid with four quadrants, and the 4th quadrant is the + * _content area,_ which is recursively extended to hold nested PatchbayWidget elements, + * corresponding to the child tracks of this track. To _fund_ this recursively extensible + * structure, we need to set up the first four quadrants + */ + void + TrackHeadWidget::injectSubFork (TrackHeadWidget& subForkHead) { + UNIMPLEMENTED ("how actually to represent the track in the patchbay"); } - }}// namespace gui::timeline diff --git a/src/gui/timeline/track-head-widget.hpp b/src/gui/timeline/track-head-widget.hpp index 3de1be01e..19d158ca4 100644 --- a/src/gui/timeline/track-head-widget.hpp +++ b/src/gui/timeline/track-head-widget.hpp @@ -22,10 +22,22 @@ /** @file track-head-widget.hpp - ** Widget to represent a track head with placement control patchbay - ** within Lumiera's timeline UI. + ** Widget to represent a track head with placement parameters, within the timeline header pane. + ** [The fork](\ref session::Fork), a recursively nested system of scopes, is rendered within + ** the timeline display as a system of nested tracks. Each of these tracks possibly holds some + ** child tracks plus some actual media clips, which all inherit parameters of placement from + ** this fork ("track"). These parameters address various aspects of how content is attached + ** ("placed") into the model at large, examples being + ** - how to route the output + ** - how to "place" this content into an _output space_, like e.g. + ** + sound panning + ** + video overlay parameters (additive, opaque, transparent) + ** + video or audio _level_ (=fader) + ** - how to locate this content in time (e.g. relative to some marker) + ** For each track, we show a patchbay in the timeline header pane, which serves to control + ** such aspects relevant for all content contained within the scope of this track. ** - ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo WIP-WIP-WIP as of 10/2018 ** */ @@ -34,6 +46,7 @@ #define GUI_TIMELINE_TRACK_HEAD_WIDGET_H #include "gui/gtk-base.hpp" +#include "gui/ctrl/bus-term.hpp" //#include "lib/util.hpp" @@ -45,16 +58,30 @@ namespace gui { namespace timeline { + using ID = ctrl::BusTerm::ID; /** + * Header pane control area corresponding to a Track with nested child Tracks. + * This structure is used recursively to build up the Fork of nested Tracks. + * - first row: Placement + Property pane + * - second row: content or nested tracks. * @todo WIP-WIP as of 12/2016 */ class TrackHeadWidget + : public Gtk::Grid { + Gtk::Label nameTODO_; + Gtk::Label treeTODO_; + public: - TrackHeadWidget (); + TrackHeadWidget(); ~TrackHeadWidget(); - + + void setTrackName (cuString&); + + /** Integrate the control area for a nested sub track fork. */ + void injectSubFork (TrackHeadWidget& subForkHead); + private:/* ===== Internals ===== */ }; diff --git a/src/gui/timeline/track-presenter.cpp b/src/gui/timeline/track-presenter.cpp index cc6e3e993..e84610543 100644 --- a/src/gui/timeline/track-presenter.cpp +++ b/src/gui/timeline/track-presenter.cpp @@ -25,14 +25,14 @@ ** Implementation details of track presentation management. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ #include "gui/gtk-base.hpp" +#include "include/ui-protocol.hpp" #include "gui/timeline/track-presenter.hpp" -#include "gui/timeline/clip-presenter.hpp" -#include "gui/timeline/marker-widget.hpp" //#include "gui/ui-bus.hpp" //#include "lib/format-string.hpp" @@ -64,31 +64,20 @@ namespace timeline { - TrackPresenter::TrackPresenter (ID identity, ctrl::BusTerm& nexus) - : Controller{identity, nexus} - , subFork_{} - , markers_{} - , clips_{} - , head_{} - , body_{} - { - UNIMPLEMENTED ("how to attach the TrackPresenter into the two relevant GTK display contexts"); - } - TrackPresenter::~TrackPresenter() - { - } + TrackPresenter::~TrackPresenter() { } /** * @note we distinguish between the contents of our three nested child collections - * based on the symbolic type field send in the Record type within the diff representation + * based on the symbolic type field sent in the Record type within the diff representation * - "Marker" designates a Marker object * - "Clip" designates a Clip placed on this track * - "Fork" designates a nested sub-track + * @see TimelineController::buildMutator() for a basic explanation of the data binding mechanism */ void TrackPresenter::buildMutator (TreeMutator::Handle buffer) @@ -102,60 +91,69 @@ namespace timeline { .attach (collection(markers_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Marker" - return "Marker" == spec.data.recordType(); + return TYPE_Marker == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PMarker const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PMarker { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi, this->uiBus_); }) .buildChildMutator ([&](PMarker& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; })) .attach (collection(clips_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Clip" - return "Clip" == spec.data.recordType(); + return TYPE_Clip == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PClip const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PClip { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi, this->uiBus_); }) .buildChildMutator ([&](PClip& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; })) .attach (collection(subFork_) .isApplicableIf ([&](GenNode const& spec) -> bool { // »Selector« : require object-like sub scope with type-field "Fork" - return "Fork" == spec.data.recordType(); + return TYPE_Fork == spec.data.recordType(); }) .matchElement ([&](GenNode const& spec, PFork const& elm) -> bool { - return spec.idi == ID(elm); + return spec.idi == ID{*elm}; }) .constructFrom ([&](GenNode const& spec) -> PFork { - return make_unique(spec.idi, this->uiBus_); + return make_unique (spec.idi + ,this->uiBus_ + ,[&](TrackHeadWidget& head,TrackBody& body) + { + display_.injectSubTrack (head, body); + }); }) .buildChildMutator ([&](PFork& target, GenNode::ID const& subID, TreeMutator::Handle buff) -> bool { - if (ID(target) != subID) return false; + if (ID{*target} != subID) return false; target->buildMutator (buff); return true; - }))); + })) + .change(ATTR_name, [&](string val) + { // »Attribute Setter« : receive a new value for the track name field + this->setTrackName (val); + })); } diff --git a/src/gui/timeline/track-presenter.hpp b/src/gui/timeline/track-presenter.hpp index 41df348f5..70b4d8c97 100644 --- a/src/gui/timeline/track-presenter.hpp +++ b/src/gui/timeline/track-presenter.hpp @@ -24,7 +24,7 @@ /** @file track-presenter.hpp ** Presentation control element to model and manage a track within the timeline UI. ** In the Lumiera timeline UI, we are mixing two different scope of concerns: For one, - ** we have the globally tangible scope of actual session elements an operations performed + ** we have the globally tangible scope of actual session elements and operations performed ** on those. And then there are more local considerations regarding the "mechanics" of the ** UI elements, their state and immediate feedback to user interactions. The _Presenter_ -- ** as known from the [MVP pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) -- @@ -36,9 +36,10 @@ ** part of layout building, delegating to a mostly passive GTK widget for the actual display. ** This way it becomes possible to manage the actual UI resources on a global level, avoiding to ** represent potentially several thousand individual elements as GTK entities, while at any time - ** only several can be visible and active as far as user interaction is concerned. + ** only a small number of elements can be visible and active as far as user interaction is concerned. ** ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo as of 10/2018 timeline display in the UI is rebuilt to match the architecture ** */ @@ -48,12 +49,16 @@ #include "gui/gtk-base.hpp" #include "gui/model/controller.hpp" +#include "gui/timeline/marker-widget.hpp" +#include "gui/timeline/clip-presenter.hpp" #include "gui/timeline/track-head-widget.hpp" #include "gui/timeline/track-body.hpp" +#include "lib/nocopy.hpp" //#include "lib/util.hpp" //#include +#include #include @@ -64,8 +69,43 @@ namespace timeline { using std::vector; using std::unique_ptr; - class ClipPresenter; - class MarkerWidget; + + /** + * Reference frame to organise the display related to a specific Track in the Timeline-GUI. + */ + struct DisplayFrame + : util::NonCopyable + { + TrackHeadWidget head; + TrackBody body; + + template + DisplayFrame (FUN anchorDisplay) + : head{} + , body{} + { + anchorDisplay (head, body); + } + + ~DisplayFrame() + { + TODO ("cause the managed presentation elements to detach from their parents"); + } + + void + setTrackName (cuString& name) + { + head.setTrackName (name); + body.setTrackName (name); ///////////////////////////////////TICKET #1017 -- TODO 11/18 : not clear yet if TrackBody needs to know its name + } + + void + injectSubTrack (TrackHeadWidget& head, TrackBody& body) + { + UNIMPLEMENTED ("inject the widgets to represent a nested sub-track within this timeline track display frame"); + } + }; + /** * @todo WIP-WIP as of 12/2016 @@ -73,22 +113,30 @@ namespace timeline { class TrackPresenter : public model::Controller { + DisplayFrame display_; + vector> subFork_; vector> markers_; vector> clips_; - TrackHeadWidget head_; - TrackBody body_; - public: + ~TrackPresenter(); + /** * @param identity used to refer to a corresponding session::Fork in the Session * @param nexus a way to connect this Controller to the UI-Bus. */ - TrackPresenter (ID identity, ctrl::BusTerm& nexus); - - ~TrackPresenter(); + template + TrackPresenter (ID id, ctrl::BusTerm& nexus, FUN anchorDisplay) + : Controller{id, nexus} + , display_{anchorDisplay} + , subFork_{} + , markers_{} + , clips_{} + { + setTrackName (id.getSym()); // fallback initialise track-name from human-readable ID symbol + } /** set up a binding to respond to mutation messages via UiBus */ @@ -96,6 +144,13 @@ namespace timeline { private:/* ===== Internals ===== */ + + /** invoked via diff to show a (changed) track name */ + void + setTrackName (string name) + { + display_.setTrackName (name); + } }; diff --git a/src/gui/ui-bus.cpp b/src/gui/ui-bus.cpp index 999dcedce..d6c3233f7 100644 --- a/src/gui/ui-bus.cpp +++ b/src/gui/ui-bus.cpp @@ -25,9 +25,9 @@ /** @file ui-bus.cpp ** Implementation of the UI backbone service for messaging. ** Any globally relevant widget or controller within the Lumiera UI - ** is connected to the [UI-Bus](ui-bus.hpp), which is essentially implemented + ** is connected to the [UI-Bus](\ref ui-bus.hpp), which is essentially implemented ** within this compilation unit. [Clients](\ref gui::model::Tangible) typically - ** use the [BusTerm-Interface](bus-term.hpp) to route generic actions and + ** use the [BusTerm-Interface](\ref bus-term.hpp) to route generic actions and ** receive notifications, state changes and mutations. ** ** @see core-service.hpp @@ -143,7 +143,7 @@ namespace ctrl { /** capture and record a "state mark" for later replay for restoring UI state. - * @param subject the [endpoint-ID](BusTerm::endpointID_) of the emitting element + * @param subject the [endpoint-ID](\ref BusTerm::endpointID_) of the emitting element * @param mark the actual state update or notification message to be remembered * @remarks relevant changes to presentation state, which are to be recalled and * restored later, are emitted from the place they occur, packaged as @@ -162,11 +162,11 @@ namespace ctrl { /** route a state update or notification to the given subject. - * @param subject the [endpoint-ID](BusTerm::endpointID_) of the element to address + * @param subject the [endpoint-ID](\ref BusTerm::endpointID_) of the element to address * @param mark the actual state update or notification message to be delivered * @remarks each addressed "subject" is a gui::model::Tangible, and as such holds * a BusTerm of its own, which in turn ensures a registration and connection - * from the [central routing hub](gui::ctrl::Nexus) down to the element. Thus, + * from the [central routing hub](\ref gui::ctrl::Nexus) down to the element. Thus, * the default implementation is just to pass the given state mark "up", * assuming that it will reach the hub eventually, which in turn knows * how to reach the element, and invoke the Tangible::mark() operation. diff --git a/src/gui/ui-bus.hpp b/src/gui/ui-bus.hpp index 6455b9cfb..d6788528d 100644 --- a/src/gui/ui-bus.hpp +++ b/src/gui/ui-bus.hpp @@ -64,11 +64,11 @@ ** We distinguish between _up-link messages,_ directed towards some central service ** (presentation state management or command invocation) and _down-link messages,_ ** directed towards individual elements. The interactions at the bus are closely interrelated - ** with the [elementary UI-Element operations](tangible.hpp). + ** with the [elementary UI-Element operations](\ref tangible.hpp). ** ** - *act*: send a [GenNode] representing the action ** - the ID is either a globally registered command-ID or an explicitly - ** ["opened"](proc::control::SessionCommand::cycle(Symbol,string)) command instance ID. + ** ["opened"](\ref proc::control::SessionCommand::cycle(Symbol,string)) command instance ID. ** - the payload is a Record holding the actual command arguments ** - on reception, an _instance_ (anonymous clone copy) of the command is created, bound ** with the arguments and handed over to the ProcDispatcher to be enqueued for execution. diff --git a/src/gui/widget/label-widget.cpp b/src/gui/widget/element-box-widget.cpp similarity index 81% rename from src/gui/widget/label-widget.cpp rename to src/gui/widget/element-box-widget.cpp index 1882506df..7156a0e92 100644 --- a/src/gui/widget/label-widget.cpp +++ b/src/gui/widget/element-box-widget.cpp @@ -1,5 +1,5 @@ /* - LabelWidget - display an ID label, possibly with icon + ElementBoxWidget - fundamental UI building block to represent a placed element Copyright (C) Lumiera.org 2016, Hermann Vosseler @@ -24,13 +24,13 @@ /** @file label-widget.cpp ** Implementation details of the UI building block to display an ID label. ** - ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo WIP-WIP-WIP as of 11/2018 ///////////////////////////////////////////////////////////////////////TICKET #1185 ** */ #include "gui/gtk-base.hpp" -#include "gui/widget/label-widget.hpp" +#include "gui/widget/element-box-widget.hpp" //#include "gui/ui-bus.hpp" //#include "lib/format-string.hpp" @@ -59,12 +59,12 @@ namespace widget { - LabelWidget::LabelWidget () + ElementBoxWidget::ElementBoxWidget () { } - LabelWidget::~LabelWidget() + ElementBoxWidget::~ElementBoxWidget() { } diff --git a/src/gui/widget/label-widget.hpp b/src/gui/widget/element-box-widget.hpp similarity index 76% rename from src/gui/widget/label-widget.hpp rename to src/gui/widget/element-box-widget.hpp index 904a4b5eb..530961ef0 100644 --- a/src/gui/widget/label-widget.hpp +++ b/src/gui/widget/element-box-widget.hpp @@ -1,8 +1,8 @@ /* - LABEL-WIDGET.hpp - display an ID label, possibly with icon + ELEMENT-BOX-WIDGET.hpp - fundamental UI building block to represent a placed element Copyright (C) Lumiera.org - 2016, Hermann Vosseler + 2018, 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 @@ -21,7 +21,7 @@ */ -/** @file label-widget.hpp +/** @file element-box-widget.hpp ** Widget to render an ID label with associated icon. ** This is a UI building block used at various places, e.g. to show ** the name of a clip in the media bins or in the timeline, to indicate @@ -32,13 +32,13 @@ ** support for picking the icon and the indicator style based on some ** notion of _"type"._ ** - ** @todo WIP-WIP-WIP as of 12/2016 + ** @todo WIP-WIP-WIP as of 11/2018 ///////////////////////////////////////////////////////////////////////TICKET #1185 ** */ -#ifndef GUI_WIDGET_LABEL_WIDGET_H -#define GUI_WIDGET_LABEL_WIDGET_H +#ifndef GUI_WIDGET_ELEMENT_BOX_WIDGET_H +#define GUI_WIDGET_ELEMENT_BOX_WIDGET_H #include "gui/gtk-base.hpp" @@ -56,11 +56,11 @@ namespace widget { /** * @todo WIP-WIP as of 12/2016 */ - class LabelWidget + class ElementBoxWidget { public: - LabelWidget (); - ~LabelWidget(); + ElementBoxWidget (); + ~ElementBoxWidget(); private:/* ===== Internals ===== */ @@ -68,4 +68,4 @@ namespace widget { }}// namespace gui::widget -#endif /*GUI_WIDGET_LABEL_WIDGET_H*/ +#endif /*GUI_WIDGET_ELEMENT_BOX_WIDGET_H*/ diff --git a/src/gui/widget/error-log-display.hpp b/src/gui/widget/error-log-display.hpp new file mode 100644 index 000000000..9e75b0977 --- /dev/null +++ b/src/gui/widget/error-log-display.hpp @@ -0,0 +1,337 @@ +/* + ERROR-LOG-DISPLAY.hpp - display of error messages in a text box + + 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 error-log-display.hpp + ** Widget to display error messages to be accessible in non-modal way. + ** Notifications are shown in a simple text window with scrollbars; new + ** entries can be added with a severity level, causing the widget to scroll + ** down to the last line of the content buffer. + ** + ** ## Lifecycle considerations + ** + ** The ErrorLogDisplay is typically managed as child of a container widget. + ** However, the ctrl::NotificationHub as controller does a lookup and attaches + ** to an ErrorLogDisplay found within the InfoBoxPanel, using it as holder for + ** information and error messages pushed into the GUI. For that reason, it is + ** important really _to destroy_ the ErrorLogDisplay, when it is taken out of + ** service. Since ErrorLogDisplay inherits from `sigc::trackable`, it is + ** automatically detached from the WLink implementing this cross-attachment, + ** but only when _actually the dtor is called._ + ** @note this is a special convention; usually it is sufficient just to hide() + ** a GTK widget, because it is then left alone by the event handling and + ** automatically cleaned up at shutdown. Moreover, if you ` remove()` a + ** widget from a container, the container no longer manages the widget, + ** so you are responsible to take ownership in such situations. And, + ** as said, in case of the ErrorLogDisplay, you should `delete` it. + ** @remark the standard use case is to have the ErrorLogDisplay sitting + ** within the InfoBoxPanel. If the latter is destroyed, it destroys + ** its children and all works as expected without much ado. + ** + ** @todo WIP-WIP-WIP as of 9/2017 this is a first draft of a widget to be + ** used as receiver by the GuiNotificationService. + ** @todo WIP-WIP and in 9/2018 this draft gradually shifts towards a prototype + ** how to deal with custom configured widget behaviour, and how to integrate + ** with our GUI framework (#1099) + ** + */ + + +#ifndef GUI_WIDGET_ERROR_LOG_DISPLAY_H +#define GUI_WIDGET_ERROR_LOG_DISPLAY_H + +#include "gui/gtk-base.hpp" +#include "gui/style-scheme.hpp" +#include "gui/model/flash-deco.hpp" +#include "gui/model/expander-revealer.hpp" +#include "include/gui-notification-facade.h" +#include "lib/format-string.hpp" +#include "lib/symbol.hpp" +#include "lib/util.hpp" + +#include +#include + + + +namespace gui { +namespace widget { + + using util::max; + using util::_Fmt; + using lib::Literal; + using std::make_pair; + using std::vector; + using std::move; + + namespace { + + using Tag = Glib::RefPtr; + + /** + * @internal inject some generic standard styles for use in `TextView` components. + * @param tagTable reference to a `TagTable` bound with an existing `Gtk::TextBuffer`. + * @todo 9/2018 dummy placeholder code, later to be transformed into a framework /////////////////////TICKET #1168 : find a way to manage style of custom extended UI elements + */ + inline void + populateStandardTextTags (Glib::RefPtr tagTable) + { + Tag errorTag = Gtk::TextBuffer::Tag::create (cuString{TAG_ERROR}); + errorTag->property_background() = "Yellow"; ////////////////////////////////////////////TICKET #1168 : should be retrieved from a central location + errorTag->property_weight() = PANGO_WEIGHT_BOLD; + tagTable->add (errorTag); + + Tag warnTag = Gtk::TextBuffer::Tag::create (cuString{TAG_WARN}); + warnTag->property_background() = "LightYellow"; ////////////////////////////////////////////TICKET #1168 : should be retrieved from a central location + tagTable->add (warnTag); + } + } + + + + /*********************************************************************//** + * Widget to display log and error messages. + * Based on a multiline text display box with scrollbars. + * Warning and error messages are highlighted by special formatting. + * And [error entries are treated specially](\ref addError()), insofar + * they are tracked by an index, allowing to build additional convenience + * features later on... + * @remarks this is _just a widget_ intended for direct use. By default, + * it is managed by the \ref NotificationHub, which is a controller, + * attached to the UI-Bus and responding to the [UI-Element protocol] + * (\ref tangible.hpp). + */ + class ErrorLogDisplay + : public Gtk::ScrolledWindow + { + + using Mark = Glib::RefPtr; + using Entry = std::pair; + using TextWidget = model::FlashDeco; + + vector errorMarks_; + TextWidget textLog_; + + sigc::signal errorChangedSignal_; + + + public: + ~ErrorLogDisplay() { }; + + ErrorLogDisplay() + : Gtk::ScrolledWindow() + , errorMarks_{} + , textLog_{} + { + set_size_request (200, 80); // must be > 50 for the scrollbar to work properly + property_expand() = true; // always grab any available additional space + set_border_width (10); + set_shadow_type (Gtk::SHADOW_NONE); + + // the vertical scrollbar will always be necessary.... + set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + textLog_.set_editable (false); + this->add (textLog_); + + populateStandardTextTags (textLog_.get_buffer()->get_tag_table()); + } + + model::Expander expand; + model::Revealer reveal; + + + /** empty text buffer and discard all error bookmarks */ + void + clearAll() + { + bool shallNotify = this->isError(); + + errorMarks_.clear(); + size_t lineCnt = max (0, textLog_.get_buffer()->get_line_count() - 1); + string placeholder; + if (lineCnt > 0) + placeholder = _Fmt{_("───════ %d preceding lines removed ════───\n")} % lineCnt; + textLog_.get_buffer()->set_text (placeholder); // discard existing content + + if (shallNotify) + errorChangedSignal_.emit (false); + } + + + /** just add normal information message to buffer, + * without special markup and without expanding the widget */ + void + addInfo (string text) + { + addEntry (text); + } + + /** add an information message, formatted more prominent as warning */ + void + addWarn (string text) + { + addEntry ("WARNING: "+text, TAG_WARN); + } + + /** present an error notification prominently. + * Adds the error text, formatted accordingly to stand out, + * but also stores a [Mark] to bookmark the presence of this + * error entry. And finally expand the display if collapsed. + * + * [Mark]: https://developer.gnome.org/gtkmm-tutorial/stable/sec-textview-buffer.html.en#textview-marks + */ + void + addError (string text) + { + bool shallNotify = not this->isError(); + + errorMarks_.emplace_back( + addEntry ("ERROR: "+text, TAG_ERROR)); + if (not expand.isExpanded()) + expand (true); + + if (shallNotify) + errorChangedSignal_.emit (true); + } + + /** + * clear all mere information messages; + * retain just the previously tagged errors + * @remark in fact populates a new buffer + */ + void + clearInfoMsg() + { + auto newBuff = Gtk::TextBuffer::create (textLog_.get_buffer()->get_tag_table()); + vector newMarks; + for (Entry& entry : errorMarks_) + { + newBuff->insert (newBuff->end(), "\n"); + auto pos = newBuff->end(); + --pos; + newMarks.emplace_back( + make_pair ( + newBuff->create_mark (pos, true), // "left gravity" : stays to the left of inserted text + newBuff->create_mark (pos, false))); // "right gravity": sticks right behind the inserted text)) + + newBuff->insert (pos // copy from old to new buffer, complete with formatting tag + ,entry.first->get_iter() + ,entry.second->get_iter() + ); + } + // install the reduced new buffer + auto oldBuff = textLog_.get_buffer(); + textLog_.set_buffer(newBuff); + swap (errorMarks_, newMarks); + + // add a marker line to indicate the removed old log contents + int oldLines = oldBuff->get_line_count(); + int newLines = newBuff->get_line_count(); + ASSERT (oldLines >= newLines); + addInfo (_Fmt{_("───════ %d old log lines removed ════───\n")} % (oldLines-newLines)); + } + + + /** visit all errors and downgrade the markup; discard all bookmarks */ + void + turnError_into_InfoMsg() + { + if (not isError()) return; + + auto buff = textLog_.get_buffer(); + for (Entry& entry : errorMarks_) + { + auto begin = entry.first->get_iter(); + auto end = entry.second->get_iter(); + + buff->remove_tag_by_name(uString{TAG_ERROR}, begin,end); + buff->apply_tag_by_name (uString{TAG_WARN}, begin,end); + } + errorMarks_.clear(); + errorChangedSignal_.emit (false); + } + + + /** temporarily change display style to prompt for attention; + * set callback-timeout for return to normal state. + * @see \ref gui::CSS_CLASS_FLASH + */ + void + triggerFlash() + { + textLog_.flash(); + } + + + /* ======= Error-State ======= */ + + bool + isError() const + { + return not errorMarks_.empty(); + } + + /** signal fired when error state changes */ + sigc::signal + signalErrorChanged() + { + return errorChangedSignal_; + } + + + + private:/* ===== Internals ===== */ + + /** add message entry to the (ever growing) text buffer. + * @return pair of anonymous marks bracketing the content added + * @remark an entry is content sans the following line break, which is appended automatically. + * We inject the content _between_ two marks, which will adjust when content is altered. + * @remark According to the [API doc], `TextView::scroll_to(iter)` is not reliable; preferably + * we should use a text mark, which could e.g. be the [insert position][insert-mark]. + * The handling of marks and tags is described in the [GTKmm tutorial]. + * @warning Each entry creates a new pair of marks. Not sure about the impact on performance... + * + * [GTKmm tutorial]: https://developer.gnome.org/gtkmm-tutorial/stable/sec-textview-buffer.html.en#textview-marks + * [insert-mark]: https://developer.gnome.org/gtkmm/3.22/classGtk_1_1TextMark.html#details + * [API doc]: https://developer.gnome.org/gtkmm/3.22/classGtk_1_1TextView.html#a8412941c4da9a71a381052d6049164e4 + */ + Entry + addEntry (string const& text, Literal markupTagName =nullptr) + { + auto buff = textLog_.get_buffer(); + buff->insert (buff->end(), "\n"); + auto pos = buff->end(); + --pos; + auto begin = buff->create_mark (pos, true); // "left gravity" : stays to the left of inserted text + auto after = buff->create_mark (pos, false); // "right gravity": sticks right behind the inserted text + if (markupTagName) + buff->insert_with_tag(pos, text, cuString{markupTagName}); + else + buff->insert (pos, text); + textLog_.scroll_to (begin); + return make_pair (move(begin), move(after)); + } + }; + + +}}// namespace gui::widget +#endif /*GUI_WIDGET_ERROR_LOG_DISPLAY_H*/ diff --git a/src/gui/widget/error-log-widget.hpp b/src/gui/widget/error-log-widget.hpp deleted file mode 100644 index 4533e8add..000000000 --- a/src/gui/widget/error-log-widget.hpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - ERROR-LOG-WIDGET.hpp - display of error messages in a text box - - 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 error-log-widget.hpp - ** Widget to display error messages to the use in non-modal way. - ** Notifications are shown in a simple text window with scrollbars; new - ** entries can be added with a severity level, causing the widget to scroll - ** down to the last line of the content buffer. - ** - ** @todo WIP-WIP-WIP as of 9/2017 this is a first draft of a widget to be - ** used as receiver by the GuiNotificationService. - ** - */ - - -#ifndef GUI_WIDGET_ERROR_LOG_WIDGET_H -#define GUI_WIDGET_ERROR_LOG_WIDGET_H - -#include "gui/gtk-base.hpp" -#include "include/gui-notification-facade.h" - -//#include "lib/util.hpp" - -//#include -//#include - - - -namespace gui { -namespace widget { - - - /** - * @todo WIP-WIP as of 9/2017 - * Just a text display box with scrollbars. - * Need to add formatting etc. - */ - class ErrorLogWidget - : public Gtk::ScrolledWindow - { - public: - ~ErrorLogWidget() { }; - - ErrorLogWidget() - : Gtk::ScrolledWindow() - , textLog_{} - { - set_size_request (200, 50); - set_border_width (10); - set_shadow_type (Gtk::SHADOW_NONE); - - // the vertical scrollbar will always be necessary.... - set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); - this->add (textLog_); - textLog_.set_editable (false); - - } - - - void - showMsg (NotifyLevel severity, string const& text) - { - //////////////////////////////////////////////////TICKET #1102 : add formatting according to the error level - switch (severity) { - case NOTE_ERROR: - addEntry ("ERROR: "+text); - break; - case NOTE_WARN: - addEntry ("WARN: "+text); - break; - default: - addEntry (text); - break; - } - } - - - private:/* ===== Internals ===== */ - - Gtk::TextView textLog_; - - /** add message entry to the (ever growing) text buffer. - * @remark According to the Gtkmm tutorial, TextView::scroll_to(iter) is not reliable; - * rather we need to use a text mark and set that text mark to the insert position. - * Actually, there is always one predefined text mark called "insert", which corresponds - * to the text cursor. Thus it suffices to navigate to text end, insert and scroll into view. - */ - void - addEntry (string const& text) - { - auto buff = textLog_.get_buffer(); - auto cursor = buff->get_insert(); - buff->move_mark (cursor, buff->end()); - buff->insert (buff->end(), text); - textLog_.scroll_to (cursor); - } - }; - - -}}// namespace gui::widget -#endif /*GUI_WIDGET_ERROR_LOG_WIDGET_H*/ diff --git a/src/gui/workspace/dock-area.cpp b/src/gui/workspace/dock-area.cpp index 54df374aa..e4e5abaff 100644 --- a/src/gui/workspace/dock-area.cpp +++ b/src/gui/workspace/dock-area.cpp @@ -42,6 +42,7 @@ #include "gui/panel/timeline-panel-obsolete.hpp" #include "include/logging.h" +#include "lib/util-foreach.hpp" using namespace boost; ////////////////////////////////////////////////////////////////////////////////TICKET #1071 no wildcard includes please! using namespace std; ////////////////////////////////////////////////////////////////////////////////TICKET #1071 no wildcard includes please! @@ -143,7 +144,16 @@ namespace workspace { } - void + bool + DockArea::hasPanel (const int description_index) + { + return util::has_any (panels_, [=](panel::Panel* panel) + { + return getPanelType(panel) == description_index; + }); + } + + panel::Panel& DockArea::showPanel (const int description_index) { // Try and find the panel and present it if possible @@ -158,7 +168,8 @@ namespace workspace { Gdl::DockItem &dock_item = panel->getDockItem(); // ENSURE(dock_item); dock_item.present(dock_); - return; + ENSURE (panel); + return *panel; } } @@ -167,6 +178,9 @@ namespace workspace { // Dock the item dock_.add_item(new_panel->getDockItem(), Gdl::DOCK_FLOATING); + + ENSURE (new_panel); + return *new_panel; } @@ -256,7 +270,7 @@ namespace workspace { int - DockArea::findPanelDescription (const char* class_name) const + DockArea::findPanelDescription (const char* class_name) { REQUIRE(class_name); diff --git a/src/gui/workspace/dock-area.hpp b/src/gui/workspace/dock-area.hpp index eb4b50f84..683d4cf91 100644 --- a/src/gui/workspace/dock-area.hpp +++ b/src/gui/workspace/dock-area.hpp @@ -28,6 +28,10 @@ ** @todo 2017 need to clarify the intended behaviour of panels ** ///////////////////////////////////////////////////////////////////////////TICKET #1097 clarify the role and behaviour of Panels ** @todo will be transformed into a Dock entity as of 6/2018 /////////////////////////////////////////////TICKET #1144 refactor dock handling + ** @todo as of 8/2018 this is a copy of the (still actively used) PanelManager; + ** the intention was to do a clean rewrite and then throw away + ** the old messy PanelManager implementation. Meanwhile I was forced to add + ** yet more cruft to PanelManager, just to keep it going. Beware! ** ** @see actions.hpp */ @@ -117,8 +121,12 @@ namespace workspace { /** * Shows a panel given a description index. * @param description_index The index of the panel type to show. + * @return existing or new Panel, docked within the realm of this PanelManager. */ - void showPanel (const int description_index); + panel::Panel& showPanel (const int description_index); + + /** was the indicated panel already allocated within this PanelManager's realm? */ + bool hasPanel (const int description_index); /** * Switches a panel from one type to another, @@ -138,6 +146,14 @@ namespace workspace { public: + /** + * retrieve the internal type-ID corresponding to the given panel implementation type. + * @return internal index into the #panelDescriptionList, or `-1` if not found. + * @deprecated the whole concept of panel identification needs overhaul 8/2018 + */ + template + static int findPanelID(); + /** Gets the number of panel descriptions. */ static int getPanelDescriptionCount(); @@ -166,7 +182,7 @@ namespace workspace { * @return Returns the index of the panel description found, or -1 * if no description was found for this type. */ - int findPanelDescription (const char* class_name) const; + static int findPanelDescription (const char* class_name); /** * Creates a panel by description index. @@ -338,5 +354,16 @@ namespace workspace { }; + + + template + inline int + DockArea::findPanelID() + { + return DockArea::findPanelDescription (typeid(P).name()); + } + + + }}// namespace gui::workspace #endif /*GUI_WORKSPACE_DOCK_AREA_H*/ diff --git a/src/gui/workspace/panel-manager.cpp b/src/gui/workspace/panel-manager.cpp index 9df74186c..87194261c 100644 --- a/src/gui/workspace/panel-manager.cpp +++ b/src/gui/workspace/panel-manager.cpp @@ -37,6 +37,7 @@ #include "gui/panel/timeline-panel-obsolete.hpp" #include "include/logging.h" +#include "lib/util-foreach.hpp" using namespace boost; ////////////////////////////////////////////////////////////////////////////////TICKET #1071 no wildcard includes please! using namespace std; ////////////////////////////////////////////////////////////////////////////////TICKET #1071 no wildcard includes please! @@ -136,7 +137,16 @@ namespace workspace { } - void + bool + PanelManager::hasPanel (const int description_index) + { + return util::has_any (panels_, [=](panel::Panel* panel) + { + return getPanelType(panel) == description_index; + }); + } + + panel::Panel& PanelManager::showPanel (const int description_index) { // Try and find the panel and present it if possible @@ -149,9 +159,9 @@ namespace workspace { if (!panel->is_shown()) panel->show(); Gdl::DockItem &dock_item = panel->getDockItem(); - // ENSURE(dock_item); dock_item.present(dock_); - return; + ENSURE (panel); + return *panel; } } @@ -160,6 +170,9 @@ namespace workspace { // Dock the item dock_.add_item(new_panel->getDockItem(), Gdl::DOCK_FLOATING); + + ENSURE (new_panel); + return *new_panel; } @@ -249,7 +262,7 @@ namespace workspace { int - PanelManager::findPanelDescription (const char* class_name) const + PanelManager::findPanelDescription (const char* class_name) { REQUIRE(class_name); diff --git a/src/gui/workspace/panel-manager.hpp b/src/gui/workspace/panel-manager.hpp index 3608618a5..1c136edd0 100644 --- a/src/gui/workspace/panel-manager.hpp +++ b/src/gui/workspace/panel-manager.hpp @@ -114,8 +114,12 @@ namespace workspace { /** * Shows a panel given a description index. * @param description_index The index of the panel type to show. + * @return existing or new Panel, docked within the realm of this PanelManager. */ - void showPanel (const int description_index); + panel::Panel& showPanel (const int description_index); + + /** was the indicated panel already allocated within this PanelManager's realm? */ + bool hasPanel (const int description_index); /** * Switches a panel from one type to another, @@ -135,6 +139,14 @@ namespace workspace { public: + /** + * retrieve the internal type-ID corresponding to the given panel implementation type. + * @return internal index into the #panelDescriptionList, or `-1` if not found. + * @deprecated the whole concept of panel identification needs overhaul 8/2018 + */ + template + static int findPanelID(); + /** Gets the number of panel descriptions. */ static int getPanelDescriptionCount(); @@ -163,7 +175,7 @@ namespace workspace { * @return Returns the index of the panel description found, or -1 * if no description was found for this type. */ - int findPanelDescription (const char* class_name) const; + static int findPanelDescription (const char* class_name); /** * Creates a panel by description index. @@ -334,5 +346,16 @@ namespace workspace { }; + + + template + inline int + PanelManager::findPanelID() + { + return PanelManager::findPanelDescription (typeid(P).name()); + } + + + }}// namespace gui::workspace #endif /*GUI_WORKSPACE_PANEL_MANAGER_H*/ diff --git a/src/gui/workspace/ui-style.cpp b/src/gui/workspace/ui-style.cpp index 2315866b1..065c2215b 100644 --- a/src/gui/workspace/ui-style.cpp +++ b/src/gui/workspace/ui-style.cpp @@ -96,7 +96,7 @@ namespace workspace { } Gtk::StyleContext::add_provider_for_screen (screen, css_provider, - GTK_STYLE_PROVIDER_PRIORITY_USER); + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } diff --git a/src/include/display-facade.h b/src/include/display-facade.h index 270dc6172..82e9e846a 100644 --- a/src/include/display-facade.h +++ b/src/include/display-facade.h @@ -33,7 +33,7 @@ ** by a better design, where rather the \em provider of an output facility ** registers with the OutputManager in the core. ** - ** @see [corresponding implementation](display-service.hpp) + ** @see [corresponding implementation](\ref display-service.hpp) ** @see gui::GuiFacade ** @see dummy-player-facade.h ** diff --git a/src/include/gui-notification-facade.h b/src/include/gui-notification-facade.h index f85f00fc1..bb4970cfe 100644 --- a/src/include/gui-notification-facade.h +++ b/src/include/gui-notification-facade.h @@ -44,15 +44,20 @@ #ifdef __cplusplus /* ============== C++ Interface ================= */ #include "lib/depend.hpp" -#include "lib/diff/mutation-message.hpp" #include "lib/idi/entry-id.hpp" #include +namespace lib { + namespace diff { + class GenNode; + class MutationMessage; +} } namespace gui { using std::string; + using lib::diff::GenNode; using lib::diff::MutationMessage; using ID = lib::idi::BareEntryID const&; @@ -69,7 +74,7 @@ namespace gui { * from the lower layers into the Lumiera GUI. Typically, this happens * asynchronously and triggered by events within the lower layers. * - * This is a layer separation facade interface. Clients should use + * This is a layer separation façade interface. Clients should use * the embedded #facade factory, which yields a proxy routing any * calls through the lumieraorg_GuiNotification interface * @throws lumiera::error::State when interface is not opened @@ -93,6 +98,9 @@ namespace gui { /** attach an warning or state information element */ virtual void markNote (ID uiElement, string const& text) =0; + /** send a generic _state mark_ message to some element */ + virtual void mark (ID uiElement, GenNode&& stateMark) =0; + /** push a diff message up into the user interface. * @remark this is the intended way how to populate or * manipulate the contents of the user interface from @@ -129,9 +137,10 @@ extern "C" { LUMIERA_INTERFACE_DECLARE (lumieraorg_GuiNotification, 0, LUMIERA_INTERFACE_SLOT (void, displayInfo, (uint, const char*)), - LUMIERA_INTERFACE_SLOT (void, markError, (LumieraUid, const char*)), - LUMIERA_INTERFACE_SLOT (void, markNote, (LumieraUid, const char*)), - LUMIERA_INTERFACE_SLOT (void, mutate, (LumieraUid, void*)), + LUMIERA_INTERFACE_SLOT (void, markError, (const void*, const char*)), ////////////TICKET #1175 : need a way to pass EntryID + LUMIERA_INTERFACE_SLOT (void, markNote, (const void*, const char*)), + LUMIERA_INTERFACE_SLOT (void, mark, (const void*, void*)), + LUMIERA_INTERFACE_SLOT (void, mutate, (const void*, void*)), LUMIERA_INTERFACE_SLOT (void, triggerGuiShutdown, (const char*)), ); diff --git a/src/include/session-command-facade.h b/src/include/session-command-facade.h index 5c1205e8d..ce49d9f28 100644 --- a/src/include/session-command-facade.h +++ b/src/include/session-command-facade.h @@ -75,7 +75,7 @@ namespace control { * * The service exposed through this facade offers dedicated support for * the _standard command cycle_, as is typically performed from the UI. - * Such a usage cycle starts with ["opening"](#cycle) a local anonymous + * Such a usage cycle starts with ["opening"](\ref #cycle) a local anonymous * clone copy from the global command definition, which is then used * in further calls to be outfitted with actual arguments and finally * to be handed over to the dispatcher for execution. @@ -87,7 +87,7 @@ namespace control { * use the embedded #facade factory, which yields a proxy to route any * calls through the lumieraorg_SessionCommand interface * @throws lumiera::error::State when interface is not opened - * @see [Command system](command.hpp) + * @see [Command system](\ref command.hpp) * @see SessionCommandFunction_test */ class SessionCommand diff --git a/src/include/ui-protocol.hpp b/src/include/ui-protocol.hpp new file mode 100644 index 000000000..eb6b7c3b8 --- /dev/null +++ b/src/include/ui-protocol.hpp @@ -0,0 +1,100 @@ +/* + UI-PROTOCOL.h - magic keys used for communication with the Lumiera UI + + Copyright (C) Lumiera.org + 2018, 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-protocol.hpp + ** Hard wired key constants and basic definitions for communication with the GUI. + ** The Lumiera UI is connected to the application core via message oriented interfaces, + ** running an asynchronous communication protocol. + ** - the [UI-Bus](\ref ui-bus.hpp) allows for some specific kinds of messages to be + ** sent either "upstream" (towards the CoreService) or "downstream" to individual + ** [UI-Elements](\ref gui::model::Tangible) known by ID. + ** - these ["tangible interface elements"](\ref tangible.hpp) themselves define a + ** basic set of actions and "state mark" messages, known as the "element protocol" + ** - and changes to the structure of tangible elements exposed through the UI are + ** conducted by pushing up [»diff messages«](\ref mutation-message.hpp) into the + ** UI via UI-Bus. These messages need to comply to an underlying structural + ** model of elements to be edited within the session and exposed for GUI + ** manipulation. This is referred to as "Session Model Scheme" + ** + ** These protocols are basically _implementation defined_ conventions and not + ** rigorously formalised. To a large extent, they are guided by the types and + ** corresponding API interfaces. However, since the lumiera session allows for + ** quite flexible structures, these protocols are kept open for local extensions. + ** At various levels, magic ID-Values are used to initiate specific interactions. + ** This header supplies the hard wired key constants and IDs thereby employed. + ** + ** @see common/ui-protocol.cpp (translation unit backing those constants) + */ + + +#ifndef GUI_INTERFACE_UI_PROTOCOL_H +#define GUI_INTERFACE_UI_PROTOCOL_H + + +#include "lib/error.hpp" +#include "lib/symbol.hpp" + + +namespace lib { +namespace diff{ + LUMIERA_ERROR_DECLARE(DIFF_STRUCTURE); ///< Invalid diff structure: implicit rules and assumptions violated. +}} +namespace Glib { +//class ustring; /////////////////////////////TODO needed? +} +namespace gui { + +//using lib::Literal; + using lib::Symbol; +//using cuString = const Glib::ustring; + using lib::diff::LERR_(DIFF_STRUCTURE); + + + extern const Symbol META_kind; + + extern const Symbol TYPE_Fork; + extern const Symbol TYPE_Clip; + extern const Symbol TYPE_Marker; + extern const Symbol TYPE_Channel; + extern const Symbol TYPE_Effect; + + extern const Symbol ATTR_name; + extern const Symbol ATTR_fork; + extern const Symbol ATTR_assets; + + + /* ======== UI-Element protocol ======== */ + + extern const Symbol MARK_reset; + extern const Symbol MARK_clearErr; + extern const Symbol MARK_clearMsg; + extern const Symbol MARK_expand; + extern const Symbol MARK_reveal; + extern const Symbol MARK_Flash; + extern const Symbol MARK_Error; + extern const Symbol MARK_Warning; + extern const Symbol MARK_Message; + + +}// namespace gui +#endif /*GUI_INTERFACE_UI_PROTOCOL_H*/ diff --git a/src/lib/depend.hpp b/src/lib/depend.hpp index 3dff59462..2a3e1c910 100644 --- a/src/lib/depend.hpp +++ b/src/lib/depend.hpp @@ -141,7 +141,7 @@ namespace lib { Deleter deleter_; public: - ZombieCheck zombieCheck; + ZombieCheck zombieCheck{util::typeStr()}; DependencyFactory() = default; ~DependencyFactory() diff --git a/src/lib/diff/diff-language.hpp b/src/lib/diff/diff-language.hpp index f4683c2a5..511108389 100644 --- a/src/lib/diff/diff-language.hpp +++ b/src/lib/diff/diff-language.hpp @@ -33,7 +33,8 @@ ** Such an architecture allows for tight cooperation between strictly separated ** components, without the need of a fixed, predefined and shared data structure. ** - ** \par Basic Assumptions + ** # Basic Assumptions + ** ** While the \em linearisation folds knowledge about the underlying data structure ** down into the actual diff, we deliberately assume that the data to be diffed is ** \em structured data. Moreover, we'll assume implicitly that this data is \em typed, @@ -44,7 +45,8 @@ ** send the actual content data this way, or to serve as redundancy to verify ** proper application of the changes at the diff receiver downstream. ** - ** \par Solution Pattern + ** # Solution Pattern + ** ** The representation of this linearised diff language relies on a specialised form ** of the visitor pattern: We assume the vocabulary of the diff language to be ** relatively fixed, while the actual effect when consuming the stream of diff tokens @@ -98,7 +100,8 @@ namespace diff{ namespace error = lumiera::error; - LUMIERA_ERROR_DECLARE(DIFF_CONFLICT); ///< Collision in diff application: contents of target not as expected. + LUMIERA_ERROR_DECLARE(DIFF_STRUCTURE); ///< Invalid diff structure: implicit rules and assumptions violated. + LUMIERA_ERROR_DECLARE(DIFF_CONFLICT); ///< Collision in diff application: contents of target not as expected. template diff --git a/src/lib/diff/diff.cpp b/src/lib/diff/diff.cpp index 95de683a0..54d5224df 100644 --- a/src/lib/diff/diff.cpp +++ b/src/lib/diff/diff.cpp @@ -39,6 +39,7 @@ namespace lib { namespace diff{ + LUMIERA_ERROR_DEFINE(DIFF_STRUCTURE, "Invalid diff structure: implicit rules and assumptions violated."); LUMIERA_ERROR_DEFINE(DIFF_CONFLICT, "Collision in diff application: contents of target not as expected."); diff --git a/src/lib/diff/gen-node.hpp b/src/lib/diff/gen-node.hpp index 6b77c0f3f..1e8a122c7 100644 --- a/src/lib/diff/gen-node.hpp +++ b/src/lib/diff/gen-node.hpp @@ -52,6 +52,16 @@ ** addressable by-name and an (ordered) collection of elements treated ** as children within the scope of the given record. ** + ** ## The GenNode ID + ** + ** Each GenNode holds an ID tag, allowing to establish _identical_ and _distinct_ + ** elements within a scope. This ID is based on lib::idi::BareEntryID, thereby + ** providing a human readable symbolic part, and a hash value. By default, these + ** GenNode IDs are fabricated such as to hold a non-reproducible, random hash + ** value -- however, there are construction flavours allowing to pass in an + ** pre-existing distinct Entry-ID. + ** + ** ** # Requirements ** ** GenNode elements are to be used in the diff detection and implementation. @@ -66,7 +76,7 @@ ** typed context, either based on some kind of embedded type tag, or ** alternatively by visitation and matching ** - finally, the handling of changes prompts us to support installation - ** of a specifically typed change handling closure. + ** of a specifically typed _change handling closure_. ** ** ## monadic nature? ** @@ -223,7 +233,12 @@ namespace diff{ idi::getTypeHash()) { } + ID (idi::BareEntryID&& rawD) + : idi::BareEntryID{move (rawD)} + { } + public: + explicit ID (GenNode const& node) : ID(node.idi) { } @@ -432,7 +447,15 @@ namespace diff{ } }; - + /** fabricate a GenNode with the literally given ID */ + template + static GenNode + asAttribute (idi::BareEntryID && rawID, X&& payload) + { + return GenNode{ID{move (rawID)}, DataCap{forward (payload)}}; + } + + protected: /** @internal for dedicated builder subclasses */ GenNode (ID&& id, DataCap&& d) @@ -771,14 +794,21 @@ namespace diff{ inline GenNode MakeRec::genNode() { - return GenNode(std::move(record_)); + return GenNode{std::move(record_)}; } template<> inline GenNode - MakeRec::genNode(string const& symbolicID) + MakeRec::genNode (idi::BareEntryID rawID) { - return GenNode(symbolicID, std::move(record_)); + return GenNode::asAttribute (std::move(rawID), std::move(record_)); + } + + template<> + inline GenNode + MakeRec::genNode (string const& symbolicID) + { + return GenNode{symbolicID, std::move(record_)}; } @@ -844,7 +874,7 @@ namespace diff{ inline GenNode Rec::buildAttribute (string const& key, X&& payload) { - return GenNode(key, std::forward(payload)); + return GenNode{key, forward(payload)}; } diff --git a/src/lib/diff/list-diff.hpp b/src/lib/diff/list-diff.hpp index 45e151e1b..3b90d61cc 100644 --- a/src/lib/diff/list-diff.hpp +++ b/src/lib/diff/list-diff.hpp @@ -23,7 +23,7 @@ /** @file list-diff.hpp ** A token language to represent changes in a list of elements. - ** In combination with the [DiffLanguage framework](diff-langue.hpp), this building + ** In combination with the [DiffLanguage framework](\ref diff-langue.hpp), this building ** block defines the set of operations to express changes in a given list of elements. ** By implementing the lib::diff::ListDiffInterpreter interface (visitor), a concrete ** usage can receive such a diff description and e.g. apply it to a target data structure. diff --git a/src/lib/diff/mutation-message.hpp b/src/lib/diff/mutation-message.hpp index 416bec91b..ff146cdac 100644 --- a/src/lib/diff/mutation-message.hpp +++ b/src/lib/diff/mutation-message.hpp @@ -256,7 +256,7 @@ namespace diff{ * becomes the new generator and the old generator object is released, * since the assignment of the new backend typically removes the last * reference in the smart-ptr managing the generation backend. This - * process can be repeated and then the [diagnostics](operator string()) + * process can be repeated and then the [diagnostics](\ref operator string()) * will show the remainder of the sequence _left at that point._ */ inline MutationMessage& diff --git a/src/lib/diff/record.hpp b/src/lib/diff/record.hpp index 0e2f8ce03..f00ae35a8 100644 --- a/src/lib/diff/record.hpp +++ b/src/lib/diff/record.hpp @@ -25,17 +25,22 @@ ** Special collection to represent object-like data. ** To be used in a context where introspection, open, extensible definitions ** and loose coupling of data representation matters. Typically, structures - ** defined in terms of Record elements are linked to the actual \em core + ** defined in terms of Record elements are linked to the _actual core_ ** representation of the same entities relying on [diff messages](\ref diff-language.hpp). ** Record is one of the supported flavours within the DataCap of GenNode elements, ** which in turn serve as the standard handle to refer to other elements, entities, ** attributes or references within the "backbone" of the Lumiera GUI. ** + ** A Record holds + ** - (optionally) a type-ID string + ** - a collection of _named attributes_ (key-value data) + ** - a list of _enclosed children_ (contents within the "scope" of this Record) + ** ** ## design decisions ** The Record type is shaped from its intended use: It serves to symbolically represent - ** _objects_ in the "external tree description". Here, "objects" means objects _for real_, - ** i.e. with types, fields and an enclosed scope. But \em external means that we do not work - ** on these objects right here, we only represent them, for later referral, _symbolically_. + ** *objects* in the "external tree description". Here, "objects" means objects _for real_, + ** i.e. with types, fields and an enclosed scope. Yet the term *external* means that we do not + ** work on these objects right here, we only represent them, for later referral, _symbolically_. ** ** \par rationale ** The underlying theme and motivation of this design is negative: we do not want @@ -107,7 +112,9 @@ namespace lib { template class PlantingHandle; - +namespace idi { + class BareEntryID; +} namespace diff{ namespace error = lumiera::error; @@ -557,6 +564,7 @@ namespace diff{ */ VAL genNode(); + VAL genNode(idi::BareEntryID rawID); VAL genNode(string const& symbolicID); template diff --git a/src/lib/diff/tree-diff-application.hpp b/src/lib/diff/tree-diff-application.hpp index e5e275495..336df1888 100644 --- a/src/lib/diff/tree-diff-application.hpp +++ b/src/lib/diff/tree-diff-application.hpp @@ -158,7 +158,7 @@ namespace diff{ public: virtual ~ScopeManager(); ///< this is an interface - virtual TreeMutator::Handle openScope() =0; + virtual TreeMutator::Handle openScope() =0; ///< @remark picks up `treeMutatorSize(...)` virtual TreeMutator& closeScope() =0; virtual void clear() =0; diff --git a/src/lib/diff/tree-diff.cpp b/src/lib/diff/tree-diff.cpp index 301767708..8c61b297f 100644 --- a/src/lib/diff/tree-diff.cpp +++ b/src/lib/diff/tree-diff.cpp @@ -205,7 +205,7 @@ namespace diff{ void TreeDiffMutatorBinding::mut (GenNode const& n) { - TreeMutator::Handle buffHandle = scopeManger_->openScope(); + TreeMutator::Handle buffHandle = scopeManger_->openScope(); // hint: treeMutatorSize(...) if (not treeMutator_->mutateChild(n, buffHandle)) __failMismatch("enter nested scope", n); diff --git a/src/lib/diff/tree-mutator-attribute-binding.hpp b/src/lib/diff/tree-mutator-attribute-binding.hpp index 69221a1d2..398aa5963 100644 --- a/src/lib/diff/tree-mutator-attribute-binding.hpp +++ b/src/lib/diff/tree-mutator-attribute-binding.hpp @@ -359,6 +359,15 @@ namespace diff{ return chainedBuilder> (key, mutatorBuilderClosure); } + /** Entry point for DSL builder; variation to handle an attribute-object with given ID */ + template + template + inline auto + Builder::mutateAttrib (idi::BareEntryID const& rawID, CLO mutatorBuilderClosure) + { + return chainedBuilder> (rawID, mutatorBuilderClosure); + } + }//(END)Mutator-Builder decorator components... diff --git a/src/lib/diff/tree-mutator-gen-node-binding.hpp b/src/lib/diff/tree-mutator-gen-node-binding.hpp index 914c23aed..95f063edf 100644 --- a/src/lib/diff/tree-mutator-gen-node-binding.hpp +++ b/src/lib/diff/tree-mutator-gen-node-binding.hpp @@ -23,7 +23,7 @@ /** @file tree-mutator-gen-node-binding.hpp ** Special binding implementation for TreeMutator, allowing to map - ** tree diff operations onto an »External Tree Description«. Such is is a + ** tree diff operations onto an »External Tree Description«. This term denotes a ** DOM like representation of tree like structures, comprised of GenNode elements. ** TreeMutator is a customisable intermediary, which enables otherwise opaque ** implementation data structures to receive and respond to generic structural diff --git a/src/lib/diff/tree-mutator-noop-binding.hpp b/src/lib/diff/tree-mutator-noop-binding.hpp new file mode 100644 index 000000000..51762e2e7 --- /dev/null +++ b/src/lib/diff/tree-mutator-noop-binding.hpp @@ -0,0 +1,124 @@ +/* + TREE-MUTATOR-NOOP-BINDING.hpp - diff::TreeMutator implementation building block + + Copyright (C) Lumiera.org + 2018, 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 tree-mutator-noop-binding.hpp + ** Special binding implementation for TreeMutator, allowing to accept + ** and ignore any tree diff without tangible effect. TreeMutator is a + ** customisable intermediary, which enables otherwise opaque implementation + ** data structures to receive and respond to generic structural change messages + ** ("tree diff"). + ** + ** Each concrete TreeMutator instance will be configured differently, and this + ** adaptation is done by combining various building blocks. This header defines + ** a special `/dev/null` building block, which behaves as if successfully consuming + ** the given diff without actually doing anything. Obviously, such a "black hole layer" + ** need to be below any other diff binding, and may be used to absorb any diff verbs + ** not matched and consumed by a more specific binding. Contrast this to the TreeMutator + ** default implementation, which likewise absorbs diff verbs, but in a way to trigger + ** a lumiera::error::LUMIERA_ERROR_DIFF_CONFLICT. + ** + ** @note the header tree-mutator-attribute-binding.hpp was split off for sake of readability + ** and is included automatically from bottom of tree-mutator.hpp + ** + ** @see tree-mutator-test.cpp + ** @see TreeMutator::build() + ** + */ + + +#ifndef LIB_DIFF_TREE_MUTATOR_NOOP_BINDING_H +#define LIB_DIFF_TREE_MUTATOR_NOOP_BINDING_H + + +#include "lib/error.hpp" +#include "lib/diff/gen-node.hpp" +#include "lib/diff/tree-mutator.hpp" + +#include + +namespace lib { +namespace diff{ + + namespace { // Mutator-Builder decorator components... + + + /** + * Diff binding for black holes, happily accepting anything. + * @note absorbs and silently ignores any diff verb. + */ + class BlackHoleMutation + : public TreeMutator + { + + public: + BlackHoleMutation () = default; + + + /* ==== TreeMutator API ==== */ + + using Elm = GenNode const&; + using Buff = TreeMutator::Handle; + + bool hasSrc() override { return true; } ///< always keen to do yet more + + bool injectNew (Elm) override { return true; } ///< pretend to inject something new + bool matchSrc (Elm) override { return true; } ///< purport suitable element is waiting + bool acceptSrc (Elm) override { return true; } ///< claim to handle any diff task + bool accept_until (Elm) override { return true; } ///< profess to forward anywhere + bool findSrc (Elm) override { return true; } ///< sham to find anything + bool assignElm (Elm) override { return true; } ///< accept any assignment + + bool + mutateChild (Elm, Buff buff) override ///< ignore inferiors, yet reproduce yourself + { + buff.create (BlackHoleMutation()); + return true; + } + }; + + + + /** Entry point for DSL builder: create a binding which consumes any diff without effect. + * @warning must be used as bottom most layer in a custom TreeMutator, since it would + * otherwise shadow and disable any binding layer below. + * @note however it is possible to add a (typically selective) binding layer on top; + * any diff verb _not handled_ by such a top layer will fall through and + * be silently ignored. Such a setup can be used to "fish" for some specific + * attributes within a diff stream. + */ + template + inline auto + Builder::ignoreAllChanges() + { + static_assert (std::is_same() + ,"ignoreAllChanges() must be used as bottom layer."); + + return Builder (BlackHoleMutation{}); + } + + + + }//(END)Mutator-Builder decorator components... + +}} // namespace lib::diff +#endif /*LIB_DIFF_TREE_MUTATOR_NOOP_BINDING_H*/ diff --git a/src/lib/diff/tree-mutator.hpp b/src/lib/diff/tree-mutator.hpp index 4a5c0df61..17ce59b5d 100644 --- a/src/lib/diff/tree-mutator.hpp +++ b/src/lib/diff/tree-mutator.hpp @@ -83,6 +83,7 @@ ** ** @see tree-mutator-test.cpp ** @see tree-mutator-binding-test.cpp + ** @see [usage for tree diff application](\ref tree-diff-application.hpp) ** @see diff-language.hpp ** @see DiffDetector ** @@ -156,7 +157,7 @@ namespace diff{ /** initialisation immediately before start of diff application * @remark allows for setup of state which is dependent on memory location, - * like e.g. iterators. Due to the invokation via Builder DSL, the + * like e.g. iterators. Due to the invocation via Builder DSL, the * implementation object may be moved after construction, but prior * to invoking this hook */ @@ -373,7 +374,12 @@ namespace diff{ template auto mutateAttrib (Symbol attributeID, CLO mutatorBuilderClosure); - ///////////////////////////////////////TODO define variant taking a GenNode::ID ?? + /** + * @param rawID the explicitly given ID of an attribute object, + * used literally to match the attribute in question + */ + template + auto mutateAttrib (idi::BareEntryID const& rawID, CLO mutatorBuilderClosure); /** set up a binding to a structure of "child objects", @@ -439,6 +445,9 @@ namespace diff{ */ auto attachDummy (TestMutationTarget& dummy); + + /** set up a catch-all and ignore-everything layer */ + auto ignoreAllChanges(); }; }//(END) Mutator-Builder... @@ -460,4 +469,5 @@ namespace diff{ #include "lib/diff/tree-mutator-gen-node-binding.hpp" #include "lib/diff/tree-mutator-attribute-binding.hpp" #include "lib/diff/tree-mutator-collection-binding.hpp" +#include "lib/diff/tree-mutator-noop-binding.hpp" diff --git a/src/lib/error-exception.cpp b/src/lib/error-exception.cpp index a1e4c64ce..67b54a001 100644 --- a/src/lib/error-exception.cpp +++ b/src/lib/error-exception.cpp @@ -116,7 +116,8 @@ namespace lumiera { , desc_{description} , cause_{extractCauseMsg(cause)} { - lumiera_error_set (this->id_, description.c_str()); + string detailInfo{description + (isnil(cause_)? "" : " | cause = "+cause_)}; + lumiera_error_set (this->id_, detailInfo.c_str()); } diff --git a/src/lib/format-cout.hpp b/src/lib/format-cout.hpp index 10840e1a1..3b7b65683 100644 --- a/src/lib/format-cout.hpp +++ b/src/lib/format-cout.hpp @@ -45,7 +45,7 @@ ** @see FormatCOUT_test ** @see FormatHelper_test ** @see [generic string conversion helper](\ref util::toString() ) - ** @see [frontend for boost::format, printf-style](format-string.hpp) + ** @see [frontend for boost::format, printf-style](\ref format-string.hpp) ** */ diff --git a/src/lib/format-obj.cpp b/src/lib/format-obj.cpp index f2cdc0216..b7eb0d26a 100644 --- a/src/lib/format-obj.cpp +++ b/src/lib/format-obj.cpp @@ -31,7 +31,7 @@ ** investigate object contents and show types and addresses. They ** are referred from our [lightweight string converter](\ref ** util::StringConv), but also from the util::toString() function - ** and more common [formatting utils](format-util.hpp). + ** and more common [formatting utils](\ref format-util.hpp). ** ** @see FormatHelper_test ** @see FormatString_test @@ -205,7 +205,7 @@ apologies for that." "|lumiera::" , regex::ECMAScript | regex::optimize}; - static regex stdString {"(__cxx11::)?basic_string, allocator\\s*>\\s*" + static regex stdString {"(__cxx11::)?basic_string, allocator\\s*>(\\s+\\B)?" , regex::ECMAScript | regex::optimize}; static regex stdAllocator {"(\\w+<(" TYP_EXP ")), allocator<\\2>\\s*" diff --git a/src/lib/format-obj.hpp b/src/lib/format-obj.hpp index 562269e11..a4f7c8a87 100644 --- a/src/lib/format-obj.hpp +++ b/src/lib/format-obj.hpp @@ -38,7 +38,7 @@ ** the way we did it for `boost::format`? This would reduce inclusion cost... ** ** @see FormatHelper_test - ** @see [frontend for boost::format, printf-style](format-string.hpp) + ** @see [frontend for boost::format, printf-style](\ref format-string.hpp) ** */ diff --git a/src/lib/format-util.hpp b/src/lib/format-util.hpp index eebf22638..3d926e9f5 100644 --- a/src/lib/format-util.hpp +++ b/src/lib/format-util.hpp @@ -30,7 +30,7 @@ ** through our [generic string conversion](\ref util::toString) ** ** @see FormatHelper_test - ** @see [frontend for boost::format, printf-style](format-string.hpp) + ** @see [frontend for boost::format, printf-style](\ref format-string.hpp) ** */ diff --git a/src/lib/idi/entry-id.hpp b/src/lib/idi/entry-id.hpp index 8a860a9c5..a32cf0978 100644 --- a/src/lib/idi/entry-id.hpp +++ b/src/lib/idi/entry-id.hpp @@ -32,6 +32,13 @@ ** @note as of 3/2010 this is an experimental setup and exists somewhat in parallel ** to the assets. We're still in the process of finding out what's really required ** to keep track of all the various kinds of objects. ///////////////////TICKET #739 + ** @todo as of 11/2018 the basic design seems adequate, but the actual solution looks fishy. + ** Even more so, since we now use subclasses of BareEntryID + ** - as identity tag within lib::diff::GenNode + ** - as identity tag for all [tangible UI elements](\ref gui::model::Tangible) + ** There are various quirks and hacks to make all of this happen, and especially + ** the hashed-in type information feels gratuitous at places, when all we actually + ** need is a distinct identity plus a human readable symbol. ** ** @see asset::Asset::Ident ** @see entry-id-test.cpp @@ -101,7 +108,7 @@ namespace idi { * which would require both to yield the same hash values.... * @warning there is a weakness in boost::hash for strings of running numbers, * causing collisions already for a small set with less than 100000 entries. - * To ameliorate the problem, we hash in the trailing digits, and + * To mitigate the problem, we hash in the trailing digits, and * spread them by the #KNUTH_MAGIC /////////TICKET #865 * @warning this code isn't portable and breaks if sizeof(size_t) < sizeof(void*) * @see HashGenerator_test#verify_Knuth_workaround @@ -146,11 +153,18 @@ namespace idi { * encoded into a hash seed. Thus even the same symbolicID * generates differing hash-IDs for different type parameters */ - BareEntryID (string const& symbolID, HashVal seed =0) + BareEntryID (string const& symbolID, HashVal seed) : symbol_(symbolID) , hash_(buildHash (symbol_, seed)) { } + /** store the symbol but use a random hash part */ + explicit + BareEntryID (string const& symbolID) + : symbol_(symbolID) + , hash_{} // random + { } + public: /* default copyable and assignable */ @@ -265,7 +279,7 @@ namespace idi { return static_cast (bID); } - + explicit operator string() const; friend bool operator< (EntryID const& i1, EntryID const& i2) { return i1.getSym() < i2.getSym(); } @@ -281,6 +295,26 @@ namespace idi { + /** + * Entry-ID with a symbolic tag but just a plain random hash part. + * @remarks use this flavour when it is _not relevant_ to tag with + * some type information nor to reproduce the hash value. + */ + struct RandID + : BareEntryID + { + RandID (string const& symbolID) + : BareEntryID{util::sanitise (symbolID)} + { } + RandID (const char* symbolID) + : BareEntryID{util::sanitise (symbolID)} + { } + RandID (Symbol const& internalSymbol) + : BareEntryID{string{internalSymbol}} + { } + }; + + /** try to upcast this BareEntryID to a fully typed EntryID. * Effectively, this is the attempt to reverse a type erasure; * thus the caller needs to know the type information (as provided diff --git a/src/lib/iter-adapter-ptr-deref.hpp b/src/lib/iter-adapter-ptr-deref.hpp index ca4a5b958..b038cadc0 100644 --- a/src/lib/iter-adapter-ptr-deref.hpp +++ b/src/lib/iter-adapter-ptr-deref.hpp @@ -141,7 +141,11 @@ namespace lib { return *this; } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } @@ -266,7 +270,11 @@ namespace lib { takeAddress(); } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } diff --git a/src/lib/iter-adapter-stl.hpp b/src/lib/iter-adapter-stl.hpp index e412973e8..e41e29008 100644 --- a/src/lib/iter-adapter-stl.hpp +++ b/src/lib/iter-adapter-stl.hpp @@ -72,11 +72,15 @@ namespace iter_stl { DistinctIter() : i_(), prev_() { } DistinctIter(IT const& i) : i_(i),prev_() { memorise(); } - pointer operator->() const { return i_; } - reference operator*() const { return *i_;} - bool isValid() const { return i_; } + pointer operator->() const { return i_; } + reference operator*() const { return *i_; } + bool isValid() const { return i_; } - operator bool() const { return i_; } + explicit + operator bool() const + { + return bool{i_}; + } DistinctIter& @@ -472,8 +476,17 @@ namespace iter_stl { IterSnapshot& operator= (IterSnapshot const&) = default; IterSnapshot& operator= (IterSnapshot &&) = default; - operator bool() const { return isValid(); } - size_t size() const { return buffer_.size(); } + explicit + operator bool() const + { + return isValid(); + } + + size_t + size() const + { + return buffer_.size(); + } diff --git a/src/lib/iter-adapter.hpp b/src/lib/iter-adapter.hpp index 8834b166c..1364c36d5 100644 --- a/src/lib/iter-adapter.hpp +++ b/src/lib/iter-adapter.hpp @@ -194,7 +194,11 @@ namespace lib { , pos_() { } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } /* === lumiera forward iterator concept === */ @@ -352,7 +356,11 @@ namespace lib { : core_() { } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } /* === lumiera forward iterator concept === */ @@ -491,7 +499,11 @@ namespace lib { , e_(oIter.getEnd()) { } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } /* === lumiera forward iterator concept === */ @@ -599,7 +611,11 @@ namespace lib { // standard copy operations acceptable - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } @@ -732,7 +748,11 @@ namespace lib { : i_(srcIter) { } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } diff --git a/src/lib/iter-chain-search.hpp b/src/lib/iter-chain-search.hpp new file mode 100644 index 000000000..084a24851 --- /dev/null +++ b/src/lib/iter-chain-search.hpp @@ -0,0 +1,320 @@ +/* + ITER-CHAIN-SEARCH.hpp - chained search with backtracking based on (bidirectional) iterator + + Copyright (C) Lumiera.org + 2018, 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 iter-chain-search.hpp + ** Evaluation mechanism to apply a sequence of conditions onto a linear search. + ** This search algorithm is implemented on top of a tree expanding (monadic) filter pipeline, + ** to allow for backtracking. The intention is not just to combine the individual conditions, but + ** rather to apply them one by one. After finding a match for the first condition, we'll search + ** for the next condition _starting at the position of the previous match_. In the most general + ** case, this immediate progression down the search chain might be too greedy; it could be that + ** we don't find a match for the next condition, but if we backtrack and first search further + ** on the previous condition, continuing with the search from that further position might + ** then lead to a match. Basically we have to try all combinations of all possible local + ** matches, to find a solution to satisfy the whole chain of conditions. + ** + ** ## Design + ** + ** The IterChainSearch component is built as a processing pipeline, based on the + ** [Tree-Explorer framework](\ref iter-tree-explorer.hpp). This yields several benefits, but + ** imposes some drawbacks. Without much effort, we get an extremely flexible and configurable + ** solution, with acceptable / moderate performance. The result automatically adapts to a huge + ** selection of data sources; it is possible (and is even intended) to attach it on top of an + ** existing on-demand data processing pipeline. The "source" can itself be a "state core" and + ** we may install suitable filter predicates to possibly collaborate with the internals of + ** such a state core. Yet we may also confine ourselves to pure functions and value processing. + ** + ** The obvious downside of such an approach is its complexity in terms of code to understand. + ** The collaboration between several layers in a filter pipeline can be intricate, leading to + ** chains of layered similar functions calling each other. Most of these abstractions and + ** decorations will be resolved by the compiler and optimiser, typically leading to long + ** compilation times and rather large size of the generated code (which in debugging mode + ** with full type information can even become excessively large). + ** + ** Some caveats + ** - the resulting pipeline is copyable, and it is typically moved out from a builder + ** function into the target location. So beware of _lambda captures by reference._ + ** When you capture _anything_ within the pipeline itself, like some `this` pointer, + ** you'll end up with a dangling reference for sure. However, it is just fine to + ** capture extended facilities within the scope _enclosing_ the pipeline, as long + ** as this scope remains intact during the whole usage time of the pipeline. + ** - the resulting entity, an instantiation of the IterChainSearch template, is + ** not a generic TreeExplorer (builder). Be careful when using any builder + ** functions still accessible. The only functions meant to be used in + ** builder style are the IterChainSearch::search() variations. + ** - if you copy, the embedded state is copied alongside, but not + ** any further external state referred by it. + ** + ** @see IterChainSearch_test + ** @see IterCursor_test + ** @see iter-tree-explorer.hpp + ** @see [usage example](\ref event-log.hpp) + ** + */ + + +#ifndef SRC_LIB_ITER_CHAIN_SEARCH_H +#define SRC_LIB_ITER_CHAIN_SEARCH_H + + +#include "lib/error.hpp" +#include "lib/iter-tree-explorer.hpp" +#include "lib/meta/util.hpp" + +#include +#include +#include + + +namespace lib { +namespace iter { + + using std::move; + using std::forward; + using std::string; + + using lib::meta::disable_if; + + + namespace { // type construction helpers... + + template + auto + buildSearchFilter (SRC&& dataSource) + { + return treeExplore (forward (dataSource)) + .mutableFilter(); + } + + template + auto + buildExplorer (SRC&& dataSource) + { + return buildSearchFilter (forward (dataSource)) + .expand ([](auto it){ return it; }); // child iterator starts as copy of current level iterator + } + + /** + * @internal helper to rebind on inferred types. + * @remark we use the TreeExplorer framework to assemble the processing pipeline + * from suitable building blocks configured with some lambdas. However, we + * also want to _inherit_ from this filter pipeline, so to expose the typical + * iterator operations without much ado. Thus we use some (static) helper function + * templates, instantiate them with the actual source data iterator and pick up + * the inferred type. + */ + template + struct _IterChainSetup + { + using Filter = decltype( buildSearchFilter(std::declval()).asIterator() ); + using Pipeline = decltype( buildExplorer (std::declval()) ); + + /** each step in the chain is a functor to reconfigure the underlying filter */ + using StepFunctor = std::function; + }; + + }//(End)type construction helpers + + + + + + /*********************************************************************************************************//** + * Iterator based linear search mechanism, with the ability to perform consecutive search with backtracking. + * The IterChainSearch can be configured with a sequence of search goals (filter conditions), and will apply + * these in succession on the underlying iterator. It will search _by linear search_ for the first hit of the + * first condition, and then continue to search _from there_ matching on the second condition, and so on. + * After the first combination of matches is exhausted, the search will backtrack and try to evaluate + * the next combination, leading to a tree of on-demand search solutions. + */ + template + class IterChainSearch + : public _IterChainSetup::Pipeline + { + using _Trait = _IterChainSetup; + using _Base = typename _Trait::Pipeline; + + using Value = typename _Base::value_type; + using Filter = typename _Trait::Filter; + using Step = typename _Trait::StepFunctor; + + /** Storage for a sequence of filter configuration functors */ + std::vector stepChain_; + + + bool + needsExpansion() const + { + return _Base::depth() < stepChain_.size(); + } + + public: + /** Build a chain-search mechanism based on the given source data sequence. + * @remark iterators will be copied or moved as appropriate, while from a STL compliant + * container just a pair of (`begin()`, `end()`) iterators is retrieved; the latter + * is also the reason why a rvalue reference to STL container is rejected, since the + * container needs to reside elsewhere; only the iterator is wrapped here. + */ + template + explicit + IterChainSearch (SEQ&& srcData) + : _Base{buildExplorer (forward (srcData))} + { // mark initial pristine state + _Base::disableFilter(); + } + + // inherited default ctor and standard copy operations + using _Base::_Base; + + + /* === adapted iteration control API === */ + void + iterNext() + { + _Base::__throw_if_empty(); + if (not needsExpansion()) + _Base::iterNext(); + while (needsExpansion() // Backtracking loop: attempt to establish all conditions + and _Base::checkPoint()) // possibly trying further combinations until success: + { + uint depth =_Base::depth(); + _Base::expandChildren(); // create copy of current filter embedded into child level + stepChain_[depth] (_Base::accessCurrentChildIter()); // invoke step functor to reconfigure this filter... + _Base::dropExhaustedChildren(); // which thereby might become empty + } + } + + IterChainSearch& + operator++() + { + iterNext(); + return *this; + } + + + + /** configure additional chained search condition. + * @param a manipulation functor `void(Filter&)`, which works on the current filter + * to possibly change its configuration. + * @note the given functor, lambda or function reference will be wrapped and adapted + * to conform to the required function signature. When using a generic lambda, + * the argument type `Filter&` is used for instantiation + * @remarks the additional chained search condition given here will be applied _after_ + * matching all other conditions already in the filter chain. Each such condition + * is used to _filter_ the underlying source iterator, i.e. pull it until finding + * and element to match the condition. Basically these conditions are _not_ used in + * conjunction, but rather one after another. But since each such step in the chain + * is defined by a functor, which gets the previous filter configuration as argument, + * it is _possible_ to build a step which _extends_ or sharpens the preceding condition. + */ + template + IterChainSearch&& + addStep (FUN&& configureSearchStep) + { + if (not this->empty()) + { + Step nextStep{forward (configureSearchStep)}; + + if (_Base::isDisabled()) + nextStep (*this); // apply first step immediately + else // (implicitly enables the base filter) + { + stepChain_.emplace_back (move (nextStep)); // append all further steps into the chain... + this->iterNext(); // then establish invariant: + } // expand to leaf and forward to first match + } + return move(*this); + } + + /** attach additional search with the given filter predicate. + * After successfully searching for all the conditions currently in the filter chain, + * the embedded iterator will finally be pulled until matching the given target value. + * @remarks adds a new layer on the stack of search conditions with a _copy_ of the + * previously used iterator, and installs the given filter predicate therein. + */ + template + disable_if, + IterChainSearch&& > + search (FUN&& filterPredicate) + { + addStep ([predicate{forward (filterPredicate)}] + (Filter& filter) + { // manipulte current filter configuration + filter.setNewFilter (predicate); + }); + return move(*this); + } + + /** attach additional direct search for a given value. + * After successfully searching for all the conditions currently in the filter chain, + * the embedded iterator will finally be pulled until matching the given target value. + */ + IterChainSearch&& + search (Value target) + { + search ([target](Value const& currVal) { return currVal == target; }); + return move(*this); + } + + /** drop all search condition frames. + * @remark the filter chain becomes empty, + * passing through the reset of the + * source sequence unaltered + */ + IterChainSearch&& + clearFilter() + { + stepChain_.clear(); + _Base::rootCurrent(); + _Base::disableFilter(); + return move(*this); + } + }; + + + + + /* ==== convenient builder free function ==== */ + + /** setup a chain search configuration by suitably wrapping the given container. + * @return an IterChainSearch instantiation, which is an Iterator to yield all the + * source elements, but can be outfitted with a sequence of filter conditions, + * to be applied to the underlying source one by one. + * @param srcData either a »Lumiera Forward Iterator«, a _reference_ to a STL + * container, or a [»State Core«](\ref lib::IterStateWrapper) object. + * @warning if you capture the result of this call by an auto variable, + * be sure to understand that invoking any further builder operation on + * TreeExplorer will invalidate that variable (by moving it into the + * augmented iterator returned from such builder call). + */ + template + inline auto + chainSearch (SRC&& srcData) + { + return IterChainSearch{forward (srcData)}; + } + + + +}} // namespace lib::iter +#endif /*SRC_LIB_ITER_CHAIN_SEARCH_H*/ diff --git a/src/lib/iter-cursor.hpp b/src/lib/iter-cursor.hpp index 66b81523e..013ed975e 100644 --- a/src/lib/iter-cursor.hpp +++ b/src/lib/iter-cursor.hpp @@ -26,12 +26,12 @@ ** This wrapper relies on the ability of typical STL container iterators ** to work in both directions, similar to std::reverse_iterator. ** Yet it is a single, self-contained element and in compliance to the - ** ["Lumiera Forward Iterator"](iter-adapter.hpp) concept. But it has + ** ["Lumiera Forward Iterator"](\ref iter-adapter.hpp) concept. But it has ** the additional ability to [switch the working direction](\ref IterCursor::switchDir). ** ** @see IterCursor_test ** @see iter-adapter.hpp - ** @see [usage example](event-log.hpp) + ** @see [usage example](\ref event-log.hpp) ** */ @@ -156,7 +156,7 @@ namespace lib { * It can be built on top of any bidirectional STL iterator or similar entity, * which has an `--` operator. Initially, IterCursor will operate in forward * direction; irrespective of the current direction, it always fulfils the - * ["Lumiera Forward Iterator"](iter-adapter.hpp) concept, i.e. it can be + * ["Lumiera Forward Iterator"](\ref iter-adapter.hpp) concept, i.e. it can be * iterated until exhaustion, in which case it will evaluate to bool(false). * @note IterCursor instances can be equality compared, also taking the * current direction into account. As a special case, all @@ -170,7 +170,12 @@ namespace lib { using _Parent = IterStateWrapper; public: - IterCursor() { } + IterCursor() =default; + IterCursor (IterCursor&&) =default; + IterCursor (IterCursor const&) =default; + IterCursor& operator= (IterCursor&&) =default; + IterCursor& operator= (IterCursor const&) =default; + template explicit diff --git a/src/lib/iter-stack.hpp b/src/lib/iter-stack.hpp index 39220f291..0e33ed15c 100644 --- a/src/lib/iter-stack.hpp +++ b/src/lib/iter-stack.hpp @@ -155,6 +155,12 @@ namespace lib { return topElement; } + void + clear() + { + this->stateCore().clear(); + } + /* == diagnostics == */ diff --git a/src/lib/iter-tree-explorer.hpp b/src/lib/iter-tree-explorer.hpp index 6aa6ea8a4..fe896534e 100644 --- a/src/lib/iter-tree-explorer.hpp +++ b/src/lib/iter-tree-explorer.hpp @@ -46,7 +46,7 @@ ** calculations with exception handling but also simple data structures like lists or trees). The key point with any ** monad is the ability to _bind a function_ into the monad; this function will work on the _contained base values_ ** and produce a modified new monad instance. In the simple case of a list, "binding" a function basically means - ** to _map the function onto_ the elements in the list. + ** to _map the function onto_ the elements in the list. (actually it means the `flatMap` operation) ** ** ## Rationale ** The primary benefit of using the monad pattern is to separate the transforming operation completely from @@ -166,6 +166,13 @@ namespace lib { // standard copy operations acceptable }; + template + struct StlRange + : StlRange + { + using StlRange::StlRange; + }; + /** * Decorate a state or logic core to treat it as Lumiera Forward Iterator. @@ -180,6 +187,7 @@ namespace lib { COR & _core() { return static_cast (*this); } COR const& _core() const { return static_cast (*this); } + protected: void __throw_if_empty() const { @@ -438,6 +446,8 @@ namespace lib { namespace iter_explorer { // Implementation of Iterator decorating layers... + constexpr auto ACCEPT_ALL = [](auto){return true;}; + /** * @internal technical details of binding a functor into the TreeExplorer. * Notably, this happens when adapting an _"expansion functor"_ to allow expanding a given element @@ -456,15 +466,18 @@ namespace lib { * accepts the iterator or state core itself (the "opaque state manipulation" usage pattern). * - we generate a suitable argument accessor function and build the function composition * of this accessor and the provided _expansion functor_. - * - the resulting, combined functor is stored into a std::function, but wired in a way to - * keep the argument-accepting front-end still generic (templated `operator()`). This - * special adapter supports the case when the _expansion functor_ yields a child sequence - * type different but compatible to the original source sequence embedded in TreeExplorer. - * @tparam FUN something _"function-like"_ passed as functor to be bound - * @tparam SRC the source iterator type to apply when attempting to use a generic lambda as functor + * - the resulting, combined functor is stored into a std::function, thereby abstracting + * from the actual adapting mechanism. This allows to combine different kinds of functors + * within the same processing step; and in addition, it allows the processing step to + * remain agnostic with respect to the adaptation and concrete type of the functor/lambda. + * @tparam FUN either the signature, or something _"function-like"_ passed as functor to be bound + * @tparam SRC (optional) but need to specify the source iterator type to apply when passing + * a generic lambda or template as FUN. Such a generic functor will be _instantiated_ + * passing the type `SRC&` as argument. This instantiation may fail (and abort compilation), + * but when it succeeds, we can infer the result type `Res` from the generic lambda */ - template - struct _BoundFunctor + template + struct _FunTraits { /** handle all regular "function-like" entities */ template @@ -491,51 +504,66 @@ namespace lib { /** adapt to a functor, which accesses the source iterator or embedded "state core" */ template - struct ArgAccessor + struct ArgAdapter { using FunArgType = remove_reference_t; static_assert (std::is_convertible::value, "the bound functor must accept the source iterator or state core as parameter"); - static auto build() { return [](ARG& arg) -> ARG& { return arg; }; } + static decltype(auto) + wrap (FUN&& rawFunctor) ///< actually pass-through the raw functor unaltered + { + return forward (rawFunctor); + } }; /** adapt to a functor, which accepts the value type of the source sequence ("monadic" usage pattern) */ template - struct ArgAccessor>> - { - static auto build() { return [](auto& iter) { return *iter; }; } + struct ArgAdapter + ,__not_>>>> // need to exclude the latter, since IterableDecorator + { // often seems to accept IT::value_type (while in fact it doesn't) + static auto + wrap (function rawFun) ///< adapt by dereferencing the source iterator + { + return [rawFun](IT& srcIter) -> Res { return rawFun(*srcIter); }; + } }; /** adapt to a functor collaborating with an IterSource based iterator pipeline */ template - struct ArgAccessor, typename IT::Source> + struct ArgAdapter, typename IT::Source> , is_base_of, remove_reference_t> > >> { using Source = typename IT::Source; - static auto build() { return [](auto& iter) -> Source& { return iter.source(); }; } - }; - - - /** holder for the suitably adapted _expansion functor_ */ - struct Functor - { - function boundFunction; - - template - Res - operator() (ARG& arg) + static auto + wrap (function rawFun) ///< extract the (abstracted) IterSource { - auto accessArg = ArgAccessor::build(); - - return boundFunction (accessArg (arg)); + return [rawFun](IT& iter) -> Res { return rawFun(iter.source()); }; } }; + + + + /** builder to create a nested/wrapping functor, suitably adapting the arguments */ + static auto + adaptFunctor (FUN&& rawFunctor) + { + return function {ArgAdapter::wrap (forward (rawFunctor))}; + } }; + template + inline void + static_assert_isPredicate() + { + using Res = typename _FunTraits::Res; + static_assert(std::is_constructible::value, "Functor must be a predicate"); + } + + /** * @internal Base of pipe processing decorator chain. @@ -557,8 +585,8 @@ namespace lib { : SRC { BaseAdapter() = default; - BaseAdapter(SRC const& src) : SRC(src) { } - BaseAdapter(SRC && src) : SRC(src) { } + BaseAdapter(SRC const& src) : SRC{src} { } + BaseAdapter(SRC && src) : SRC{forward (src)} { } void expandChildren() { } size_t depth() const { return 0; } @@ -574,7 +602,7 @@ namespace lib { * yielding a compatible value type). Now, this _sequence of children_ effectively * replaces the expanded source element in the overall resulting sequence; which * means, the nested sequence was _flattened_ into the results. Since this expand() - * operation can again invoked on the results, the implementation of such an evaluation + * operation can again be invoked on the results, the implementation of such an evaluation * requires a stack datastructure, so the nested iterator from each expand() invocation * can be pushed to become the new active source for iteration. Thus the primary purpose * of this Expander (decorator) is to integrate those "nested child iterators" seamlessly @@ -583,44 +611,53 @@ namespace lib { * the source iterator wrapped by this decorator. * @remark since we allow a lot of leeway regarding the actual form and definition of the * _expansion functor_, there is a lot of minute technical details, mostly confined - * within the _BoundFunctorTraits. + * within the _FunTraits traits. For the same reason, we need to prepare two different + * bindings of the passed raw functor, one to work on the source sequence, and the other + * one to work on the result sequence of a recursive child expansions; these two sequences + * need not be implemented in the same way, which simplifies the definition of algorithms. * @tparam SRC the wrapped source iterator, typically a TreeExplorer or nested decorator. * @tparam FUN the concrete type of the functor passed. Will be dissected to find the signature */ - template + template class Expander : public SRC { static_assert(can_IterForEach::value, "Lumiera Iterator required as source"); - using _Traits = _BoundFunctor; - using ExpandFunctor = typename _Traits::Functor; - using ResIter = typename _DecoratorTraits::SrcIter; - static_assert (std::is_convertible::value, + using ResIter = typename _DecoratorTraits::SrcIter; + static_assert (std::is_convertible(), "the iterator from the expansion must yield compatible values"); - ExpandFunctor expandChildren_; + using RootExpandFunctor = function; + using ChldExpandFunctor = function; + + RootExpandFunctor expandRoot_; + ChldExpandFunctor expandChild_; + IterStack expansions_; public: Expander() =default; // inherited default copy operations + template Expander (SRC&& parentExplorer, FUN&& expandFunctor) - : SRC{move (parentExplorer)} // NOTE: slicing move to strip TreeExplorer (Builder) - , expandChildren_{forward (expandFunctor)} + : SRC{move (parentExplorer)} // NOTE: slicing move to strip TreeExplorer (Builder) + , expandRoot_ {_FunTraits ::adaptFunctor (forward (expandFunctor))} // adapt to accept SRC& + , expandChild_{_FunTraits::adaptFunctor (forward (expandFunctor))} // adapt to accept RES& , expansions_{} { } + /** core operation: expand current head element */ void expandChildren() { REQUIRE (this->checkPoint(), "attempt to expand an empty explorer"); - ResIter expanded{ hasChildren()? expandChildren_(*expansions_) - : expandChildren_(*this)}; + ResIter expanded{ hasChildren()? expandChild_(*expansions_) + : expandRoot_(*this)}; incrementCurrent(); // consume current head element (but don't clean-up) if (not isnil(expanded)) expansions_.push (move(expanded)); @@ -637,6 +674,19 @@ namespace lib { return expansions_.size(); } + /** lock into the current child sequence. + * This special feature turns the current child sequence into the new root, + * thereby discarding everything else in the expansions stack, including the + * original root sequence. + */ + void + rootCurrent() + { + if (not hasChildren()) return; + static_cast (*this) = move (*expansions_); + expansions_.clear(); + } + public: /* === Iteration control API for IterableDecorator === */ @@ -669,13 +719,7 @@ namespace lib { invariant() const { return not hasChildren() - or *expansions_; - } - - bool - hasChildren() const - { - return 0 < depth(); + or expansions_->isValid(); } void @@ -687,12 +731,28 @@ namespace lib { ++(*this); } + + protected: + /** @internal accessor for downstream layers to allow close collaboration */ + ResIter& + accessCurrentChildIter() + { + REQUIRE (hasChildren()); + return *expansions_; + } + void dropExhaustedChildren() { while (not invariant()) ++expansions_; } + + bool + hasChildren() const + { + return 0 < depth(); + } }; @@ -718,6 +778,7 @@ namespace lib { void iterNext() { + SRC::__throw_if_empty(); SRC::expandChildren(); } }; @@ -725,7 +786,7 @@ namespace lib { /** - * @internal extension to the Expander decorator to perform expansion delayed on next iteration. + * @internal extension to the Expander decorator to perform expansion delayed on next iteration. */ template class ScheduledExpander @@ -750,6 +811,7 @@ namespace lib { { if (shallExpand_) { + SRC::__throw_if_empty(); SRC::expandChildren(); shallExpand_ = false; } @@ -767,32 +829,31 @@ namespace lib { * is adapted in a similar way as the "expand functor", so to detect and convert the * expected input on invocation. */ - template + template class Transformer : public SRC { static_assert(can_IterForEach::value, "Lumiera Iterator required as source"); - using _Traits = _BoundFunctor; - using Res = typename _Traits::Res; - using TransformFunctor = typename _Traits::Functor; - using TransformedItem = wrapper::ItemWrapper; + using TransformFunctor = function; + using TransformedItem = wrapper::ItemWrapper; TransformFunctor trafo_; TransformedItem treated_; public: - using value_type = typename meta::TypeBinding::value_type; - using reference = typename meta::TypeBinding::reference; - using pointer = typename meta::TypeBinding::pointer; + using value_type = typename meta::TypeBinding::value_type; + using reference = typename meta::TypeBinding::reference; + using pointer = typename meta::TypeBinding::pointer; Transformer() =default; // inherited default copy operations + template Transformer (SRC&& dataSrc, FUN&& transformFunctor) - : SRC{move (dataSrc)} - , trafo_{forward (transformFunctor)} + : SRC{move (dataSrc)} // NOTE: slicing move to strip TreeExplorer (Builder) + , trafo_{_FunTraits::adaptFunctor (forward (transformFunctor))} { } @@ -854,16 +915,14 @@ namespace lib { * The filter predicate and thus the source iterator is evaluated _eagerly_, to establish the * *invariant* of this class: _if a "current element" exists, it has already been approved._ */ - template + template class Filter : public SRC { static_assert(can_IterForEach::value, "Lumiera Iterator required as source"); - using _Traits = _BoundFunctor; - using Res = typename _Traits::Res; - static_assert(std::is_constructible::value, "Functor must be a predicate"); - using FilterPredicate = typename _Traits::Functor; + protected: + using FilterPredicate = function; FilterPredicate predicate_; @@ -872,9 +931,10 @@ namespace lib { Filter() =default; // inherited default copy operations + template Filter (SRC&& dataSrc, FUN&& filterFun) : SRC{move (dataSrc)} - , predicate_{forward (filterFun)} + , predicate_{_FunTraits::adaptFunctor (forward (filterFun))} { pullFilter(); // initially pull to establish the invariant } @@ -885,8 +945,8 @@ namespace lib { void expandChildren() { - pullFilter(); SRC::expandChildren(); + pullFilter(); } public: /* === Iteration control API for IterableDecorator === */ @@ -910,25 +970,207 @@ namespace lib { pullFilter(); } - private: + + protected: SRC& srcIter() const { return unConst(*this); } + bool + isDisabled() const + { + return not bool{predicate_}; + } + /** @note establishes the invariant: * whatever the source yields as current element, * has already been approved by our predicate */ void pullFilter () { + if (isDisabled()) return; while (srcIter() and not predicate_(srcIter())) ++srcIter(); } }; + + /** + * @internal Special variant of the \ref Filter Decorator to allow for dynamic remoulding. + * This covers a rather specific use case, where we want to remould or even exchange the + * Filter predicate in the middle of an ongoing iteration. Such a remoulding can be achieved + * by binding the existing (opaque) filter predicate into a new combined lambda, thereby + * capturing it _by value._ After building, this remoulded version can be assigned to the + * original filter functor, under the assumption that both are roughly compatible. Moreover, + * since we wrap the actual lambda into an adapter, allowing for generic lambdas to be used + * as filter predicates, this setup allows for a lot of leeway regarding the concrete predicates. + * @note whenever the filter is remoulded, the invariant is immediately + * [re-established](\ref Filter::pullFilter() ), possibly forwarding the sequence + * to the next element approved by the new version of the filter. + * @remarks filter predicates can be specified in a wide variety of forms, and will be adapted + * automatically. This flexibility also holds for any of the additional clauses provided + * for remoulding the filter. Especially this means that functors of different kinds can + * be mixed and combined. + */ + template + class MutableFilter + : public Filter + { + using _Filter = Filter; + static_assert(can_IterForEach::value, "Lumiera Iterator required as source"); + + public: + MutableFilter() =default; + // inherited default copy operations + + template + MutableFilter (SRC&& dataSrc, FUN&& filterFun) + : _Filter{move (dataSrc), forward (filterFun)} + { } + + + public: /* === API to Remould the Filter condition underway === */ + + /** remould existing predicate to require in addition the given clause to hold */ + template + void + andFilter (COND&& conjunctiveClause) + { + remouldFilter (forward (conjunctiveClause) + ,[](auto first, auto chain) + { + return [=](auto& val) + { + return first(val) + and chain(val); + }; + }); + } + + /** remould existing predicate to require in addition the negation of the given clause to hold */ + template + void + andNotFilter (COND&& conjunctiveClause) + { + remouldFilter (forward (conjunctiveClause) + ,[](auto first, auto chain) + { + return [=](auto& val) + { + return first(val) + and not chain(val); + }; + }); + } + + /** remould existing predicate to require either the old _OR_ the given new clause to hold */ + template + void + orFilter (COND&& disjunctiveClause) + { + remouldFilter (forward (disjunctiveClause) + ,[](auto first, auto chain) + { + return [=](auto& val) + { + return first(val) + or chain(val); + }; + }); + } + + /** remould existing predicate to require either the old _OR_ the negation of a new clause to hold */ + template + void + orNotFilter (COND&& disjunctiveClause) + { + remouldFilter (forward (disjunctiveClause) + ,[](auto first, auto chain) + { + return [=](auto& val) + { + return first(val) + or not chain(val); + }; + }); + } + + /** remould existing predicate to negate the meaning of the existing clause */ + void + flipFilter() + { + auto dummy = [](auto){ return false; }; + remouldFilter (dummy + ,[](auto currentFilter, auto) + { + return [=](auto& val) + { + return not currentFilter(val); + }; + }); + } + + /** replace the existing predicate with the given, entirely different predicate */ + template + void + setNewFilter (COND&& entirelyDifferentPredicate) + { + remouldFilter (forward (entirelyDifferentPredicate) + ,[](auto, auto chain) + { + return [=](auto& val) + { + return chain(val); + }; + }); + } + + /** discard filter predicates and disable any filtering */ + void + disableFilter() + { + _Filter::predicate_ = nullptr; + } + + + private: + /** @internal boilerplate to remould the filter predicate in-place + * @param additionalClause additional functor object to combine + * @param buildCombinedClause a _generic lambda_ (important!) to define + * how exactly the old and the new predicate are to be combined + * @note the actual combination logic is handed in as generic lambda, which + * essentially is a template class, and this allows to bind to any kind + * of function objects or lambdas. This combination closure requires a + * specific setup: when invoked with the existing and the new functor + * as argument, it needs to build a new _likewise generic_ lambda + * to perform the combined evaluation. + * @warning the handed-in lambda `buildCombinedClause` must capture its + * arguments, the existing functors _by value._ This is the key piece + * in the puzzle, since it effectively moves the existing functor into + * a new heap allocated storage. + */ + template + void + remouldFilter (COND&& additionalClause, COMB buildCombinedClause) + { + static_assert_isPredicate(); + + if (_Filter::isDisabled()) + _Filter::predicate_ = ACCEPT_ALL; + + _Filter::predicate_ = buildCombinedClause (_Filter::predicate_ // pick up the existing filter predicate + ,_FunTraits::adaptFunctor (forward (additionalClause)) + ); // wrap the extension predicate in a similar way + _Filter::pullFilter(); // then pull to re-establish the Invariant + } + }; + + + + /** * Interface to indicate and expose the ability for _child expansion_. * This interface is used when packaging a TreeExplorer pipeline opaquely into IterSource. @@ -966,6 +1208,7 @@ namespace lib { using Parent = WrappedLumieraIter; using Val = typename SRC::value_type; ///////////////////////////////////TICKET #1125 : get rid of Val + ~PackagedTreeExplorerSource() { } public: using Parent::Parent; @@ -1070,10 +1313,14 @@ namespace lib { * This allows to separate the mechanics of evaluation and result combination * from the actual processing and thus to define tree structured computations * based on an opaque source data structure not further disclosed. + * @tparam SRC a suitably adapted _source iterator_ or _state core_, wrapped + * into an instance of the iter_explorer::BaseAdapter template * - * \param usage + * \par usage + * TreeExplorer is meant to be used as *Builder* for a processing pipeline. + * For this to work, it is essential to pick the `SRC` baseclass properly. * - to build a TreeExplorer, use the treeExplore() free function, - * which cares to pick up an possibly adapt the given iteration source + * which cares to pick up and possibly adapt the given iteration source * - to add processing layers, invoke the builder operations on TreeExplorer * in a chained fashion, thereby binding functors or lambdas. Capture the * final result with an auto variable. @@ -1108,7 +1355,7 @@ namespace lib { /* ==== Builder functions ==== */ /** preconfigure this TreeExplorer to allow for _"expansion of children"_. - * The resulting iterator exposes an `expand()` function, which consumes + * The resulting iterator exposes an `expandChildren()` function, which consumes * the current head element of this iterator and feeds it through the * _expansion functor_, which was provided to this builder function here. * The _expansion functor_ is expected to yield a sequence of "child" elements, @@ -1139,12 +1386,15 @@ namespace lib { * typed argument. Obviously, argument and result type should also make sense for * the desired evaluation pattern, otherwise you'll get all kinds of nasty * template compilation failures (have fun!) + * @return processing pipeline with attached [Expander](\ref iter_explorer::Expander) decorator */ template auto expand (FUN&& expandFunctor) { - using ResCore = iter_explorer::Expander; + using ExpandedChildren = typename iter_explorer::_FunTraits::Res; + + using ResCore = iter_explorer::Expander; using ResIter = typename _DecoratorTraits::SrcIter; return TreeExplorer (ResCore {move(*this), forward(expandFunctor)}); @@ -1156,6 +1406,7 @@ namespace lib { * necessary to invoke `expandChildren()` (and doing so would have no further effect than * just iterating). Thus, on iteration, each current element will be fed to the _expand functor_ * and the results will be integrated depth first. + * @return processing pipeline with attached [AutoExpander](\ref iter_explorer::AutoExpander) decorator * @warning iteration will be infinite, unless the _expand functor_ provides some built-in * termination condition, returning an empty child sequence at that point. This would * then be the signal for the internal combination mechanism to return to visiting the @@ -1171,7 +1422,7 @@ namespace lib { } - /** extension functionality to be used on top of expand(), to perform expansion on next iteration. + /** extension functionality to be used on top of expand(), to perform expansion on next iteration. * When configured, an expandChildren() call will not happen immediately, but rather in place of * the next iteration step. Basically child expansion _is kind of a special iteration step,_ and * thus all we need to do is add another layer with a boolean state flag, which catches the @@ -1198,12 +1449,15 @@ namespace lib { * functor supports the same definition styles as described for #expand * - it can be pure functional, src -> res * - it can accept the underlying source iterator and exploit side-effects + * @return processing pipeline with attached [Transformer](\ref iter_explorer::Transformer) decorator */ template auto transform (FUN&& transformFunctor) { - using ResCore = iter_explorer::Transformer; + using Product = typename iter_explorer::_FunTraits::Res; + + using ResCore = iter_explorer::Transformer; using ResIter = typename _DecoratorTraits::SrcIter; return TreeExplorer (ResCore {move(*this), forward(transformFunctor)}); @@ -1214,29 +1468,105 @@ namespace lib { * The previously created source layers will be "pulled" to fast-forward immediately to the * next element confirmed this way by the bound functor. If none of the source elements * is acceptable, the iterator will transition to exhausted state immediately. + * @return processing pipeline with attached [Filter](\ref iter_explorer::Filter) decorator */ template auto filter (FUN&& filterPredicate) { - using ResCore = iter_explorer::Filter; + iter_explorer::static_assert_isPredicate(); + + using ResCore = iter_explorer::Filter; using ResIter = typename _DecoratorTraits::SrcIter; return TreeExplorer (ResCore {move(*this), forward(filterPredicate)}); } + /** attach a special filter adapter, allowing to change the filter predicate while iterating. + * While otherwise this filter layer behaves exactly like the [standard version](\ref #filter), + * it exposes a special API to augment or even completely switch the filter predicate while + * in the middle of iterator evaluation. Of course, the underlying iterator is not re-evaluated + * from start (our iterators can not be reset), and so the new filter logic takes effect starting + * from the current element. Whenever the filter is remoulded, it is immediately re-evaluated, + * possibly causing the underlying iterator to be pulled until an element matching the condition + * is found. + * @return processing pipeline with attached [special MutableFilter](\ref iter_explorer::MutableFilter) decorator + * @see \ref IterTreeExplorer_test::verify_FilterChanges() + * @see \ref iter_explorer::MutableFilter::andFilter() + * @see \ref iter_explorer::MutableFilter::andNotFilter() + * @see \ref iter_explorer::MutableFilter::orFilter() + * @see \ref iter_explorer::MutableFilter::orNotFilter() + * @see \ref iter_explorer::MutableFilter::flipFilter() + * @see \ref iter_explorer::MutableFilter::setNewFilter() + */ + template + auto + mutableFilter (FUN&& filterPredicate) + { + iter_explorer::static_assert_isPredicate(); + + using ResCore = iter_explorer::MutableFilter; + using ResIter = typename _DecoratorTraits::SrcIter; + + return TreeExplorer (ResCore {move(*this), forward(filterPredicate)}); + } + + + auto + mutableFilter() + { + return mutableFilter (iter_explorer::ACCEPT_ALL); + } + + + + /** builder function to attach a _custom extension layer._ + * Any template in compliance with the general construction scheme can be injected through the template parameter. + * - it must take a first template parameter SRC and inherit from this source iterator + * - towards layers on top, it must behave like a _state core,_ either by redefining the state core API functions, + * as defined by \ref IterStateWrapper, or by inheriting them from a lower layer. + * - it is bound to play well with the other layers; especially it needs to be aware of `expandChildren()` calls, + * which for the consumer side behave like `iterNext()` calls. If a layer needs to do something special for + * `iterNext()`, it needs to perform a similar action for `expandChildren()`. + * - it must be behave like a default-constructible, copyable value object + * @return augmented TreeExplorer, incorporating and adpting the injected layer + */ + template class LAY> + auto + processingLayer() + { + using ResCore = LAY; + using ResIter = typename _DecoratorTraits::SrcIter; + + return TreeExplorer (ResCore {move(*this)}); + } + + + /** _terminal builder_ to package the processing pipeline as IterSource. * Invoking this function moves the whole iterator compound, as assembled by the preceding - * builder calls, into heap allocated memory and returns a [iterator front-end](\ref IterExploreSource). + * builder calls, into heap allocated memory and returns an [iterator front-end](\ref IterExploreSource). * Any iteration and manipulation on that front-end is passed through virtual function calls into * the back-end, thereby concealing all details of the processing pipeline. + * @return an front-end handle object, which is an "Lumiera Forward Iterator", + * while holding onto a heap allocated [abstracted data source](\ref lib::IterExplorer). */ IterExploreSource asIterSource() { return IterExploreSource {move(*this)}; } + + + /** _terminal builder_ to strip the TreeExplorer and expose the built Pipeline. + * @return a »Lumiera Forward iterator« incorporating the complete pipeline logic. + */ + SRC + asIterator() + { + return SRC {move(*this)}; + } }; @@ -1256,6 +1586,44 @@ namespace lib { * be sure to understand that invoking any further builder operation on * TreeExplorer will invalidate that variable (by moving it into the * augmented iterator returned from such builder call). + * + * ## usage + * + * This function starts a *Builder* expression. It picks up the given source, + * which can be something "sequence-like" or "iterable", and will automatically + * wrapped and adapted. + * - from a STL container, we retrieve a pair of STL iterators (`begin()`, `end()`) + * - a "Lumiera Forward Iterator" is copied or moved into the wrapper and used as + * data source, pulling results on demand until exhaustion + * - a _State Core_ object is copied or moved into the wrapper and adapted to + * be iterated as "Lumiera Forward Iterator". Any object with suitable extension + * points and behaviour can be used, as explained [here](\ref lib::IterStateWrapper). + * + * The resulting TreeExplorer instance can be directly used as "Lumiera Forward Iterator". + * However, typically you might want to invoke the builder functions to configure further + * processing steps in a processing pipeline... + * - to [filter](\ref TreeExplorer::filter) the results with a predicate (functor) + * - to [transform](\ref TreeExplorer::transform) the yielded results + * - to bind and configure a [child expansion operation](\ref TreeExplorer::expand), which + * can then be triggered by explicitly invoking [.expandChildren()](\ref iter_explorer::Expander::expandChildren) + * on the resulting pipeline; the generated child sequence is "flat mapped" into the results + * (a *Monad* style usage). + * - to [package](\ref TreeExplorer::asIterSource) the pipeline built thus far behind an opaque + * interface and move the implementation into heap memory. + * + * A typical usage might be... + * \code + * auto multiply = [](int v){ return 2*v; }; + * + * auto ii = treeExplore (CountDown{7,4}) + * .transform(multiply); + * + * CHECK (14 == *ii); + * ++ii; + * CHECK (12 == *ii); + * ++ii; + * \endcode + * In this example, a `CountDown` _state core_ is used, as defined in iter-tree-explorer-test.cpp */ template inline auto diff --git a/src/lib/itertools.hpp b/src/lib/itertools.hpp index e95fdd654..d1fb2a616 100644 --- a/src/lib/itertools.hpp +++ b/src/lib/itertools.hpp @@ -205,7 +205,11 @@ namespace lib { hasData(); } - operator bool() const { return isValid(); } + explicit + operator bool() const + { + return isValid(); + } diff --git a/src/lib/meta/trait.hpp b/src/lib/meta/trait.hpp index d82364655..71b2d249b 100644 --- a/src/lib/meta/trait.hpp +++ b/src/lib/meta/trait.hpp @@ -39,10 +39,10 @@ ** the ability for string conversions ** ** \par ability to iterate - ** these traits [can be used](util-foreach.hpp) to build the notion of a + ** these traits [can be used](\ref util-foreach.hpp) to build the notion of a ** generic container -- basically anything that can be enumerated. ** Within Lumiera, we frequently use our own concept of "iterability", - ** known as ["Lumiera Forward Iterator"](iter-adapter.hpp). These + ** known as ["Lumiera Forward Iterator"](\ref iter-adapter.hpp). These ** helpers here allow to unify this concept with the "Range" ** concept from the standard library (`begin()` and `end()`) ** diff --git a/src/lib/meta/util.hpp b/src/lib/meta/util.hpp index 5df0dad53..6a742c56a 100644 --- a/src/lib/meta/util.hpp +++ b/src/lib/meta/util.hpp @@ -334,7 +334,7 @@ namespace util { * representation for pretty much every language object._ This minimal solution * is defined here, to allow for built-in diagnostics for custom types without * the danger of creating much header inclusion and code size bloat. A more - * elaborate, [extended solution](lib::toString), including _lexical conversions + * elaborate, [extended solution](\ref lib::toString), including _lexical conversions * for numbers,_ is defined in format-obj.hpp * @note any exceptions during string conversion are caught and silently ignored; * the returned string indicates "↯" in this case. diff --git a/src/lib/opaque-holder.hpp b/src/lib/opaque-holder.hpp index e6badc185..4718cf38d 100644 --- a/src/lib/opaque-holder.hpp +++ b/src/lib/opaque-holder.hpp @@ -704,9 +704,10 @@ namespace lib { * handle to allow for safe _»remote implantation«_ * of an unknown subclass into a given opaque InPlaceBuffer, * without having to disclose the concrete buffer type or size. - * @remarks this is especially geared towards use in APIs, allowing - * a not yet known implementation to implant an agent or collaboration - * partner into the likewise undisclosed innards of the exposed service. + * @remarks this copyable value object is especially geared towards use + * as handle in APIs, allowing a not yet known implementation to implant + * an agent or collaboration partner into the likewise undisclosed innards + * of the service exposed. * @warning the type BA must expose a virtual dtor, since the targeted * InPlaceBuffer has to take ownership of the implanted object. */ diff --git a/src/lib/symbol-impl.cpp b/src/lib/symbol-impl.cpp index a8c7676b9..066d16246 100644 --- a/src/lib/symbol-impl.cpp +++ b/src/lib/symbol-impl.cpp @@ -112,9 +112,9 @@ namespace lib { size_t hash=0; if (literal) { + size_t cnt = 1; const char *pos = literal; - size_t maxpos = STRING_MAX_RELEVANT; - for ( ; *pos && --maxpos; ++pos) + for ( ; cnt <= STRING_MAX_RELEVANT and *pos ; ++cnt, ++pos ) hash_combine(hash, *pos); } diff --git a/src/lib/sync-classlock.hpp b/src/lib/sync-classlock.hpp index 22acbaec8..abc581ce4 100644 --- a/src/lib/sync-classlock.hpp +++ b/src/lib/sync-classlock.hpp @@ -39,6 +39,7 @@ #include "lib/nobug-init.hpp" #include "lib/zombie-check.hpp" +#include "lib/meta/util.hpp" #include "lib/sync.hpp" @@ -72,7 +73,7 @@ namespace lib { getPerClassMonitor() { static PerClassMonitor classMonitor; - static ZombieCheck zombieCheck; + static ZombieCheck zombieCheck{util::typeStr(this)}; zombieCheck(); return classMonitor; diff --git a/src/lib/sync.hpp b/src/lib/sync.hpp index add2d7ec6..76c7c034b 100644 --- a/src/lib/sync.hpp +++ b/src/lib/sync.hpp @@ -257,7 +257,7 @@ namespace lib { return *this; } - operator bool() { return 0 != tv_sec; } // allows if (timeout_).... + explicit operator bool() { return 0 != tv_sec; } // allows if (timeout_).... }; @@ -368,7 +368,7 @@ namespace lib { } void setTimeout(ulong relative) {timeout_.setOffset(relative);} - bool isTimedWait() {return (timeout_);} + bool isTimedWait() {return bool{timeout_};} }; typedef Mutex NonrecursiveLock_NoWait; diff --git a/src/lib/test/event-log.cpp b/src/lib/test/event-log.cpp new file mode 100644 index 000000000..d359f34fb --- /dev/null +++ b/src/lib/test/event-log.cpp @@ -0,0 +1,804 @@ +/* + EventLog - test facility to verify the occurrence of expected events + + Copyright (C) Lumiera.org + 2018, 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 event-log.cpp + ** Implementation details of event logging and verification for unit tests. + ** The EventLog is a helper for writing tests or for special rigging within + ** the test or debugging or mock variant of some common services. It allows + ** to log some events to memory and then to build match expressions to verify + ** those captured information. + ** + ** The machinery for matching on the occurrence of some events can be quite + ** intricate at implementation level; we use an implementation based on a + ** filtering iterator pipeline. This kind of code is template heavy and + ** can cause a significant increase of generated code size -- especially + ** on debug builds. For that reason, we prefer to call through regular + ** (non-inline) functions and package all the actual implementation + ** code into this dedicated translation unit. + ** + ** @see TestEventLog_test + ** @see IterChainSearch_test + ** @see iter-tree-explorer.hpp + ** + */ + + + +#include "lib/error.hpp" +#include "lib/test/event-log.hpp" + + + +namespace lib { +namespace test{ + namespace error = lumiera::error; + + using util::stringify; + using util::contains; + using util::isnil; + using lib::Symbol; + using std::string; + + + namespace { /* == elementary matchers == */ + + using Filter = decltype( buildSearchFilter (std::declval()) ); + + using ArgSeq = lib::diff::RecordSetup::Storage; + using RExSeq = std::vector; + + + inline auto + find (string match) + { + return [=](Entry const& entry) + { + return contains (string(entry), match); + }; + } + + inline auto + findRegExp (string regExpDef) + { + std::regex regExp(regExpDef); + return [=](Entry const& entry) + { + return std::regex_search(string(entry), regExp); + }; + } + + inline auto + findEvent (string match) + { + return [=](Entry const& entry) + { + return ( entry.getType() == "event" + or entry.getType() == "error" + or entry.getType() == "create" + or entry.getType() == "destroy" + or entry.getType() == "logJoin" + ) + and !isnil(entry.scope()) + and contains (*entry.scope(), match); + }; + } + + inline auto + findEvent (string classifier, string match) + { + return [=](Entry const& entry) + { + return ( entry.getType() == classifier + or (entry.hasAttribute("ID") and contains (entry.get("ID"), classifier)) + ) + and !isnil(entry.scope()) + and contains (*entry.scope(), match); + }; + } + + inline auto + findCall (string match) + { + return [=](Entry const& entry) + { + return entry.getType() == "call" + and contains (entry.get("fun"), match); + }; + } + + + /** this filter functor is for refinement of an existing filter + * @param argSeq perform a substring match consecutively + * for each of the log entry's arguments + * @note the match also fails, when the given log entry + * has more or less arguments, than the number of + * given match expressions in `argSeq` + * @see ExtensibleFilterIter::andFilter() + */ + inline auto + matchArguments (ArgSeq&& argSeq) + { + return [=](Entry const& entry) + { + auto scope = entry.scope(); + for (auto const& match : argSeq) + if (isnil (scope) or not contains(*scope, match)) + return false; + else + ++scope; + + return isnil(scope); // must be exhausted by now + }; // otherwise the sizes do not match... + } + + + /** refinement filter, to cover all arguments by regular expression(s) + * @param regExpSeq several regular expressions, which, when applied + * consecutively until exhaustion, must cover and verify _all_ + * arguments of the log entry. + * @remarks to explain, we "consume" arguments with a regExp from the list, + * and when this one doesn't match anymore, we try the next one. + * When we'ver tried all regular expressions, we must have also + * consumed all arguments, otherwise we fail. + */ + inline auto + matchArgsRegExp (RExSeq&& regExpSeq) + { + return [=](Entry const& entry) + { + auto scope = entry.scope(); + for (auto const& rex : regExpSeq) + { + if (isnil (scope)) return false; + while (scope and std::regex_search(*scope, rex)) + ++scope; + } + + return isnil(scope); // must be exhausted by now + }; // otherwise we didn't get full coverage... + } + + + /** refinement filter to match on the given typeID */ + inline auto + matchType (string typeID) + { + return [=](Entry const& entry) + { + return contains (entry.getType(), typeID); + }; + } + + + /** refinement filter to ensure a specific attribute is present on the log entry */ + inline auto + ensureAttribute (string key) + { + return [=](Entry const& entry) + { + return entry.hasAttribute(key); + }; + } + + + /** refinement filter to ensure a specific attribute is present on the log entry */ + inline auto + matchAttribute (string key, string valueMatch) + { + return [=](Entry const& entry) + { + return entry.hasAttribute(key) + and contains (entry.get(key), valueMatch); + }; + } + + + + /* === configure the underlying search engine === */ + + enum Direction { + FORWARD, BACKWARD, CURRENT + }; + + template + inline void + attachNextSerchStep (Filter& solution, COND&& filter, Direction direction) + { + if (CURRENT == direction) + solution.search (forward (filter)); + else + solution.addStep ([predicate{forward (filter)}, direction] + (auto& filter) + { + filter.reverse (BACKWARD == direction); + filter.disableFilter(); // deactivate any filtering temporarily + ++filter; // move one step in the required direction + filter.setNewFilter (predicate); + }); + } + + template + inline void + refineSerach (Filter& solution, COND&& additionalFilter) + { + solution.addStep ([predicate{forward (additionalFilter)}] + (auto& filter) + { + filter.andFilter (predicate); + }); + } + + }//(End) Elementary Matchers and handling of the search engine + + + + + + EventMatch::EventMatch (Log const& srcSeq) + : solution_{buildSearchFilter (srcSeq)} + , lastMatch_{"HEAD "+ solution_->get("this")} + , look_for_match_{true} + , violation_{} + { } + + + + /** core of the evaluation machinery: + * apply a filter predicate and then pull + * through the log to find a acceptable entry + */ + bool + EventMatch::foundSolution() + { + return not isnil (solution_); + } + + + /** this is actually called after each refinement of + * the filter and matching conditions. The effect is to search + * for an (intermediary) solution right away and to mark failure + * as soon as some condition can not be satisfied. Rationale is to + * indicate the point where a chained match fails + * @see ::operator bool() for extracting the result + * @param matchSpec diagnostics description of the predicate just being added + * @param rel indication of the searching relation / direction + */ + void + EventMatch::evaluateQuery (string matchSpec, Literal rel) + { + if (look_for_match_ and not isnil (violation_)) return; + // already failed, no further check necessary + + if (foundSolution()) // NOTE this pulls the filter + { + lastMatch_ = matchSpec+" @ "+string(*solution_) + + (isnil(lastMatch_)? "" + : "\n.."+rel+" "+lastMatch_); + if (not look_for_match_) + violation_ = "FOUND at least "+lastMatch_; + } + else + { + if (look_for_match_) + violation_ = "FAILED to "+matchSpec + + "\n.."+rel + + " "+lastMatch_; + else + violation_ = ""; + } + } + + + + /** + * basic search function: continue linear lookup over the elements of the + * EventLog to find a match (substring match) of the given text. The search begins + * at the current position and proceeds in the currently configured direction. + * Initially the search starts at the first record and proceeds forward. + */ + EventMatch& + EventMatch::locate (string match) + { + attachNextSerchStep (solution_, find(match), CURRENT); + evaluateQuery ("match(\""+match+"\")"); + return *this; + } + + /** + * basic search like locate() but with the given regular expression + */ + EventMatch& + EventMatch::locateMatch (string regExp) + { + attachNextSerchStep (solution_, findRegExp(regExp), CURRENT); + evaluateQuery ("find-RegExp(\""+regExp+"\")"); + return *this; + } + + /** + * basic search for a matching "event" + * @param match perform a substring match against the arguments of the event + * @see beforeEvent() for a description of possible "events" + */ + EventMatch& + EventMatch::locateEvent (string match) + { + attachNextSerchStep (solution_, findEvent(match), CURRENT); + evaluateQuery ("match-event(\""+match+"\")"); + return *this; + } + + EventMatch& + EventMatch::locateEvent (string classifier, string match) + { + attachNextSerchStep (solution_, findEvent(classifier,match), CURRENT); + evaluateQuery ("match-event(ID=\""+classifier+"\", \""+match+"\")"); + return *this; + } + + /** + * basic search for some specific function invocation + * @param match perform a substring match against the name of the function invoked + */ + EventMatch& + EventMatch::locateCall (string match) + { + attachNextSerchStep (solution_, findCall(match), CURRENT); + evaluateQuery ("match-call(\""+match+"\")"); + return *this; + } + + + + + /** + * find a match (substring match) of the given text + * in an EventLog entry after the current position + * @remarks the name of this junctor might seem counter intuitive; + * it was chosen due to expected DSL usage: `log.verify("α").before("β")`. + * Operationally this means first to find a Record matching the substring "α" + * and then to forward from this point until hitting a record to match "β". + */ + EventMatch& + EventMatch::before (string match) + { + attachNextSerchStep (solution_, find(match), FORWARD); + evaluateQuery ("match(\""+match+"\")"); + return *this; + } + + /** + * find a match with the given regular expression + */ + EventMatch& + EventMatch::beforeMatch (string regExp) + { + attachNextSerchStep (solution_, findRegExp(regExp), FORWARD); + evaluateQuery ("find-RegExp(\""+regExp+"\")"); + return *this; + } + + /** + * find a match for an "event" _after_ the current point of reference + * @remarks the term "event" designates several types of entries, which + * typically capture something happening within the observed entity. + * Especially, the following [record types](\ref lib::Record::getType()) + * qualify as event: + * - `event` + * - `error` + * - `create` + * - `destroy` + * - `logJoin` + * @param match perform a substring match against the arguments of the event + * @see ::findEvent + */ + EventMatch& + EventMatch::beforeEvent (string match) + { + attachNextSerchStep (solution_, findEvent(match), FORWARD); + evaluateQuery ("match-event(\""+match+"\")"); + return *this; + } + + EventMatch& + EventMatch::beforeEvent (string classifier, string match) + { + attachNextSerchStep (solution_, findEvent(classifier,match), FORWARD); + evaluateQuery ("match-event(ID=\""+classifier+"\", \""+match+"\")"); + return *this; + } + + /** + * find a match for some function invocation _after_ the current point of reference + * @param match perform a substring match against the name of the function invoked + * @see ::findCall + */ + EventMatch& + EventMatch::beforeCall (string match) + { + attachNextSerchStep (solution_, findCall(match), FORWARD); + evaluateQuery ("match-call(\""+match+"\")"); + return *this; + } + + + + + /** + * find a match (substring match) of the given text + * in an EventLog entry _before_ the current position, + * switching to backwards search direction + */ + EventMatch& + EventMatch::after (string match) + { + attachNextSerchStep (solution_, find(match), BACKWARD); + evaluateQuery ("match(\""+match+"\")", "before"); + return *this; + } + + EventMatch& + EventMatch::afterMatch (string regExp) + { + attachNextSerchStep (solution_, findRegExp(regExp), BACKWARD); + evaluateQuery ("find-RegExp(\""+regExp+"\")", "before"); + return *this; + } + + EventMatch& + EventMatch::afterEvent (string match) + { + attachNextSerchStep (solution_, findEvent(match), BACKWARD); + evaluateQuery ("match-event(\""+match+"\")", "before"); + return *this; + } + + EventMatch& + EventMatch::afterEvent (string classifier, string match) + { + attachNextSerchStep (solution_, findEvent(classifier,match), BACKWARD); + evaluateQuery ("match-event(ID=\""+classifier+"\", \""+match+"\")", "before"); + return *this; + } + + /** + * find a function invocation backwards, before the current point of reference + */ + EventMatch& + EventMatch::afterCall (string match) + { + attachNextSerchStep (solution_, findCall(match), BACKWARD); + evaluateQuery ("match-call(\""+match+"\")", "before"); + return *this; + } + + + + /** + * @internal refine filter condition additionally to match the call arguments. + */ + void + EventMatch::refineSerach_matchArguments (ArgSeq&& argSeq) + { + string argList(util::join(argSeq)); + refineSerach (solution_, matchArguments(move(argSeq))); + evaluateQuery ("match-arguments("+argList+")"); + } + + /** + * @internal refine filter condition additionally to cover call arguments by + * a sequence of regular expressions. Each regExp is used consecutively, + * until it fails to match; at that point, the next regExp is tried. + * When reaching the end of the argument sequence, we also expect + * to have used up all available regular expressions. + */ + void + EventMatch::refineSerach_matchArgsRegExp (RExSeq&& regExpSeq, string rendered_regExps) + { + refineSerach (solution_, matchArgsRegExp (move (regExpSeq))); + evaluateQuery ("match-args-RegExp("+rendered_regExps+")"); + } + + + + /** + * refine filter to additionally require a matching log entry type + */ + EventMatch& + EventMatch::type (string typeID) + { + refineSerach (solution_, matchType(typeID)); + evaluateQuery ("match-type("+typeID+")"); + return *this; + } + + /** + * refine filter to additionally require the presence an attribute + */ + EventMatch& + EventMatch::key (string key) + { + refineSerach (solution_, ensureAttribute(key)); + evaluateQuery ("ensure-attribute("+key+")"); + return *this; + } + + /** + * refine filter to additionally match on a specific attribute + */ + EventMatch& + EventMatch::attrib (string key, string valueMatch) + { + refineSerach (solution_, matchAttribute(key,valueMatch)); + evaluateQuery ("match-attribute("+key+"=\""+valueMatch+"\")"); + return *this; + } + + /** + * refine filter to additionally match on the ID attribute + */ + EventMatch& + EventMatch::id (string classifier) + { + refineSerach (solution_, matchAttribute("ID",classifier)); + evaluateQuery ("match-ID(\""+classifier+"\")"); + return *this; + } + + /** + * refine filter to additionally match the `'this'` attribute + */ + EventMatch& + EventMatch::on (string targetID) + { + refineSerach (solution_, matchAttribute("this",targetID)); + evaluateQuery ("match-this(\""+targetID+"\")"); + return *this; + } + + EventMatch& + EventMatch::on (const char* targetID) + { + refineSerach (solution_, matchAttribute("this",targetID)); + evaluateQuery ("match-this(\""+string(targetID)+"\")"); + return *this; + } + + + + + + /* ==== EventLog implementation ==== */ + + EventLog::EventLog (string logID) + : log_(new Log) + { + log({"type=EventLogHeader", "this="+logID}); + } + + + /** + * @remarks EventLog uses a heap based, sharable log storage, + * where the EventLog object is just a front-end (shared ptr). + * The `joinInto` operation both integrates this logs contents + * into the other log, and then disconnects from the old storage + * and connects to the storage of the combined log. + * @warning beware of clone copies. Since copying EventLog is always a + * shallow copy, all copied handles actually refer to the same + * log storage. If you invoke `joinInto` in such a situation, + * only the current EventLog front-end handle will be rewritten + * to point to the combined log, while any other clone will + * continue to point to the original log storage. + */ + EventLog& + EventLog::joinInto (EventLog& otherLog) + { + Log& target = *otherLog.log_; + target.reserve (target.size() + log_->size() + 1); + target.emplace_back (log_->front()); + auto p = log_->begin(); + while (++p != log_->end()) // move our log's content into the other log + target.emplace_back (std::move(*p)); + this->log_->resize(1); + this->log({"type=joined", otherLog.getID()}); // leave a tag to indicate + otherLog.log({"type=logJoin", this->getID()}); // where the `joinInto` took place, + this->log_ = otherLog.log_; // connect this to the other storage + return *this; + } + + + + EventLog& + EventLog::clear() + { + string originalLogID = this->getID(); + return this->clear (originalLogID); + } + + /** purge log contents and also reset Header-ID + * @note actually we're starting a new log + * and let the previous one go away. + * @warning while this also unties any joined logs, + * other log front-ends might still hold onto + * the existing, combined log. Just we are + * detached and writing to a pristine log. + */ + EventLog& + EventLog::clear (string alteredLogID) + { + log_.reset (new Log); + log({"type=EventLogHeader", "this="+alteredLogID}); + return *this; + } + + EventLog& + EventLog::clear (const char* alteredLogID) + { + return clear (string{alteredLogID}); + } + + + + /* ==== Logging API ==== */ + + EventLog& + EventLog::event (string text) + { + log ("event", ArgSeq{}, ArgSeq{text}); // we use this ctor variant to ensure + return *this; // that text is not misinterpreted as attribute, + } // which might happen when text contains a '=' + + EventLog& + EventLog::event (string classifier, string text) + { + log ("event", ArgSeq{"ID="+classifier}, ArgSeq{text}); + return *this; + } + + EventLog& + EventLog::call (string target, string function) + { + return call(target, function, ArgSeq()); + } + + EventLog& + EventLog::call (string target, string function, ArgSeq&& args) + { + log ("call", ArgSeq{"fun="+function, "this="+target}, std::forward(args)); + return *this; + } + + EventLog& + EventLog::call (const char* target, const char* function, ArgSeq&& args) + { + return call (string(target), string(function), std::forward(args)); + } + + + EventLog& + EventLog::warn (string text) + { + log({"type=warn", text}); + return *this; + } + + EventLog& + EventLog::error (string text) + { + log({"type=error", text}); + return *this; + } + + EventLog& + EventLog::fatal (string text) + { + log({"type=fatal", text}); + return *this; + } + + + + EventLog& + EventLog::create (string text) + { + log({"type=create", text}); + return *this; + } + + EventLog& + EventLog::destroy (string text) + { + log({"type=destroy", text}); + return *this; + } + + + + + /* ==== Query/Verification API ==== */ + + EventMatch + EventLog::verify (string match) const + { + EventMatch matcher(*log_); + matcher.locate (match); // new matcher starts linear search from first log element + return matcher; + } + + + EventMatch + EventLog::verifyMatch (string regExp) const + { + EventMatch matcher(*log_); + matcher.locateMatch (regExp); + return matcher; + } + + + EventMatch + EventLog::verifyEvent (string match) const + { + EventMatch matcher(*log_); + matcher.locateEvent (match); + return matcher; + } + + + EventMatch + EventLog::verifyEvent (string classifier, string match) const + { + EventMatch matcher(*log_); + matcher.locateEvent (classifier, match); + return matcher; + } + + + EventMatch + EventLog::verifyCall (string match) const + { + EventMatch matcher(*log_); + matcher.locateCall (match); + return matcher; + } + + + /** + * @remarks the query expression is built similar to the other queries, + * but the logic of evaluation is flipped: whenever we find any match + * the overall result (when evaluating to `bool`) will be `false`. + * @warning this is not an proper exhaustive negation, since the implementation + * does not proper backtracking with a stack of choices. This becomes + * evident, when you combine `ensureNot()` with a switch in search + * direction, like e.g. using `afterXXX` at some point in the chain. + */ + EventMatch + EventLog::ensureNot (string match) const + { + EventMatch matcher(*log_); + matcher.look_for_match_ = false; // flip logic; fail if match succeeds + matcher.locate (match); + return matcher; + } + + + +}} // namespace lib::test diff --git a/src/lib/test/event-log.hpp b/src/lib/test/event-log.hpp index 8e63f37e3..c88042b71 100644 --- a/src/lib/test/event-log.hpp +++ b/src/lib/test/event-log.hpp @@ -27,11 +27,47 @@ ** the [EventLog] allows to record invocations and similar events. It is ** implemented as a "PImpl" to allow sharing of logs, which helps to trace ** events from transient UI elements and from destructor code. The front-end - ** used for access offers a query facility, so the test code may express some + ** used for access offers a query DSL, so the test code may express some ** expected patterns of incidence and verify match or non-match. ** + ** # Usage + ** + ** The EventLog just captures invocations in memory, as sequence of + ** [string records](\ref lib::diff::Record). The _logging API_ offers some + ** dedicated functions to record invocations, events, the creation and destruction + ** of objects, warnings and error messages. Data passed to those logging functions + ** is rendered into string, using Lumiera's generic string conversion helpers. + ** + ** To _verify and match_ on log contents, start an evaluation as a builder (DSL) + ** expression, starting from one of the #verify functions, or starting with #ensureNot, + ** to probe that a given set of matches does not occur in the log. These entrance + ** functions set up the initial search condition, and further search conditions + ** can be chained up by invoking suitable matching functions on the generated + ** EventMatch builder. Most notably, it is possible to search for some further + ** match condition _before_ or _after_ the position where the preceding condition + ** produced a match -- allowing to specify sequences and patterns of events to + ** expect in the log + ** \code{.cpp} + ** EventLog log("baked beans"); + ** log.event("spam"); + ** log.event("ham"); + ** + ** CHECK (log.verify("ham") + ** .after("beans") + ** .before("spam") + ** .before("ham")); + ** \endcode + ** In general, this kind of search with a switch of the search direction in the middle + ** of a match requires some backtracking to try possible matches until exhaustion. + ** ** Failure of match prints a detailed trace message to `STDERR`, in order ** to deliver a precise indication what part of the condition failed. + ** @note this sequence prints the matches succeeding _at the point_ where each + ** condition is added to the chain. Adding more conditions, especially when + ** combined with changed search direction, might lead to backtracking, which + ** happens silently within the search engine, without printing any further + ** diagnostics. This means: the sequence of matches you see in this diagnostic + ** output is not necessarily the last match patch, which lead to the final failure ** ** @see TestEventLog_test ** @see [usage example](\ref AbstractTangible_test) @@ -44,12 +80,12 @@ #include "lib/error.hpp" -#include "lib/idi/entry-id.hpp" -#include "lib/iter-adapter.hpp" #include "lib/iter-cursor.hpp" +#include "lib/iter-chain-search.hpp" #include "lib/format-util.hpp" #include "lib/format-cout.hpp" #include "lib/diff/record.hpp" +#include "lib/idi/genfunc.hpp" #include "lib/symbol.hpp" #include "lib/util.hpp" @@ -62,14 +98,22 @@ namespace lib { namespace test{ - namespace error = lumiera::error; - using util::stringify; - using util::contains; - using util::isnil; using lib::Symbol; using std::string; + using util::stringify; + namespace { + using Entry = lib::diff::Record; + using Log = std::vector; + + auto + buildSearchFilter(Log const& srcSeq) + { + using Cursor = lib::iter::CursorGear; + return iter::chainSearch (Cursor{srcSeq.begin(), srcSeq.end()}); + } + } /** @@ -83,10 +127,7 @@ namespace test{ */ class EventMatch { - using Entry = lib::diff::Record; - using Log = std::vector; - using Iter = lib::IterCursor; - using Filter = ExtensibleFilterIter; + using Filter = decltype( buildSearchFilter (std::declval()) ); using ArgSeq = lib::diff::RecordSetup::Storage; using RExSeq = std::vector; @@ -104,210 +145,14 @@ namespace test{ /** record when the underlying query has failed */ string violation_; - /** core of the evaluation machinery: - * apply a filter predicate and then pull - * through the log to find a acceptable entry - */ - bool - foundSolution() - { - return !isnil (solution_); - } - - /** this is actually called after each refinement of - * the filter and matching conditions. The effect is to search - * for an (intermediary) solution right away and to mark failure - * as soon as some condition can not be satisfied. Rationale is to - * indicate the point where a chained match fails - * @see ::operator bool() for extracting the result - * @param matchSpec diagnostics description of the predicate just being added - * @param rel indication of the searching relation / direction - */ - void - evaluateQuery (string matchSpec, Literal rel = "after") - { - if (look_for_match_ and not isnil (violation_)) return; - // already failed, no further check necessary - - if (foundSolution()) - { - lastMatch_ = matchSpec+" @ "+string(*solution_) - + (isnil(lastMatch_)? "" - : "\n.."+rel+" "+lastMatch_); - if (not look_for_match_) - violation_ = "FOUND at least "+lastMatch_; - } - else - { - if (look_for_match_) - violation_ = "FAILED to "+matchSpec - + "\n.."+rel - + " "+lastMatch_; - else - violation_ = ""; - } - } /** @internal for creating EventLog matchers */ - EventMatch(Log const& srcSeq) - : solution_(Iter(srcSeq)) - , lastMatch_("HEAD "+ solution_->get("this")) - , look_for_match_(true) - , violation_() - { } + EventMatch(Log const& srcSeq); friend class EventLog; - - /* == elementary matchers == */ - - auto - find (string match) - { - return [=](Entry const& entry) - { - return contains (string(entry), match); - }; - } - - auto - findRegExp (string regExpDef) - { - std::regex regExp(regExpDef); - return [=](Entry const& entry) - { - return std::regex_search(string(entry), regExp); - }; - } - - auto - findEvent (string match) - { - return [=](Entry const& entry) - { - return ( entry.getType() == "event" - or entry.getType() == "error" - or entry.getType() == "create" - or entry.getType() == "destroy" - or entry.getType() == "logJoin" - ) - and !isnil(entry.scope()) - and contains (*entry.scope(), match); - }; - } - - auto - findEvent (string classifier, string match) - { - return [=](Entry const& entry) - { - return ( entry.getType() == classifier - or (entry.hasAttribute("ID") and contains (entry.get("ID"), classifier)) - ) - and !isnil(entry.scope()) - and contains (*entry.scope(), match); - }; - } - - auto - findCall (string match) - { - return [=](Entry const& entry) - { - return entry.getType() == "call" - and contains (entry.get("fun"), match); - }; - } - - - /** this filter functor is for refinement of an existing filter - * @param argSeq perform a substring match consecutively - * for each of the log entry's arguments - * @note the match also fails, when the given log entry - * has more or less arguments, than the number of - * given match expressions in `argSeq` - * @see ExtensibleFilterIter::andFilter() - */ - auto - matchArguments (ArgSeq&& argSeq) - { - return [=](Entry const& entry) - { - auto scope = entry.scope(); - for (auto const& match : argSeq) - if (isnil (scope) or not contains(*scope, match)) - return false; - else - ++scope; - - return isnil(scope); // must be exhausted by now - }; // otherwise the sizes do not match... - } - - - /** refinement filter, to cover all arguments by regular expression(s) - * @param regExpSeq several regular expressions, which, when applied - * consecutively until exhaustion, must cover and verify _all_ - * arguments of the log entry. - * @remarks to explain, we "consume" arguments with a regExp from the list, - * and when this one doesn't match anymore, we try the next one. - * When we'ver tried all regular expressions, we must have also - * consumed all arguments, otherwise we fail. - */ - auto - matchArgsRegExp (RExSeq&& regExpSeq) - { - return [=](Entry const& entry) - { - auto scope = entry.scope(); - for (auto const& rex : regExpSeq) - { - if (isnil (scope)) return false; - while (scope and std::regex_search(*scope, rex)) - ++scope; - } - - return isnil(scope); // must be exhausted by now - }; // otherwise we didn't get full coverage... - } - - - /** refinement filter to match on the given typeID */ - auto - matchType (string typeID) - { - return [=](Entry const& entry) - { - return contains (entry.getType(), typeID); - }; - } - - - /** refinement filter to ensure a specific attribute is present on the log entry */ - auto - ensureAttribute (string key) - { - return [=](Entry const& entry) - { - return entry.hasAttribute(key); - }; - } - - - /** refinement filter to ensure a specific attribute is present on the log entry */ - auto - matchAttribute (string key, string valueMatch) - { - return [=](Entry const& entry) - { - return entry.hasAttribute(key) - and contains (entry.get(key), valueMatch); - }; - } - - public: /** final evaluation of the match query, * usually triggered from the unit test `CHECK()`. @@ -315,7 +160,7 @@ namespace test{ */ operator bool() const { - if (!isnil (violation_)) + if (not util::isnil (violation_)) { cerr << "__Log_condition_violated__\n"+violation_ <<"\n"; return false; @@ -324,141 +169,47 @@ namespace test{ } - /** - * find a match (substring match) of the given text - * in an EventLog entry after the current position - * @remarks the name of this junctor might seem counter intuitive; - * it was chosen due to expected DSL usage: `log.verify("α").before("β")`. - * Operationally this means first to find a Record matching the substring "α" - * and then to forward from this point until hitting a record to match "β". - */ - EventMatch& - before (string match) - { - solution_.underlying().switchForwards(); - solution_.setNewFilter(find(match)); - evaluateQuery ("match(\""+match+"\")"); - return *this; - } + /* query builders to continue search at current position */ - /** find a match with the given regular expression */ - EventMatch& - beforeMatch (string regExp) - { - solution_.underlying().switchForwards(); - solution_.setNewFilter(findRegExp(regExp)); - evaluateQuery ("find-RegExp(\""+regExp+"\")"); - return *this; - } + EventMatch& locate (string match); + EventMatch& locateMatch (string regExp); + EventMatch& locateEvent (string match); + EventMatch& locateEvent (string classifier, string match); + EventMatch& locateCall (string match); - /** find a match for an "event" _after_ the current point of reference - * @remarks the term "event" designates several types of entries, which - * typically capture something happening within the observed entity. - * Especially, the following [record types](\ref lib::Record::getType()) - * qualify as event: - * - `event` - * - `error` - * - `create` - * - `destroy` - * - `logJoin` - * @param match perform a substring match against the arguments of the event - * @see ::findEvent - */ - EventMatch& - beforeEvent (string match) - { - solution_.underlying().switchForwards(); - solution_.setNewFilter(findEvent(match)); - evaluateQuery ("match-event(\""+match+"\")"); - return *this; - } - EventMatch& - beforeEvent (string classifier, string match) - { - solution_.underlying().switchForwards(); - solution_.setNewFilter(findEvent(classifier,match)); - evaluateQuery ("match-event(ID=\""+classifier+"\", \""+match+"\")"); - return *this; - } + /* query builders to find a match stepping forwards */ - /** find a match for some function invocation _after_ the current point of reference - * @param match perform a substring match against the name of the function invoked - * @see ::findCall - */ - EventMatch& - beforeCall (string match) - { - solution_.underlying().switchForwards(); - solution_.setNewFilter(findCall(match)); - evaluateQuery ("match-call(\""+match+"\")"); - return *this; - } + EventMatch& before (string match); + EventMatch& beforeMatch (string regExp); + EventMatch& beforeEvent (string match); + EventMatch& beforeEvent (string classifier, string match); + EventMatch& beforeCall (string match); - EventMatch& - after (string match) - { - solution_.underlying().switchBackwards(); - solution_.setNewFilter(find(match)); - evaluateQuery ("match(\""+match+"\")", "before"); - return *this; - } - EventMatch& - afterMatch (string regExp) - { - solution_.underlying().switchBackwards(); - solution_.setNewFilter(findRegExp(regExp)); - evaluateQuery ("find-RegExp(\""+regExp+"\")", "before"); - return *this; - } + /* query builders to find a match stepping backwards */ - EventMatch& - afterEvent (string match) - { - solution_.underlying().switchBackwards(); - solution_.setNewFilter(findEvent(match)); - evaluateQuery ("match-event(\""+match+"\")", "before"); - return *this; - } - - EventMatch& - afterEvent (string classifier, string match) - { - solution_.underlying().switchBackwards(); - solution_.setNewFilter(findEvent(classifier,match)); - evaluateQuery ("match-event(ID=\""+classifier+"\", \""+match+"\")", "before"); - return *this; - } - - /** find a function invocation backwards, before the current point of reference */ - EventMatch& - afterCall (string match) - { - solution_.underlying().switchBackwards(); - solution_.setNewFilter(findCall(match)); - evaluateQuery ("match-call(\""+match+"\")", "before"); - return *this; - } + EventMatch& after (string match); + EventMatch& afterMatch (string regExp); + EventMatch& afterEvent (string match); + EventMatch& afterEvent (string classifier, string match); + EventMatch& afterCall (string match); /** refine filter to additionally require specific arguments * @remarks the refined filter works on each record qualified by the * query expression established thus far; it additionally * looks into the arguments (children list) of the log entry. - * @warning match is processed by comparision of string representation. + * @warning match is processed by comparison of string representation. */ template EventMatch& arg (ARGS const& ...args) { - ArgSeq argSeq(stringify (args...)); - string argList(util::join(argSeq)); - - solution_.andFilter (matchArguments(move(argSeq))); - evaluateQuery ("match-arguments("+argList+")"); + refineSerach_matchArguments (stringify (args...)); return *this; } + /** refine filter to additionally cover all arguments * with a series of regular expressions. * @note For this match to succeed, any arguments of the log entry @@ -473,79 +224,44 @@ namespace test{ EventMatch& argMatch (ARGS const& ...regExps) { - solution_.andFilter (matchArgsRegExp (stringify (regExps...))); - evaluateQuery ("match-args-RegExp("+util::join(stringify(regExps...))+")"); + refineSerach_matchArgsRegExp (stringify (regExps...), + util::join(stringify(regExps...))); return *this; } - /** refine filter to additionally require a matching log entry type */ - EventMatch& - type (string typeID) - { - solution_.andFilter (matchType(typeID)); - evaluateQuery ("match-type("+typeID+")"); - return *this; - } - /** refine filter to additionally require the presence an attribute */ - EventMatch& - key (string key) - { - solution_.andFilter (ensureAttribute(key)); - evaluateQuery ("ensure-attribute("+key+")"); - return *this; - } - /** refine filter to additionally match on a specific attribute */ - EventMatch& - attrib (string key, string valueMatch) - { - solution_.andFilter (matchAttribute(key,valueMatch)); - evaluateQuery ("match-attribute("+key+"=\""+valueMatch+"\")"); - return *this; - } + /* query builders to augment and refine the currently defined search condition*/ - /** refine filter to additionally match on the ID attribute */ - EventMatch& - id (string classifier) - { - solution_.andFilter (matchAttribute("ID",classifier)); - evaluateQuery ("match-ID(\""+classifier+"\")"); - return *this; - } - - /** refine filter to additionally match the `'this'` attribute */ - EventMatch& - on (string targetID) - { - solution_.andFilter (matchAttribute("this",targetID)); - evaluateQuery ("match-this(\""+targetID+"\")"); - return *this; - } - - EventMatch& - on (const char* targetID) - { - solution_.andFilter (matchAttribute("this",targetID)); - evaluateQuery ("match-this(\""+string(targetID)+"\")"); - return *this; - } + EventMatch& type (string typeID); + EventMatch& key (string key); + EventMatch& attrib (string key, string valueMatch); + EventMatch& id (string classifier); + EventMatch& on (string targetID); + EventMatch& on (const char* targetID); template - EventMatch& - on (const X *const targetObj) + EventMatch& on (const X *const targetObj) { string targetID = idi::instanceTypeID (targetObj); - solution_.andFilter (matchAttribute("this",targetID)); - evaluateQuery ("match-this(\""+targetID+"\")"); - return *this; + return this->on(targetID); } + + private: + bool foundSolution(); + void evaluateQuery (string matchSpec, Literal rel = "after"); + + void refineSerach_matchArguments (ArgSeq&& argSeq); + void refineSerach_matchArgsRegExp (RExSeq&& regExpSeq, string rendered_regExps); }; + + + /****************************************************************//** * Helper to log and verify the occurrence of events. * The EventLog object is a front-end handle, logging flexible @@ -556,9 +272,6 @@ namespace test{ */ class EventLog { - using Entry = lib::diff::Record; - using Log = std::vector; - using Iter = lib::RangeIter; std::shared_ptr log_; @@ -586,11 +299,7 @@ namespace test{ public: explicit - EventLog (string logID) - : log_(new Log) - { - log({"type=EventLogHeader", "this="+logID}); - } + EventLog (string logID); explicit EventLog (const char* logID) @@ -610,65 +319,15 @@ namespace test{ /** Merge this log into another log, forming a combined log * @param otherLog target to integrate this log's contents into. * @return reference to the merged new log instance - * @remarks EventLog uses a heap based, sharable log storage, - * where the EventLog object is just a front-end (shared ptr). - * The `joinInto` operation both integrates this logs contents - * into the other log, and then disconnects from the old storage - * and connects to the storage of the combined log. - * @warning beware of clone copies. Since copying EventLog is always a - * shallow copy, all copied handles actually refer to the same - * log storage. If you invoke `joinInto` in such a situation, - * only the current EventLog front-end handle will be rewritten - * to point to the combined log, while any other clone will - * continue to point to the original log storage. * @see TestEventLog_test::verify_logJoining() */ - EventLog& - joinInto (EventLog& otherLog) - { - Log& target = *otherLog.log_; - target.reserve (target.size() + log_->size() + 1); - target.emplace_back (log_->front()); - auto p = log_->begin(); - while (++p != log_->end()) // move our log's content into the other log - target.emplace_back(std::move(*p)); - this->log_->resize(1); - this->log({"type=joined", otherLog.getID()}); // leave a tag to indicate - otherLog.log({"type=logJoin", this->getID()}); // where the `joinInto` took place, - this->log_ = otherLog.log_; // connect this to the other storage - return *this; - } + EventLog& joinInto (EventLog& otherLog); /** purge log contents while retaining just the original Header-ID */ - EventLog& - clear() - { - string originalLogID = this->getID(); - return this->clear (originalLogID); - } - - /** purge log contents and also reset Header-ID - * @note actually we're starting a new log - * and let the previous one go away. - * @warning while this also unties any joined logs, - * other log front-ends might still hold onto - * the existing, combined log. Just we are - * detached and writing to a pristine log. - */ - EventLog& - clear (string alteredLogID) - { - log_.reset (new Log); - log({"type=EventLogHeader", "this="+alteredLogID}); - return *this; - } - - EventLog& - clear (const char* alteredLogID) - { - return clear (string{alteredLogID}); - } + EventLog& clear(); + EventLog& clear (string alteredLogID); + EventLog& clear (const char* alteredLogID); template EventLog& @@ -683,47 +342,25 @@ namespace test{ using ArgSeq = lib::diff::RecordSetup::Storage; - EventLog& - event (string text) - { - log ("event", ArgSeq{}, ArgSeq{text}); // we use this ctor variant to ensure - return *this; // that text is not misinterpreted as attribute, - } // which might happen when text contains a '=' + /** log some text as event */ + EventLog& event (string text); /** log some event, with additional ID or classifier * @param classifier info to be saved into the `ID` attribute * @param text actual payload info, to be logged as argument */ - EventLog& - event (string classifier, string text) - { - log ("event", ArgSeq{"ID="+classifier}, ArgSeq{text}); - return *this; - } + EventLog& event (string classifier, string text); /** Log occurrence of a function call with no arguments. * @param target the object or scope on which the function is invoked * @param function name of the function being invoked */ - EventLog& - call (string target, string function) - { - return call(target, function, ArgSeq()); - } + EventLog& call (string target, string function); /** Log a function call with a sequence of stringified arguments */ - EventLog& - call (string target, string function, ArgSeq&& args) - { - log ("call", ArgSeq{"fun="+function, "this="+target}, std::forward(args)); - return *this; - } + EventLog& call (string target, string function, ArgSeq&& args); - EventLog& - call (const char* target, const char* function, ArgSeq&& args) - { - return call (string(target), string(function), std::forward(args)); - } + EventLog& call (const char* target, const char* function, ArgSeq&& args); /** Log a function call with arbitrary arguments */ template @@ -752,56 +389,29 @@ namespace test{ EventLog& note (ELMS const& ...initialiser) { - log_->emplace_back(stringify (initialiser...)); + log_->emplace_back (stringify (initialiser...)); return *this; } /** Log a warning entry */ - EventLog& - warn (string text) - { - log({"type=warn", text}); - return *this; - } - + EventLog& warn (string text); /** Log an error note */ - EventLog& - error (string text) - { - log({"type=error", text}); - return *this; - } - + EventLog& error (string text); /** Log a fatal failure */ - EventLog& - fatal (string text) - { - log({"type=fatal", text}); - return *this; - } - + EventLog& fatal (string text); /** Log the creation of an object. * Such an entry can be [matched as event](\ref ::verifyEvent) */ - EventLog& - create (string text) - { - log({"type=create", text}); - return *this; - } + EventLog& create (string text); /** Log the destruction of an object. * Can be matched as event. */ - EventLog& - destroy (string text) - { - log({"type=destroy", text}); - return *this; - } + EventLog& destroy (string text); + @@ -814,6 +424,8 @@ namespace test{ } + using Iter = lib::RangeIter; + typedef Iter const_iterator; typedef const Entry value_type; @@ -832,16 +444,11 @@ namespace test{ * The resulting matcher object will qualify on any log entry * containing the given string. By adding subsequent further * query expressions on the returned [matcher object](\ref EventMatch), - * the query can be refined. Refining a query might induce backtracking. - * The final result can be retrieved by `bool` conversion + * the query can be refined. Moreover it is possible to chain up further + * search queries, which will be executing starting from the position of the + * previous match. The final result can be retrieved by `bool` conversion */ - EventMatch - verify (string match) const - { - EventMatch matcher(*log_); - matcher.before (match); // "the start of the log is before the match" - return matcher; - } + EventMatch verify (string match) const; /** start a query to match with a regular expression * @param regExp definition @@ -849,13 +456,7 @@ namespace test{ * `string` representation of the log entries. * Meaning, it can also match type and attributes */ - EventMatch - verifyMatch (string regExp) const - { - EventMatch matcher(*log_); - matcher.beforeMatch (regExp); - return matcher; - } + EventMatch verifyMatch (string regExp) const; /** start a query to match for some event. * @remarks only a subset of all log entries is treated as "event", @@ -864,21 +465,12 @@ namespace test{ * creation and destruction of objects count as "event". * @param match text to (substring)match against the argument logged as event */ - EventMatch - verifyEvent (string match) const - { - EventMatch matcher(*log_); - matcher.beforeEvent (match); - return matcher; - } + EventMatch verifyEvent (string match) const; - EventMatch - verifyEvent (string classifier, string match) const - { - EventMatch matcher(*log_); - matcher.beforeEvent (classifier, match); - return matcher; - } + /** start a query to match for an specific kind of element + * @param classifier select kind of event by match on type or ID + */ + EventMatch verifyEvent (string classifier, string match) const; template EventMatch @@ -890,27 +482,13 @@ namespace test{ /** start a query to match especially a function call * @param match text to match against the function name */ - EventMatch - verifyCall (string match) const - { - EventMatch matcher(*log_); - matcher.beforeCall (match); - return matcher; - } + EventMatch verifyCall (string match) const; - /** start a query to ensure the given expression does \em not match. - * @remarks the query expression is built similar to the other queries, - * but the logic of evaluation is flipped: whenever we find any match - * the overall result (when evaluating to `bool`) will be `false`. - */ + /** start a query to ensure the given expression does _not_ match. */ EventMatch - ensureNot (string match) const - { - EventMatch matcher(*log_); - matcher.look_for_match_ = false; // flip logic; fail if match succeeds - matcher.before (match); - return matcher; - } + ensureNot (string match) const; + + /** equality comparison is based on the actual log contents */ diff --git a/src/lib/test/test-helper.hpp b/src/lib/test/test-helper.hpp index bb4141efc..dc5325dfe 100644 --- a/src/lib/test/test-helper.hpp +++ b/src/lib/test/test-helper.hpp @@ -147,6 +147,13 @@ namespace test{ static_assert (not sizeof(X), "### Type Debugging ###"); }; + template + void + typeDebugger(X&& x) + { + static_assert (not sizeof(X), "### Type Debugging ###"); + } + namespace { // helper for printing type diagnostics diff --git a/src/lib/util.cpp b/src/lib/util.cpp index 7ab8dfe67..bf192f190 100644 --- a/src/lib/util.cpp +++ b/src/lib/util.cpp @@ -29,7 +29,9 @@ */ +#include "error.hpp" #include "lib/util.hpp" +#include "lib/format-string.hpp" #include #include @@ -40,16 +42,22 @@ using boost::algorithm::is_any_of; using boost::algorithm::is_alnum; using boost::algorithm::is_space; +#include + +using std::regex; +using std::regex_match; + +using std::function; +using util::_Fmt; namespace util { - using std::function; - typedef function ChPredicate; + using ChPredicate = function; ChPredicate operator! (ChPredicate p) { return not bind(p,_1); } // character classes used for sanitising a string - ChPredicate isValid (is_alnum() or is_any_of("-_.+$'()@")); ///< characters to be retained + ChPredicate isValid (is_alnum() or is_any_of("-_.+$()@")); ///< characters to be retained ChPredicate isPunct (is_space() or is_any_of(",;:#*~´`?\\=/&%![]{}<>")); ///< punctuation to be replaced by '_' @@ -63,12 +71,12 @@ namespace util { while ( i != e ) { while ( i != e && !isValid (*i) ) ++i; - while ( i != e && isValid (*i) ) *(j++) = *(i++); - if ( i != e && isPunct (*i) ) + while ( i != e && isValid (*i) ) *(j++) = *(i++); + if ( i != e && isPunct (*i) ) { *j++ = '_'; do ++i; - while ( i != e && isPunct (*i)); + while ( i != e && isPunct (*i)); } } res.erase(j,res.end()); @@ -79,7 +87,7 @@ namespace util { /** * @remarks this function just forwards to boost::algorithm::trim_copy. * Use this call when boost header inclusion is an issue, otherwise - * a direct invocation is likely to perform better, due to inlining. + * a direct invocation is likely to perform better, due to inlining. */ string trim (string const& org) @@ -87,6 +95,29 @@ namespace util { return boost::algorithm::trim_copy (org); } + + + + namespace { + regex trueTokens{ "\\s*(true|True|TRUE|yes|Yes|YES|1|\\+)\\s*", regex::ECMAScript | regex::optimize}; + regex falseTokens{"\\s*(false|False|FALSE|no|No|NO|0|\\-)\\s*", regex::ECMAScript | regex::optimize}; + } + + bool + boolVal (string const& textForm) + { + if (regex_match (textForm, trueTokens)) return true; + if (regex_match (textForm, falseTokens)) return false; + throw lumiera::error::Invalid(_Fmt{"String '%s' can not be interpreted as bool value"} % textForm); + } + + + bool + isYes (string const& textForm) noexcept + { + return regex_match (textForm, trueTokens); + } + } // namespace util diff --git a/src/lib/util.hpp b/src/lib/util.hpp index 4d35ff019..493c25caa 100644 --- a/src/lib/util.hpp +++ b/src/lib/util.hpp @@ -336,8 +336,8 @@ namespace util { inline bool isSameObject (A const& a, B const& b) { - return static_cast (&a) - == static_cast (&b); + return static_cast (std::addressof(a)) + == static_cast (std::addressof(b)); } @@ -350,17 +350,17 @@ namespace util { * * @par Example Conversions \verbatim - "Word" --> 'Word' - "a Sentence" --> 'a_Sentence' - "trailing Withespace \t \n" --> 'trailing_Withespace' - "with a lot \nof Whitespace" --> 'with_a_lot_of_Whitespace' - "with\"much (punctuation)[]!" --> 'withmuch_(punctuation)' - "§&Ω%€ leading garbage" --> 'leading_garbage' - "mixed Ω garbage" --> 'mixed_garbage' - "Bääääh!!" --> 'Bh' + "Word" --> "Word" + "a Sentence" --> "a_Sentence" + "trailing Withespace \t \n" --> "trailing_Withespace" + "with a lot \nof Whitespace" --> "with_a_lot_of_Whitespace" + "@with\".\'much ($punctuation)[]!" --> "@with.much_($punctuation)" + "§&Ω%€ leading garbage" --> "leading_garbage" + "mixed Ω garbage" --> "mixed_garbage" + "Bääääh!!" --> "Bh" \endverbatim - * @see sanitised-identifier-test.cpp - * @see lib::meta::sanitisedSymbol() + * @see \ref UtilSanitizedIdentifier_test + * @see \ref lib::meta::sanitisedSymbol() */ string sanitise (string const& org); @@ -371,6 +371,23 @@ namespace util { string trim (string const& org); + /** interpret text representation of a boolean value. + * @remarks this function detects the relevant token rather strict.... + * - yields `true` for the tokens "true", "True", "TRUE", "yes", "Yes", "YES", "1", "+" + * - yields `false` for the tokens "false", "False", "FALSE", "no", "No, "NO", "0", "-" + * - leading and trailing whitespace is ignored + * @throws lumiera::error::Invalid for any other text content + */ + bool boolVal(string const&); + + + /** check the given text if it can be interpreted as affirmative answer (bool `true`). + * @remarks this function just fishes for the known `true` tokens and interprets + * all other content as `false`, including empty strings. Never throws. + */ + bool isYes(string const&) noexcept; + + /** convenience shortcut: conversion to c-String via string. diff --git a/src/lib/variant.hpp b/src/lib/variant.hpp index 87621e14b..fe7720055 100644 --- a/src/lib/variant.hpp +++ b/src/lib/variant.hpp @@ -341,15 +341,16 @@ namespace lib { + static string indicateTypeMismatch (Buffer&); + + static Buff& downcast (Buffer& b) { Buff* buff = dynamic_cast (&b); if (!buff) - throw error::Logic("Variant type mismatch: " - "the given variant record does not hold " - "a value of the type requested here" + throw error::Logic(indicateTypeMismatch(b) ,error::LERR_(WRONG_TYPE)); else return *buff; @@ -550,6 +551,27 @@ namespace lib { return util::typedString (this->access()); } + /** + * error message when accessing the variant content with wrong type assumptions. + * @remark while this diagnostics can be crucial for finding bugs, we avoid + * including \ref format-string.hpp, since lib::Variant is used pervasively + * as part of lib::diff::GenNode. Especially in development builds, we observed + * a tangible leverage on executable size. Thus we implement the protection + * against follow-up exceptions explicitly here. + */ + template + template + inline string + Variant::Buff::indicateTypeMismatch(Variant::Buffer& target) + { + try { + return "Variant type mismatch: expected value of type «" + + lib::meta::typeStr()+"», " + + "however the given variant record is " + + string{target}; + } + catch(...) { return lib::meta::FAILURE_INDICATOR; } + } }// namespace lib diff --git a/src/lib/wrapper.hpp b/src/lib/wrapper.hpp index aa66db2fe..363274fb6 100644 --- a/src/lib/wrapper.hpp +++ b/src/lib/wrapper.hpp @@ -185,11 +185,13 @@ namespace wrapper { explicit ItemWrapper(TY const& o) + : created_(false) { build (o); } explicit ItemWrapper(TY && ro) + : created_(false) { build (std::move(ro)); } diff --git a/src/lib/zombie-check.hpp b/src/lib/zombie-check.hpp index 81ffa4fff..b4f005d7e 100644 --- a/src/lib/zombie-check.hpp +++ b/src/lib/zombie-check.hpp @@ -20,7 +20,7 @@ */ -/** @file dependable-base.hpp +/** @file zombie-check.hpp ** Detector to set off alarm when (re)using deceased objects. ** When implementing services based on static fields or objects, ** an invocation after static shutdown can not be precluded -- be it by @@ -30,6 +30,7 @@ ** we may plant an automatic "zombie detector" to give a clear indication of ** such a policy violation (Lumiera forbids to use dependencies from dtors). ** + ** @see ZombieCheck_test ** @see sync-classlock.hpp ** @see depend.hpp */ @@ -40,20 +41,38 @@ #include "lib/error.hpp" +#include +#include + namespace lib { namespace error = lumiera::error; + using std::string; + /** * Automatic lifecycle tracker, to produce an alarm when accessing objects after deletion. + * @warning ensure the ZombieCheck instance lives in static memory, otherwise it won't work. */ class ZombieCheck { bool deceased_ = false; + char zombieID_[42]{}; public: + /** + * install a zombie check, tagged with the given id. + * When invoked after death, the raised error::Fatal + * includes this ID in the diagnostic message. + * @remark recommended to use util::typeStr + */ + ZombieCheck (string id) + { + std::strncpy(zombieID_, id.c_str(), 41); + } + ZombieCheck() = default; ~ZombieCheck() { @@ -69,10 +88,20 @@ namespace lib { operator() () const { if (deceased_) - throw error::Fatal("Already deceased object called out of order during Application shutdown. " - "Lumiera Policy violated: Dependencies must not be used from destructors." + throw error::Fatal(buildDiagnosticMessage() ,error::LUMIERA_ERROR_LIFECYCLE); } + + private: + string + buildDiagnosticMessage() const + { + string msg{"Already deceased object called out of order during Application shutdown. " + "Lumiera Policy violated: Dependencies must not be used from destructors."}; + if (zombieID_[0]) + msg += " Offender = "+string{zombieID_}; + return msg; + } }; diff --git a/src/proc/asset.hpp b/src/proc/asset.hpp index 5e1cec4c2..58b456a30 100644 --- a/src/proc/asset.hpp +++ b/src/proc/asset.hpp @@ -101,6 +101,8 @@ namespace asset { * @see AssetManager#getID generating ID values * @see asset::Media * @see ID + * + * @todo it would be desirable to use EntryID instead ///////////////////////////////////////////TICKET #739 : make identification schemes compatible */ template class ID @@ -345,7 +347,7 @@ namespace asset { /** marker constant denoting a NIL asset */ template - ID ID::INVALID = ID(0); + ID ID::INVALID = ID{0}; }} // namespace proc::asset diff --git a/src/proc/asset/category.hpp b/src/proc/asset/category.hpp index ce6028921..aca63b08f 100644 --- a/src/proc/asset/category.hpp +++ b/src/proc/asset/category.hpp @@ -57,7 +57,7 @@ namespace asset { , VIDEO , EFFECT , CODEC - , STRUCT + , STRUCT //////////////////////////////////TICKET #1156 : do we need the distinction between STRUCT and META? , META }; diff --git a/src/proc/asset/meta.cpp b/src/proc/asset/meta.cpp index 927ef2a1c..b30eb1590 100644 --- a/src/proc/asset/meta.cpp +++ b/src/proc/asset/meta.cpp @@ -96,9 +96,7 @@ namespace asset { /**************************************************/ #include "proc/asset/meta/time-grid.hpp" -//#include "proc/asset/procpatt.hpp" -//#include "proc/asset/timeline.hpp" -//#include "proc/asset/sequence.hpp" +#include "proc/asset/meta/error-log.hpp" namespace proc { @@ -109,7 +107,11 @@ namespace asset { using meta::TimeGrid; template Builder MetaFactory::operator() (EntryID); - template Builder MetaFactory::operator() (Descriptor const&, EntryID); + using meta::ErrorLog; + + template Builder MetaFactory::operator() (EntryID); + template Builder MetaFactory::operator() (Descriptor const&, EntryID); + }} // namespace proc::asset diff --git a/src/proc/asset/meta.hpp b/src/proc/asset/meta.hpp index f701a665f..245d13da9 100644 --- a/src/proc/asset/meta.hpp +++ b/src/proc/asset/meta.hpp @@ -28,13 +28,14 @@ ** works with. Lumiera exposes this self-referential control and customisation ** aspects as a special kind of Asset. Examples being types, scales and quantisation ** grids, decision rules, control data stores (automation data), annotations attached - ** to labels, inventory entities etc. + ** to labels, inventory entities etc. ** - ** For the different Kinds of Assets, we use sub-interfaces inheriting + ** For the different _Kinds_ of Assets, we use sub-interfaces inheriting ** from the general Asset interface. To be able to get asset::Meta instances ** directly from the AssetManager, we define a specialisation of the Asset ID. ** - ** \par using meta assets + ** ## using meta assets + ** ** The usage pattern of asset::Meta entities differs from the other assets, ** insofar they aren't created as individual entries, rather added as part ** of a larger scale configuration activity, or they are derived from category. @@ -47,7 +48,7 @@ ** They are created from a descriptor, which stands for a category or sub-category ** and can be another already existing asset::Meta (inheriting from meta::Descriptor) ** Generally this yields a Builder object, which can be used for outfitting the new - ** or changed metadata entry, finally \em committing this builder to yield a new + ** or changed metadata entry, finally *committing* this builder to yield a new ** asset::Meta (which, in case of a mutation, might supersede an existing one). ** ** @ingroup asset @@ -60,6 +61,7 @@ #define ASSET_META_H #include "proc/asset.hpp" +#include "proc/asset/struct-scheme.hpp" #include "lib/idi/entry-id.hpp" #include "lib/nocopy.hpp" @@ -97,9 +99,9 @@ namespace asset { * thus, on creation or when changing / superseding a * meta asset, the client gets a special builder instance, * which is a value object for configuring the specific details - * to set. When done, the client invokes a \c commit() function, + * to set. When done, the client invokes a `commit()` function, * which yields a smart-ptr to the new meta asset. - * Individual meta asset subclasses are bound to define a + * Individual meta asset subclasses are bound to define a * specialisation of this Builder template, which will then be * instantiated and provided by the MetaFactory. */ @@ -111,6 +113,7 @@ namespace asset { /** * key abstraction: metadata, parametrisation, customisation and similar organisational traits. * @todo just a stub, still have to figure out the distinctive properties of asset::Meta + * //////////////////////////////////TICKET #1156 : do we need the distinction between STRUCT and META? */ class Meta : public Asset @@ -120,13 +123,20 @@ namespace asset { static MetaFactory create; /** @return ID of kind Meta */ - virtual const ID& getID() const - { + virtual const ID& getID() const + { return static_cast& > (Asset::getID()); } protected: - Meta (Asset::Ident const& idi) : Asset(idi) {} //////////////TODO + Meta (Asset::Ident const& idi) + : Asset{idi} + { } + + template + Meta (lib::idi::EntryID const& nameID) + : Meta{idi::getAssetIdent (nameID, META)} + { } // friend class MetaFactory; ///////////////////////////////////TODO still necessary? }; @@ -142,9 +152,9 @@ namespace asset { - /** + /** * Factory specialised for creating Metadata Asset objects. - */ + */ class MetaFactory : util::NonCopyable { diff --git a/src/proc/asset/meta/error-log.cpp b/src/proc/asset/meta/error-log.cpp new file mode 100644 index 000000000..60639a7f2 --- /dev/null +++ b/src/proc/asset/meta/error-log.cpp @@ -0,0 +1,98 @@ +/* + ErrorLog - Entity to collect and persist incident records + + Copyright (C) Lumiera.org + 2018, 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 error-log.cpp + ** Implementation details of incident logging and persistence. + */ + + +#include "proc/asset/meta/error-log.hpp" +#include "proc/asset/struct-scheme.hpp" +#include "proc/assetmanager.hpp" +//#include "lib/time/timevalue.hpp" +//#include "lib/format-string.hpp" +//#include "lib/util.hpp" + +//#include + +//using util::_Fmt; +//using util::cStr; +//using util::isnil; +//using std::string; +using std::dynamic_pointer_cast; + + +namespace proc { +namespace asset{ +namespace meta { + + namespace error = lumiera::error; + + + /** storage for an unique, globally known ID. + * Used to address the corresponding ErrorLogView in the UI + * @todo 8/2018 for now this is a mere placeholder, but someone + * need to build a singleton asset to incorporate this eventually ///////////////////////////////////TICKET #1157 + */ + LogID theErrorLog_ID{"global_ErrorLog"}; + + + /** */ + ErrorLog::ErrorLog (LogID const& nameID) + : Meta{nameID} + { } + + + PLog + ErrorLog::global() + { + Asset::Ident ident = asset::idi::getAssetIdent (theErrorLog_ID, META); + ID globalLogID = AssetManager::instance().getID(ident); + /////////////////////////////////////////////////////////////////////////////////TICKET #739 sort out this mess with asset::ID vs EntryID + + if (not AssetManager::instance().known (globalLogID)) + return asset::Meta::create (theErrorLog_ID) + .commit(); + else + return dynamic_pointer_cast (AssetManager::instance().getAsset (globalLogID)); + } + +// using lib::time::Time; +// using lib::time::TimeValue; + + + /** Setup of an ErrorLog: validate the settings within this builder instance, + * then create an appropriately configured ErrorLog instance. + * @return shared_ptr holding onto the new asset::Meta, which has already been + * registered with the AssetManager. + * @todo currently (8/2018) this is a mere placeholder, we just need an EntryID. ///////////////TICKET #1157 + */ + lib::P + Builder::commit() + { + ASSERT (nameID == theErrorLog_ID.getSym(), "only the single global Error Log is implemented for now");///TICKET #1157 : and the entity created here is bare of any functionality + return AssetManager::wrap (*new ErrorLog{theErrorLog_ID}); + } + + +}}} // namespace asset::meta diff --git a/src/proc/asset/meta/error-log.hpp b/src/proc/asset/meta/error-log.hpp new file mode 100644 index 000000000..cd856528e --- /dev/null +++ b/src/proc/asset/meta/error-log.hpp @@ -0,0 +1,113 @@ +/* + ERROR-LOG.hpp - Entity to collect and persist incident records + + Copyright (C) Lumiera.org + 2018, 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 error-log.hpp + ** An entity to collect, possibly filter and persist incident records. + ** + ** @todo 8/2018 created as mere placeholder; for now we just need an EntryID + ** in order to mark the corresponding receiver widget in the UI. The idea is eventually + ** to persist relevant messages, filtering them out as time passes. Such an incident log + ** would be part of the session model, thus replicating its contents into the corresponding + ** gui::widget::ErrorLogView -- which displays notifications without blocking the UI. + ** + ** @see MetaFactory creating concrete asset::Meta instances + ** @see gui::ctrl::NotificationHub corresponding UI controller + ** @see gui::interact::Wizard information service in the UI + ** + */ + +#ifndef ASSET_META_ERROR_LOG_H +#define ASSET_META_ERROR_LOG_H + +#include "proc/asset/meta.hpp" +#include "lib/idi/entry-id.hpp" +//#include "lib/time/timevalue.hpp" +//#include "lib/symbol.hpp" + +#include + + + +namespace proc { +namespace asset{ +namespace meta { + + using std::string; +// using lib::Symbol; +// using lib::time::Time; +// using lib::time::TimeVar; +// using lib::time::TimeValue; + + + class ErrorLog; + using PLog = lib::P; + using LogID = lib::idi::EntryID; + + extern LogID theErrorLog_ID; + + + /** + * Receive, collect, filter and possibly persist incident records. + * @todo 8/2018 mere placeholder type for now, to allow defining an EntyID. + * We conceptually need "the" ErrorLog entity as correspondence to the ErrorLogView in the GUI. + */ + class ErrorLog + : public Meta + { + + public: + /** retrieve (possibly create) the global singleton asset + * corresponding to "the" global error log, as shown in the UI. + */ + static PLog global(); + + protected: + ErrorLog (LogID const&); + + friend class Builder; + }; + + + + + + template<> + struct Builder + { + string nameID; + /** + * @todo what is "the default log"?? + */ + Builder(string name) + : nameID{name} + { } + + /** create a specific error log + * configured as defined through this builder + */ + lib::P commit(); + + }; + +}}} // namespace proc::asset::meta +#endif /*ASSET_META_ERROR_LOG_H*/ diff --git a/src/proc/asset/meta/time-grid.cpp b/src/proc/asset/meta/time-grid.cpp index 4c374e573..52b000553 100644 --- a/src/proc/asset/meta/time-grid.cpp +++ b/src/proc/asset/meta/time-grid.cpp @@ -51,9 +51,12 @@ namespace meta { - /** */ + /** + * @internal to be invoked indirectly by means of the Builder. + * @todo drafted in 2010 -- usage of the builder feels unsatisfactory as of 2018 + */ TimeGrid::TimeGrid (GridID const& nameID) - : Meta (idi::getAssetIdent (nameID)) + : Meta{nameID} { } @@ -76,7 +79,7 @@ namespace meta { /** @internal helper to retrieve the smart-ptr * from the AssetManager, then attach a further * smart-ptr-to-Quantiser to that, which then can be - * published via the \link advice.hpp "advice system"\endlink + * published via the [advice system](\ref advice.hpp) * @note this allows to use a time grid just "by name", * without explicit dependence to the Session / Assets */ @@ -142,19 +145,19 @@ namespace meta { lib::P Builder::commit() { - if (predecessor_) + if (predecessor) throw error::Invalid("compound and variable time grids are a planned feature" , error::LUMIERA_ERROR_UNIMPLEMENTED); - ENSURE (fps_, "infinite grid should have been detected by FrameRate ctor"); + ENSURE (fps, "infinite grid should have been detected by FrameRate ctor"); - if (isnil (id_)) + if (isnil (id)) { _Fmt gridIdFormat("grid(%f_%d)"); - id_ = string(gridIdFormat % fps_ % _raw(origin_)); + id = string(gridIdFormat % fps % _raw(origin)); } - GridID nameID (id_); + GridID nameID (id); - return publishWrapped (*new SimpleTimeGrid(origin_, fps_, nameID)); + return publishWrapped (*new SimpleTimeGrid(origin, fps, nameID)); } @@ -172,8 +175,8 @@ namespace meta { { string name(gridID); Builder spec(name); - spec.fps_ = frames_per_second; - spec.origin_ = origin; + spec.fps = frames_per_second; + spec.origin = origin; return spec.commit(); } diff --git a/src/proc/asset/meta/time-grid.hpp b/src/proc/asset/meta/time-grid.hpp index 2cf593edb..dd341b5b6 100644 --- a/src/proc/asset/meta/time-grid.hpp +++ b/src/proc/asset/meta/time-grid.hpp @@ -22,7 +22,7 @@ /** @file time-grid.hpp - ** Establishing a reference scale for quantised time values. + ** To establish a reference scale for quantised time values. ** Contrary to the continuous Time values (Lumiera internal time scale), ** quantised time can only take on some discrete values corresponding ** to blocks or frames, according to the respective \em timecode format. @@ -34,7 +34,8 @@ ** Time grid specifications are integrated into Lumiera's framework ** for meta assets, automation, configuration and similar metadata. ** - ** \par using time grids + ** ## using time grids + ** ** TimeGrid is an interface (ABC), but provides some actual factory ** functions, which can be used as a convenience shortcut to fabricate ** the kind of simple time grid used most often. @@ -103,17 +104,17 @@ namespace meta { template<> struct Builder { - string id_; + string id; - FrameRate fps_; - TimeVar origin_; + FrameRate fps; + TimeVar origin; /** when building a compound or variable grid, - * the predecessor is the grid active \em before + * the predecessor is the grid active _before_ * the origin of this (local) grid. * @todo currently not supported (as of 12/2010) */ - lib::P predecessor_; + lib::P predecessor; /** * initialise to blank (zero). @@ -121,10 +122,10 @@ namespace meta { * in order to create a usable TimeGrid */ Builder(string const& nameID ="") - : id_(nameID) - , fps_(1) - , origin_(TimeValue(0)) - , predecessor_() + : id {nameID} + , fps{1} + , origin{TimeValue(0)} + , predecessor{} { } /** create a time grid diff --git a/src/proc/asset/struct-scheme.hpp b/src/proc/asset/struct-scheme.hpp index 15db48dbf..c807ed5b4 100644 --- a/src/proc/asset/struct-scheme.hpp +++ b/src/proc/asset/struct-scheme.hpp @@ -151,13 +151,13 @@ namespace asset{ * The remaining fields are filled in with hardwired defaults. * @note there is a twist, as this asset identity tuple generates * a different hash as the EntryID. It would be desirable - * to make those two addressing systems interchangeable. /////////////TICKET #739 + * to make those two addressing systems interchangeable. ////////////////////////////////TICKET #739 : make identification schemes compatible */ template inline Asset::Ident - getAssetIdent (lib::idi::EntryID const& entryID) - { - Category cat (STRUCT, idi::StructTraits::catFolder()); + getAssetIdent (lib::idi::EntryID const& entryID, asset::Kind assetKind =STRUCT) + { //////////////////////////////////////////TICKET #1156 : do we need the distinction between STRUCT and META? + Category cat (assetKind, idi::StructTraits::catFolder()); return Asset::Ident (entryID.getSym(), cat); } diff --git a/src/proc/asset/struct.hpp b/src/proc/asset/struct.hpp index 4ed21312e..bf5e58a2f 100644 --- a/src/proc/asset/struct.hpp +++ b/src/proc/asset/struct.hpp @@ -108,6 +108,7 @@ namespace asset { * The idea is to have a generic mechanism for attaching properties and relations. * This will become relevant once we build the real query subsystem. * Right now (as of 1/2015), asset::Struct is just an ID provider. + * //////////////////////////////////TICKET #1156 : do we need the distinction between STRUCT and META? */ class Struct : public Asset { diff --git a/src/proc/asset/timeline.hpp b/src/proc/asset/timeline.hpp index a9975ac2e..1877d3674 100644 --- a/src/proc/asset/timeline.hpp +++ b/src/proc/asset/timeline.hpp @@ -47,6 +47,9 @@ ** was no opportunity to set this system really into motion; this is ** not necessarily a bad thing, since meanwhile we understand way ** better in which way the Session will actually be accessed... + ** @todo looks like we'll represent a secondary, attached slave display of the + ** same Timeline rather by materialising it into a TimelineClone within + ** the session. This topic has been postponed as of 10/2018 //////////////////////////////////////TICKET #1083 ** @see Session ** @see Sequence ** @see StructFactory diff --git a/src/proc/asset/unknown.cpp b/src/proc/asset/unknown.cpp index 7f6af5083..da79d941f 100644 --- a/src/proc/asset/unknown.cpp +++ b/src/proc/asset/unknown.cpp @@ -41,19 +41,19 @@ namespace asset { /** create a placeholder for a media with the given identity. * the denoted original media (identity) can be accessed later * on using the Unknown::getOrg() function. - * @note we don't depend on the refered media... + * @note we don't depend on the referred media... */ Unknown::Unknown (const Asset::Ident& idi, string name, Duration length) : Media (idi, name, length) { TODO ("implement some sensible behaviour for the »unknown media« placeholder..."); - } + } - /** using the information stored in this placeholder asset, + /** using the information stored in this placeholder asset, * try to access the "real" media it stands for. */ - Media::PMedia + Media::PMedia Unknown::getOrg() { UNIMPLEMENTED ("how to get at the original media from a »Unknown« placeholder"); diff --git a/src/proc/asset/unknown.hpp b/src/proc/asset/unknown.hpp index 9f588030d..4116ebfbb 100644 --- a/src/proc/asset/unknown.hpp +++ b/src/proc/asset/unknown.hpp @@ -48,8 +48,8 @@ namespace asset { /** * Placeholder Asset for unknown or unavailable media source. * @todo maybe do special handling of the media length, allowing - * it to support existing clips even if the media length - * is not known? + * it to support existing clips even if the media length + * is not known? */ class Unknown : public Media { diff --git a/src/proc/assetmanager.cpp b/src/proc/assetmanager.cpp index 509b6b98c..8b572f72a 100644 --- a/src/proc/assetmanager.cpp +++ b/src/proc/assetmanager.cpp @@ -254,6 +254,7 @@ namespace asset { #include "proc/asset/timeline.hpp" #include "proc/asset/sequence.hpp" #include "proc/asset/meta/time-grid.hpp" +#include "proc/asset/meta/error-log.hpp" namespace proc { namespace asset { @@ -278,7 +279,9 @@ namespace asset { template P AssetManager::wrap (const Sequence& asset); using meta::TimeGrid; + using meta::ErrorLog; template P AssetManager::wrap (const TimeGrid& asset); + template P AssetManager::wrap (const ErrorLog& asset); }} // namespace proc::asset diff --git a/src/proc/cmd.hpp b/src/proc/cmd.hpp index e23aec071..c56ac6c2c 100644 --- a/src/proc/cmd.hpp +++ b/src/proc/cmd.hpp @@ -76,6 +76,11 @@ namespace cmd { extern CommandSetup test_meta_activateContentDiff; ////////////////////////////////TICKET #211 prefix test_ bypasses logging and immediately executes for now (2018) extern CommandSetup meta_deactivateContentDiff; + //--Demo-and-Development---- + extern CommandSetup test_meta_displayInfo; + extern CommandSetup test_meta_markError; + extern CommandSetup test_meta_markNote; + extern CommandSetup test_meta_markAction; }} // namespace proc::cmd diff --git a/src/proc/cmd/meta-cmd.cpp b/src/proc/cmd/meta-cmd.cpp index 100eddfe3..030a5ebfa 100644 --- a/src/proc/cmd/meta-cmd.cpp +++ b/src/proc/cmd/meta-cmd.cpp @@ -38,17 +38,30 @@ #include "proc/cmd.hpp" #include "proc/control/command-def.hpp" //#include "proc/mobject/session.hpp" +#include "include/ui-protocol.hpp" //////////////////////////////////////////////////////////////TICKET #1140 : verify if this include is required #include "include/gui-notification-facade.h" -//#include "lib/symbol.hpp" +#include "gui/interact/wizard.hpp" //////////////////////////////////////////////////////////////TICKET #1140 : include needed temporarily +#include "lib/diff/gen-node.hpp" #include "lib/idi/entry-id.hpp" -//#include "lib/format-string.hpp" +#include "lib/format-string.hpp" //////////////////////////////////////////////////////////////TICKET #1140 : include needed temporarily +//#include "lib/symbol.hpp" +#include "lib/util.hpp" -//#include +#include using lib::hash::LuidH; -//using std::string; +using gui::ID; +using gui::NOTE_INFO; +using gui::NOTE_WARN; +using gui::NOTE_ERROR; +using gui::NotifyLevel; +using gui::MARK_expand; +using gui::GuiNotification; +using lib::diff::GenNode; //using util::cStr; -//using util::_Fmt; +using util::_Fmt; //////////////////////////////////////////////////////////////TICKET #1140 : include needed temporarily +using util::isYes; +using std::string; namespace proc { @@ -80,7 +93,7 @@ COMMAND_DEFINITION (test_meta_activateContentDiff) TODO ("send a population diff starting from scratch"); TODO ("activate life-update service within the session"); // Temporary hack to get any meaningful UI <-> Proc communication - gui::GuiNotification::facade().displayInfo(gui::NOTE_INFO, "Request: population-Diff from Session."); + GuiNotification::facade().displayInfo(NOTE_INFO, "Request: population-Diff from Session."); }) .captureUndo ([]() -> bool { @@ -130,6 +143,94 @@ COMMAND_DEFINITION (meta_deactivateContentDiff) + /* ===== Demo and Development ===== */ + + /** DemoGuiRoundtrip: push a notification info message back up into the UI. + * @todo this is a demo mock setup to further development of the Proc-UI integration //////////////////TICKET #1140 : prototypical Proc-GUI communication + */ +COMMAND_DEFINITION (test_meta_displayInfo) + { + def.operation ([](int level, string message) + { + GuiNotification::facade().displayInfo (static_cast(level), message); + }) + .captureUndo ([](int level, string message) -> string + { + return _Fmt{"displayInfo(%d, '%s')"} % level % message; + }) + .undoOperation ([](int, string, string uiAction) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO UI-Action: "+uiAction); + }); + }; + + + /** DemoGuiRoundtrip: send a `markError` message back up into the UI. + * @todo this is a demo mock setup to further development of the Proc-UI integration //////////////////TICKET #1140 : prototypical Proc-GUI communication + */ +COMMAND_DEFINITION (test_meta_markError) + { + def.operation ([](string message) + { + ID errorLogID = gui::interact::Wizard::getErrorLogID(); + GuiNotification::facade().markError (errorLogID, message); + }) + .captureUndo ([](string message) -> string + { + return _Fmt{"GUI::errorLog <- markError('%s')"} % message; + }) + .undoOperation ([](string, string uiAction) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO UI-Action: "+uiAction); + }); + }; + + + /** DemoGuiRoundtrip: send a `markNote` message back up into the UI. + * @todo this is a demo mock setup to further development of the Proc-UI integration //////////////////TICKET #1140 : prototypical Proc-GUI communication + */ +COMMAND_DEFINITION (test_meta_markNote) + { + def.operation ([](string message) + { + ID errorLogID = gui::interact::Wizard::getErrorLogID(); + GuiNotification::facade().markNote (errorLogID, message); + }) + .captureUndo ([](string message) -> string + { + return _Fmt{"GUI::errorLog <- markNote('%s')"} % message; + }) + .undoOperation ([](string, string uiAction) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO UI-Action: "+uiAction); + }); + }; + + + /** DemoGuiRoundtrip: send a generic `mark` message with given _action ID_ back up into the UI. + * @todo this is a demo mock setup to further development of the Proc-UI integration //////////////////TICKET #1140 : prototypical Proc-GUI communication + */ +COMMAND_DEFINITION (test_meta_markAction) + { + def.operation ([](string actionID, string message) + { + ID errorLogID = gui::interact::Wizard::getErrorLogID(); + GuiNotification::facade().mark (errorLogID + ,actionID==MARK_expand? GenNode{actionID, isYes(message)} + : GenNode{actionID, message}); + }) + .captureUndo ([](string actionID, string message) -> string + { + return _Fmt{"GUI::errorLog <- mark(%s, '%s')"} % actionID % message; + }) + .undoOperation ([](string, string, string uiAction) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO UI-Action: "+uiAction); + }); + }; + + + /** more to come here...*/ diff --git a/src/proc/control/command-instance-manager.hpp b/src/proc/control/command-instance-manager.hpp index 1134d9d1b..7a7f3c478 100644 --- a/src/proc/control/command-instance-manager.hpp +++ b/src/proc/control/command-instance-manager.hpp @@ -88,7 +88,7 @@ namespace control { * #getInstance. It represents an _anonymous instance_ kept alive solely by the CommandInstanceManager * (i.e. there is no registration of a command under that instanceID in the global CommandRegistry). * When done with the parametrisation, by calling #dispatch, this anonymous instance will be handed - * over to the [Dispatcher](CommandDispatch) (installed on construction). Typically, this will in fact + * over to the [Dispatcher](\ref CommandDispatch) (installed on construction). Typically, this will in fact * be the proc::control::ProcDispatcher, which runs in a dedicated thread ("session loop thread") and * maintains a queue of commands to be dispatched towards the current session. Since Command is a smart * handle, the enqueued instance will stay alive until execution and then go out of scope. But, after diff --git a/src/proc/control/command-setup.cpp b/src/proc/control/command-setup.cpp index e592325e1..690eec058 100644 --- a/src/proc/control/command-setup.cpp +++ b/src/proc/control/command-setup.cpp @@ -295,8 +295,8 @@ namespace control { * opened instance, which will then be bound and dispatched likewise. * @param instanceID global commandID or previously opened local instanceID * @param argSeq command argument tuple packaged as Record, which - * is the standard format [sent](BusTerm::act(GenNode)) for - * command execution via [UI-bus](ui-bus.hpp) + * is the standard format [sent](\ref BusTerm::act(GenNode)) for + * command execution via [UI-bus](\ref ui-bus.hpp) */ void CommandInstanceManager::bindAndDispatch (Symbol instanceID, Rec const& argSeq) diff --git a/src/proc/control/command-setup.hpp b/src/proc/control/command-setup.hpp index fedcf7871..251d665d9 100644 --- a/src/proc/control/command-setup.hpp +++ b/src/proc/control/command-setup.hpp @@ -24,7 +24,7 @@ /** @file command-setup.hpp ** Provision for setup of concrete commands for use by the UI. ** A *Proc-Layer command* is a functor, which can be parametrised with actual arguments. - ** It needs to be [defined](command-def.hpp) beforehand, which means to establish an unique + ** It needs to be [defined](\ref command-def.hpp) beforehand, which means to establish an unique ** name and to supply three functions, one for the actual command operation, one to capture ** state and one to _undo_ the effect of the command invocation. CommandSetup allows to create ** series of such definitions with minimal effort. Since any access and mutation from the UI into diff --git a/src/proc/control/command.hpp b/src/proc/control/command.hpp index 1a670785a..24b78c54e 100644 --- a/src/proc/control/command.hpp +++ b/src/proc/control/command.hpp @@ -37,8 +37,8 @@ ** stored definition backend. ** ** # Command definition, argument types and UNDO operation - ** For a command to be usable at all, a concrete [command definition](command-def.hpp) need to be supplied - ** somewhere in the code base. Typically this is done through static [command-setup bindings](command-setup.hpp). + ** For a command to be usable at all, a concrete [command definition](\ref command-def.hpp) need to be supplied + ** somewhere in the code base. Typically this is done through static [command-setup bindings](\ref command-setup.hpp). ** Such a command definition links three functions with the name-ID of the command ** - the actual command operation ** - a function to capture state diff --git a/src/proc/control/handling-pattern.cpp b/src/proc/control/handling-pattern.cpp index da1188175..043a25306 100644 --- a/src/proc/control/handling-pattern.cpp +++ b/src/proc/control/handling-pattern.cpp @@ -104,10 +104,14 @@ namespace control { /* ====== execution result state object ======= */ - /** @note we just grab and retain the error message. */ + /** @note just grab and retain the error message, but _clear_ the error flag. + * Rationale: by packaging into the ExecResult, the excepton counts as treated. + */ ExecResult::ExecResult (lumiera::Error const& problem) : log_(problem.what()) - { } + { + lumiera_error(); // ensure error flag is cleared + } bool diff --git a/src/proc/control/media-impl-lib.hpp b/src/proc/control/media-impl-lib.hpp index 08496ca30..d151d4954 100644 --- a/src/proc/control/media-impl-lib.hpp +++ b/src/proc/control/media-impl-lib.hpp @@ -24,7 +24,7 @@ /** @file media-impl-lib.hpp ** Abstraction to represent (metadata) of an external library to handle media data ** Lumiera delegates most actual media data processing to well established external - ** libraries. While configuring the render process, a [stream type](streamtype.hpp) + ** libraries. While configuring the render process, a [stream type](\ref streamtype.hpp) ** framework is used to gain uniform access to heterogeneous media and processing ** facilities. At some point, we need to integrate the individual capabilities of ** the referenced libraries, though. This will be done with adapter implementations, diff --git a/src/proc/control/proc-dispatcher.cpp b/src/proc/control/proc-dispatcher.cpp index 8098da914..c15ec1ce2 100644 --- a/src/proc/control/proc-dispatcher.cpp +++ b/src/proc/control/proc-dispatcher.cpp @@ -247,8 +247,8 @@ namespace control { updateState(); } } - catch (lumiera::Error& problem) - { + catch (std::exception& problem) + { // could also be lumiera::Error errorMsg = problem.what(); lumiera_error(); // clear error flag } @@ -304,7 +304,8 @@ namespace control { if (util::startsWith (string(cmd.getID()), "test")) { INFO (command, "+++ -------->>> bang!"); - cmd(); + auto resultState = cmd(); + resultState.maybeThrow(); } //////////////////////////////////////////////////////TODO : magic to invoke commands from unit tests } diff --git a/src/proc/engine/dispatcher.hpp b/src/proc/engine/dispatcher.hpp index 2b61ce5af..440037837 100644 --- a/src/proc/engine/dispatcher.hpp +++ b/src/proc/engine/dispatcher.hpp @@ -23,7 +23,7 @@ /** @file dispatcher.hpp ** Service abstraction within the render engine for generating render jobs. - ** On interface level, the render engine uses the notion of a [calculation stream](CalcStream) + ** On interface level, the render engine uses the notion of a [calculation stream](\ref CalcStream) ** to represent an ongoing rendering process. Within the _implementation_ of such a process, ** the Dispatcher is responsible for transforming the generic setup of such a calculation stream ** into a sequence of concrete jobs, anchored at some distinct point in time. diff --git a/src/proc/mobject/session/binding.hpp b/src/proc/mobject/session/binding.hpp index 1d9d638dc..e7e815e6f 100644 --- a/src/proc/mobject/session/binding.hpp +++ b/src/proc/mobject/session/binding.hpp @@ -25,6 +25,7 @@ ** MObject in session to represent the top-level binding of a sequence ** @todo stalled effort towards a session implementation from 2010 ** @todo 2016 likely to stay, but expect some extensive rework + ** @todo Also consider timeline-slave, a topic postponed as of 10/2018 /////////////////////////////////TICKET #1083 */ diff --git a/src/proc/mobject/session/dummy-session-connection.cpp b/src/proc/mobject/session/dummy-session-connection.cpp index edff76ccd..fcbd37adc 100644 --- a/src/proc/mobject/session/dummy-session-connection.cpp +++ b/src/proc/mobject/session/dummy-session-connection.cpp @@ -29,7 +29,7 @@ ** - some session content ** - commands to be invoked ** - ** @todo WIP as of 12/2016 ///////////////////////TICKET #1042 + ** @todo WIP as of 10/2018 ///////////////////////TICKET #1042 ** ** @see DummySessionConnection_test ** @@ -39,11 +39,34 @@ //#include "lib/symbol.hpp" //#include "include/logging.h" #include "proc/mobject/session/dummy-session-connection.hpp" +#include "proc/mobject/session/root.hpp" +#include "proc/control/command-def.hpp" +#include "include/ui-protocol.hpp" +#include "include/gui-notification-facade.h" +#include "lib/diff/tree-diff-application.hpp" +#include "lib/diff/mutation-message.hpp" +#include "lib/diff/gen-node.hpp" +//#include "lib/idi/entry-id.hpp" +#include "lib/format-string.hpp" +#include "lib/format-cout.hpp" +//#include "lib/symbol.hpp" +#include "lib/util.hpp" #include -#include +//#include -using std::map; +using lib::diff::MutationMessage; +using lib::diff::GenNode; +using lib::diff::MakeRec; +using lib::diff::Ref; +using lib::idi::RandID; +//using util::cStr; +using util::_Fmt; +using std::string; + +#include + +//using std::map; using std::string; using util::contains; @@ -55,15 +78,23 @@ namespace session { namespace { //Implementation details.... + GenNode + emptyTimeline (string baseID, RandID const& forkRootID) + { + return MakeRec() + .set(MakeRec() + .type (string{gui::TYPE_Fork}) + .genNode(forkRootID) + ) + .genNode(baseID); + } } //(End)Implementation details.... - DummySessionConnection::DummySessionConnection ( ) - { - } - DummySessionConnection::~DummySessionConnection ( ) + DummySessionConnection::~DummySessionConnection() { } + + DummySessionConnection::DummySessionConnection() { - // ////TODO Auto-generated destructor stub } @@ -72,10 +103,127 @@ namespace session { /** - * - * @param id - * @return + * Build a population diff message to describe a specific session structure to add */ + MutationMessage + DummySessionConnection::fabricateSeq1 (string baseID) + { + const RandID forkRootID{gui::ATTR_fork}; + const GenNode timeline = emptyTimeline (baseID, forkRootID) + , rootTrackName = GenNode{string{gui::ATTR_name}, "Track-"+baseID} + , FORK_ROOT = MakeRec().genNode(forkRootID) + ; + + return MutationMessage{ ins (timeline) + , mut (timeline) + , mut (FORK_ROOT) + , set (rootTrackName) + , emu (FORK_ROOT) + , emu (timeline) + }; + } -}}} // namespace proc::mobject::session + void + DummySessionConnection::applyCopy (MutationMessage const& diff) + { + TODO ("build internal diagnostic data structure, apply a copy of the message"); + } + + +}}// namespace proc::mobject::session + +namespace cmd { + + using lib::hash::LuidH; + using gui::ID; + using gui::NOTE_INFO; + using gui::NOTE_WARN; + using gui::NOTE_ERROR; +// using gui::NotifyLevel; +// using gui::MARK_expand; + using gui::GuiNotification; +// using util::isYes; + + namespace session = proc::mobject::session; + + using DummySess = session::DummySessionConnection; + + + /* ============ dedicated Fake-Commands ============ */ + + /** Populate the Timeline in the UI with a typical simple Dummy sequence. + * This Proc-Layer command script fabricates a faked "population diff", which not + * corresponds to any existing session data structure, but looks as if emanated while + * loading current session state. + * - one single Timeline + * - just the root track + * - two clips placed on that track + * @todo use this to establish basic Timeline display in the UI //////////////////TICKET #1014 : produce Dummy content to populate timeline + */ +COMMAND_DEFINITION (test_fake_injectSequence_1) + { + def.operation ([](string dummyID) + { + string message{_Fmt{"fabricate Sequence_1 (dummyID='%s')"} % dummyID}; + GuiNotification::facade().displayInfo (NOTE_INFO, message); + auto popuDiff = DummySess::instance().fabricateSeq1 (dummyID); + DummySess::instance().applyCopy (popuDiff); + auto rootID = session::Root::getID(); + GuiNotification::facade().mutate (rootID, move(popuDiff)); + }) + .captureUndo ([](string dummyID) -> string + { + return _Fmt{"fabricateSequence_1('%s')"} % dummyID; + }) + .undoOperation ([](string, string memento) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO Dummy-Action: "+memento); + }); + }; + + + /** Populate the Timeline in the UI with a rather complex Dummy sequence. + * This command script fabricates a faked convoluted "population diff"... + * @todo use this to enact a complex layout structure in the Timeline-UI //////////////////TICKET #1014 : produce Dummy content to populate timeline + */ +COMMAND_DEFINITION (test_fake_injectSequence_2) + { + def.operation ([](string dummyID) + { + string message{_Fmt{"fabricate Sequence_2 (dummyID='%s')"} % dummyID}; + GuiNotification::facade().displayInfo (NOTE_INFO, message); + }) + .captureUndo ([](string dummyID) -> string + { + return _Fmt{"fabricateSequence_2('%s')"} % dummyID; + }) + .undoOperation ([](string, string memento) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO Dummy-Action: "+memento); + }); + }; + + + /** Template for dummy-code.... + * @todo use this to enact a complex layout structure in the Timeline-UI //////////////////TICKET #1042 : further the DummySessionConnection + */ +COMMAND_DEFINITION (test_fake_blubb) + { + def.operation ([](string dummyID) + { + string message{_Fmt{"fabricate gulp (dummyID='%s')"} % dummyID}; + GuiNotification::facade().displayInfo (NOTE_INFO, message); + }) + .captureUndo ([](string dummyID) -> string + { + return _Fmt{"fabricateGulp('%s')"} % dummyID; + }) + .undoOperation ([](string, string memento) + { + GuiNotification::facade().displayInfo (NOTE_WARN, "can not UNDO Dummy-Action: "+memento); + }); + }; + + +}} // namespace proc::mobject::session diff --git a/src/proc/mobject/session/dummy-session-connection.hpp b/src/proc/mobject/session/dummy-session-connection.hpp index f83d47741..745785420 100644 --- a/src/proc/mobject/session/dummy-session-connection.hpp +++ b/src/proc/mobject/session/dummy-session-connection.hpp @@ -26,14 +26,14 @@ ** parts of the application, which are to defined in the very attempt this placeholder ** is used for... ** - it can fake some session content - ** - it may expose some fake command implementations + ** - it exposes some fake command implementations ** - and all this together with suitable diagnostic facilities ** ** The plan is to push this helper along, as we proceed from the UI down towards the session. ** In the end, this placeholder service may be inserted into the session implementation proper, ** until we have the ability to load a persistent session from storage. ** - ** @todo WIP as of 12/2016 //////////////////////////////////////////////////////////////////////TICKET #1042 + ** @todo WIP as of 10/2018 //////////////////////////////////////////////////////////////////////TICKET #1042 ** ** @see ui-bus.hpp ** @see dummy-session-connection.cpp implementation @@ -48,23 +48,49 @@ #include "lib/nocopy.hpp" #include "lib/util.hpp" +#include "lib/diff/tree-diff.hpp" +#include "proc/control/command-setup.hpp" + #include +namespace lib { +namespace diff{ + class MutationMessage; +}} namespace proc { +namespace cmd { + + using control::Command; + using control::CommandDef; + using control::CommandSetup; + + /* ============ dedicated Fake-Commands ============ */ + + extern CommandSetup test_fake_injectSequence_1; + extern CommandSetup test_fake_injectSequence_2; + +} namespace mobject { namespace session { //using util::isnil; using std::string; + using lib::diff::MutationMessage; /** - * Basic (abstracted) view of... + * Scaffolding to drive the evolution of the Lumiera application. + * @todo as of 2018, the most pressing problem is to establish the interplay between + * GUI-Layer and Proc-Layer, so the software gains some tangible functionality... + * @note readily-available singleton to enact prototypical behaviour as appropriate. * - * @see SomeSystem * @see DummySessionConnection_test + * @see gui::interact::InteractionDirector::buildMutator + * @see gui::interact::InteractionDirector::injectTimeline + * @see gui::timeline::TimelineController */ class DummySessionConnection - : util::NonCopyable + : lib::diff::TreeDiffLanguage + , util::NonCopyable { string nothing_; @@ -80,9 +106,14 @@ namespace session { static lib::Depend instance; - /* == X interface for == */ + /* == do X == */ + + /* == forget Y == */ + + MutationMessage fabricateSeq1 (string baseID); + + void applyCopy (MutationMessage const&); - /* == Adapter interface for == */ #if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////UNIMPLEMENTED :: TICKET #1042 void setSolution (string const& solution = "") diff --git a/src/proc/streamtype.hpp b/src/proc/streamtype.hpp index a6475cc60..7522dc1e6 100644 --- a/src/proc/streamtype.hpp +++ b/src/proc/streamtype.hpp @@ -75,7 +75,7 @@ namespace proc { class ImplFacade; class ImplConstraint; - typedef lib::idi::EntryID ID; + using ID = lib::idi::EntryID; Prototype const& prototype; diff --git a/tests/00support.tests b/tests/00support.tests index 6d11e9424..68857d8d1 100644 --- a/tests/00support.tests +++ b/tests/00support.tests @@ -34,6 +34,12 @@ err-lit: ..before match("beans") @ Rec(EventLogHeader| this = baked beans ) err-lit: ..before match("spam") @ Rec(event|{spam}) err-lit: ..after HEAD baked beans err-lit: __Log_condition_violated__ +err-lit: FAILED to match("spam") +err-lit: ..before match("spam") @ Rec(event|{spam}) +err-lit: ..before match("ham") @ Rec(event|{ham}) +err-lit: ..before match("beans") @ Rec(EventLogHeader| this = baked beans ) +err-lit: ..after HEAD baked beans +err-lit: __Log_condition_violated__ err-lit: FOUND at least match("ham") @ Rec(event|{ham}) err-lit: ..after match("eggs") @ Rec(EventLogHeader| this = eggs ) err-lit: ..before match("spam") @ Rec(event|{spam}) diff --git a/tests/15library.tests b/tests/15library.tests index 3aa51767f..88c97555e 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -122,6 +122,11 @@ return: 0 END +TEST "Diff: accept and ignore arbitrary diff" DiffIgnoreChanges_test < 'Word' -out-lit: 'a Sentence' --> 'a_Sentence' -out-lit: 'trailing Withespace -out-lit: ' --> 'trailing_Withespace' -out-lit: 'with a lot -out-lit: of Whitespace' --> 'with_a_lot_of_Whitespace' -out-lit: 'with"much (punctuation)[]!' --> 'withmuch_(punctuation)' -out-lit: '§&Ω%€ leading garbage' --> 'leading_garbage' -out-lit: 'mixed Ω garbage' --> 'mixed_garbage' -out-lit: 'Bääääh!!' --> 'Bh' -out-lit: '§&Ω%€' --> '' -return: 0 -END - - TEST "ScopedHolder_test" ScopedHolder_test <... out: checking ScopedPtrHolder... @@ -588,6 +577,11 @@ return: 0 END +TEST "ownership of malloced data" UniqueMallocOwner_test < 'Word' +out-lit: 'a Sentence' --> 'a_Sentence' +out-lit: 'trailing Withespace +out-lit: ' --> 'trailing_Withespace' +out-lit: 'with a lot +out-lit: of Whitespace' --> 'with_a_lot_of_Whitespace' +out-lit: '@with".'much (\$punctuation)[]!' --> '@with.much_(\$punctuation)' +out-lit: '§&Ω%€ leading garbage' --> 'leading_garbage' +out-lit: 'mixed Ω garbage' --> 'mixed_garbage' +out-lit: 'Bääääh!!' --> 'Bh' +out-lit: '§&Ω%€' --> '' return: 0 END diff --git a/tests/25fundamental.tests b/tests/25fundamental.tests index f18b6dff9..f42fe79cb 100644 --- a/tests/25fundamental.tests +++ b/tests/25fundamental.tests @@ -257,3 +257,8 @@ return: 0 END +TEST "Zombie tripwire" ZombieCheck_test < + + 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 zombie-check-test.cpp + ** unit test \ref Singleton_test + */ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/zombie-check.hpp" +#include "lib/util.hpp" + +#include + +using util::contains; +using util::isnil; +using std::string; + + +namespace lib { +namespace test{ + + + + + + + /**********************************************************************//** + * @test verify the ability to trap access to deceased objects. + * @remark For actual usage, the zombie detector needs to be planted into + * static memory. This test can only verify the trigger mechanics. + * @see zombie-check.hpp + * @see lib::DependencyFactory::zombieCheck + */ + class ZombieCheck_test : public Test + { + + virtual void + run (Arg) + { + auto zombieID = randStr(50); + ZombieCheck trap{zombieID}; + CHECK (not trap); + + trap.~ZombieCheck(); // note: unconventional kill + CHECK (trap); // accessing deceased object... + + try { // + trap(); // trigger the tripwire + } + catch(lumiera::error::Fatal& casuality) + { + string obituary = casuality.what(); + CHECK (not isnil (obituary)); + CHECK ( contains (obituary, zombieID.substr(0,41))); + CHECK (not contains (obituary, zombieID)); + } + } + }; + + + + /** Register this test class... */ + LAUNCHER (ZombieCheck_test, "unit common"); + + + +}} // namespace lib::test diff --git a/tests/core/proc/asset/asset-diagnostics.hpp b/tests/core/proc/asset/asset-diagnostics.hpp index 4f93636cb..2e91b5e71 100644 --- a/tests/core/proc/asset/asset-diagnostics.hpp +++ b/tests/core/proc/asset/asset-diagnostics.hpp @@ -54,14 +54,14 @@ namespace proc { namespace asset { inline void - dump (PcAsset& aa) + dump (PcAsset const& aa) { if (!aa) cout << "Asset(NULL)\n"; else { _Fmt fmt("%s %|50T.| id=%s adr=%p smart-ptr=%p use-count=%u"); - cout << fmt % aa % aa->getID() % aa.get() % &aa % (aa.use_count() - 1) << "\n"; + cout << fmt % aa % aa->getID() % (void*)aa.get() % &aa % (aa.use_count() - 1) << "\n"; } } @@ -70,7 +70,8 @@ namespace asset { { list assets (AssetManager::instance().listContent()); cout << "----all-registered-Assets----\n"; - for_each (assets, bind (&dump, _1)); + for (auto const& pA : assets) + dump (pA); } diff --git a/tests/core/proc/asset/basicpipetest.cpp b/tests/core/proc/asset/basicpipetest.cpp index 476619c66..f0bf2126b 100644 --- a/tests/core/proc/asset/basicpipetest.cpp +++ b/tests/core/proc/asset/basicpipetest.cpp @@ -64,9 +64,10 @@ namespace test { */ class BasicPipe_test : public Test { - virtual void run(Arg arg) + virtual void + run (Arg arg) { - string pipeID = isnil(arg)? "Black Hole" : arg[1]; + string pipeID = isnil (arg)? "Black Hole" : arg[1]; string streamID = 2>arg.size()? "teststream" : arg[2] ; createExplicit (pipeID,streamID); @@ -80,7 +81,7 @@ namespace test { void createExplicit (string pID, string sID) { - string pID_sane (pID); + string pID_sane{pID}; normaliseID (pID_sane); CHECK (pID_sane != pID); @@ -89,17 +90,17 @@ namespace test { CHECK (thePipe); CHECK (thePipe->getProcPatt()); CHECK (thePipe->getPipeID() == pID_sane); - CHECK (thePipe->getStreamID() == StreamType::ID(sID)); + CHECK (thePipe->getStreamID() == StreamType::ID{sID}); CHECK (thePipe->shortDesc == pID_sane); Asset::Ident idi = thePipe->ident; CHECK (idi.org == "lumi"); CHECK (contains (idi.name, thePipe->getPipeID())); - CHECK (contains (idi.name, thePipe->getStreamID())); + CHECK (contains (idi.name, string{thePipe->getStreamID()})); - Category cat (idi.category); - Category refcat (STRUCT,"pipes"); - CHECK ( cat.hasKind(STRUCT) ); + Category cat{idi.category}; + Category refcat{STRUCT,"pipes"}; + CHECK ( cat.hasKind (STRUCT) ); CHECK ( cat.isWithin(refcat) ); } @@ -131,25 +132,25 @@ namespace test { PPipe pipe1 = Pipe::query (""); // "the default pipe" PPipe pipe2; CHECK (pipe1); - CHECK (pipe1 == Session::current->defaults (Query())); + CHECK (pipe1 == Session::current->defaults (Query{})); CHECK (pipe1->ident.category.hasKind(VIDEO)); CHECK (pipe1->getProcPatt()); - PProcPatt propa = Session::current->defaults (Query("pipe(default)")); + PProcPatt propa = Session::current->defaults (Query{"pipe(default)"}); CHECK (propa == pipe1->getProcPatt()); // several variants to query for "the default pipe" - pipe2 = Session::current->defaults(Query ()); + pipe2 = Session::current->defaults(Query{}); CHECK (pipe2 == pipe1); - pipe2 = asset::Struct::retrieve (Query ()); + pipe2 = asset::Struct::retrieve (Query{}); CHECK (pipe2 == pipe1); - pipe2 = asset::Struct::retrieve (Query ("pipe(default)")); + pipe2 = asset::Struct::retrieve (Query{"pipe(default)"}); CHECK (pipe2 == pipe1); - string sID = pipe1->getStreamID(); // sort of a "default stream type" + auto sID = string{pipe1->getStreamID()}; // sort of a "default stream type" PPipe pipe3 = Pipe::query ("stream("+sID+")"); CHECK (pipe3); - CHECK (pipe3->getStreamID() == StreamType::ID(sID)); - CHECK (pipe3->getProcPatt() == Session::current->defaults (Query("stream("+sID+")"))); + CHECK (pipe3->getStreamID() == StreamType::ID{sID}); + CHECK (pipe3->getProcPatt() == Session::current->defaults (Query{"stream("+sID+")"})); } @@ -174,7 +175,7 @@ namespace test { // default pipe for this pattern automatically PPipe pipe2x = Pipe::query ("pattern(another)"); CHECK (pattern2 == pipe2x->getProcPatt()); - CHECK (pipe2x == Session::current->defaults (Query("pattern(another)"))); + CHECK (pipe2x == Session::current->defaults (Query{"pattern(another)"})); thePipe->switchProcPatt(pattern2); CHECK ( dependencyCheck (thePipe, pattern2)); @@ -197,7 +198,7 @@ namespace test { PPipe pipe3x = Pipe::query ("pattern(another)"); pattern3 = pipe3x->getProcPatt(); /////TODO: transition to P<> CHECK (pattern3 != pattern2); // because pattern2 is already unlinked... - CHECK (pipe3x == Session::current->defaults (Query("pattern(another)"))); + CHECK (pipe3x == Session::current->defaults (Query{"pattern(another)"})); CHECK (pipe3x != pipe2x); // ..we got a new default pipe for "pattern(another)" too! diff --git a/tests/core/proc/asset/create-asset-test.cpp b/tests/core/proc/asset/create-asset-test.cpp index c81140b08..ece46790f 100644 --- a/tests/core/proc/asset/create-asset-test.cpp +++ b/tests/core/proc/asset/create-asset-test.cpp @@ -27,18 +27,24 @@ #include "include/logging.h" #include "lib/test/run.hpp" -#include "lib/util.hpp" +#include "lib/test/test-helper.hpp" #include "proc/assetmanager.hpp" #include "proc/asset/media.hpp" #include "proc/asset/proc.hpp" +#include "proc/asset/meta/time-grid.hpp" +#include "proc/asset/meta/error-log.hpp" #include "proc/asset/asset-diagnostics.hpp" #include "backend/media-access-mock.hpp" +#include "lib/time/timevalue.hpp" #include "lib/depend-inject.hpp" +#include "lib/util.hpp" using util::isnil; using std::string; +using lib::time::FrameRate; +using lib::test::randStr; namespace proc { @@ -63,6 +69,7 @@ namespace test { createMedia(); factoryVariants(); + createMetaAssets(); if (!isnil (arg)) dumpAssetManager(); @@ -71,7 +78,7 @@ namespace test { ////////////////////////////////////TICKET #589 - typedef lib::P PM; /////TODO: transition to P<> + using PM = lib::P; /** @test Creating and automatically registering Asset instances. * Re-Retrieving the newly created objects from AssetManager. @@ -79,7 +86,8 @@ namespace test { * different kinds of Assets by ID, querying with the * wrong Category and querying unknown IDs. */ - void createMedia() + void + createMedia() { Category cat(VIDEO,"bin1"); Asset::Ident key("test-1", cat, "ichthyo", 5); @@ -89,7 +97,7 @@ namespace test { // Assets have been registered and can be retrieved by ID AssetManager& aMang = AssetManager::instance(); - PM registered; /////TODO: transition to P<> + PM registered; registered = aMang.getAsset (mm1->getID()); CHECK (registered == mm1); registered = aMang.getAsset (mm2->getID()); @@ -167,7 +175,8 @@ namespace test { * creation machinery. Covers filling out Asset's * datafields, amending missing pieces of information. */ - void factoryVariants() + void + factoryVariants() { PM candi; @@ -185,19 +194,19 @@ namespace test { candi = asset::Media::create(string("test-3.wav"), Category(AUDIO)); CHECK ( checkProperties (candi, Asset::Ident("test-3", Category(AUDIO), "lumi", 1) - , "test-3.wav")); + , "test-3.wav")); candi = asset::Media::create("some/path/test-4.wav", Category(AUDIO)); CHECK ( checkProperties (candi, Asset::Ident("test-4", Category(AUDIO), "lumi", 1) - , "some/path/test-4.wav")); + , "some/path/test-4.wav")); candi = asset::Media::create("", Category(AUDIO,"sub/bin")); CHECK ( checkProperties (candi, Asset::Ident("nil", Category(AUDIO,"sub/bin"), "lumi", 1) - , "")); + , "")); candi = asset::Media::create("", AUDIO); CHECK ( checkProperties (candi, Asset::Ident("nil", Category(AUDIO), "lumi", 1) - , "")); + , "")); } bool checkProperties (PM object, Asset::Ident identity, string filename) @@ -205,6 +214,36 @@ namespace test { return identity == object->ident && filename == object->getFilename(); } + + + void + createMetaAssets() + { + using asset::meta::GridID; + using asset::meta::PGrid; + + GridID myGrID (randStr(8)); + auto gridSpec = asset::Meta::create (myGrID); + gridSpec.fps = FrameRate{23}; + PGrid myGrid = gridSpec.commit(); + + CHECK (myGrid); + CHECK (myGrid->ident.name == myGrID.getSym()); ////////////////////////////////TICKET #739 : assets should use EntryID instead of asset::ID + + CHECK (AssetManager::instance().known (myGrid->getID())); + CHECK (myGrid == AssetManager::instance().getAsset (myGrid->getID())); + + // for the ErrorLog assert, as of 8/2018 there is just one single global placeholder entity available + asset::meta::PLog globalLog = asset::meta::ErrorLog::global(); /////////////////////////////////TICKET #1157 : what's the purpose of this ErrorLog Asset after all?? + + CHECK (globalLog->ident.name == meta::theErrorLog_ID.getSym()); + CHECK (AssetManager::instance().known (globalLog->getID())); + CHECK (2 == globalLog.use_count()); // AssetManager also holds a reference + + PAsset furtherRef = asset::meta::ErrorLog::global(); + CHECK (3 == globalLog.use_count()); + CHECK (furtherRef == globalLog); + } }; diff --git a/tests/core/proc/asset/meta/time-grid-basics-test.cpp b/tests/core/proc/asset/meta/time-grid-basics-test.cpp index a5cae3e7c..82d2d93c6 100644 --- a/tests/core/proc/asset/meta/time-grid-basics-test.cpp +++ b/tests/core/proc/asset/meta/time-grid-basics-test.cpp @@ -85,12 +85,12 @@ namespace test { GridID myGrID (randStr(8)); GridBuilder spec = asset::Meta::create (myGrID); - CHECK ( spec.fps_ == 1); - CHECK ( spec.origin_ == TimeValue(0)); - CHECK (!spec.predecessor_); + CHECK ( spec.fps == 1); + CHECK ( spec.origin == TimeValue(0)); + CHECK (!spec.predecessor); - spec.fps_ = testFps; - spec.origin_ = testOrigin; + spec.fps = testFps; + spec.origin = testOrigin; PGrid myGrid = spec.commit(); CHECK (myGrid); diff --git a/tests/core/proc/asset/typed-id-test.cpp b/tests/core/proc/asset/typed-id-test.cpp index 7fdc569f3..737b71901 100644 --- a/tests/core/proc/asset/typed-id-test.cpp +++ b/tests/core/proc/asset/typed-id-test.cpp @@ -111,7 +111,7 @@ namespace test{ * - re-access the instances just by symbolic ID * - verify the registration is cleaned up automatically. * - * @todo partially unimplemented and thus commented out ////////////////////TICKET #599 + * @todo draft from 2010 -- partially unimplemented and thus commented out //////////////////////////////////////////STICKET #599 * * @see typed-id.hpp interface covered here * @see typed-lookup.cpp implementation diff --git a/tests/core/proc/mobject/session/defs-manager-test.cpp b/tests/core/proc/mobject/session/defs-manager-test.cpp index a7db7369e..8225dcae8 100644 --- a/tests/core/proc/mobject/session/defs-manager-test.cpp +++ b/tests/core/proc/mobject/session/defs-manager-test.cpp @@ -128,16 +128,16 @@ namespace test { retrieveConstrainedDefault (string pID, string sID) { PPipe pipe1 = Pipe::query (""); // "the default pipe" - CHECK ( pipe1->getStreamID() != StreamType::ID(sID), + CHECK ( pipe1->getStreamID() != StreamType::ID{sID}, "stream-ID \"%s\" not suitable for test, because " "the default-pipe \"%s\" happens to have the same " "stream-ID. We need it to be different", sID.c_str(), pID.c_str() ); - string query_for_sID ("stream("+sID+")"); + string query_for_sID{"stream("+sID+")"}; PPipe pipe2 = Pipe::query (query_for_sID); - CHECK (pipe2->getStreamID() == StreamType::ID(sID)); + CHECK (pipe2->getStreamID() == StreamType::ID{sID}); CHECK (pipe2 != pipe1); CHECK (pipe2 == Pipe::query (query_for_sID)); // reproducible } @@ -146,13 +146,13 @@ namespace test { void failureCreatesNewDefault() { - PPipe pipe1 = Session::current->defaults(Query ("")); // "the default pipe" + PPipe pipe1 = Session::current->defaults(Query{""}); // "the default pipe" string new_pID = _Fmt{"dummy_%s_%i"} % pipe1->getPipeID() % std::rand() ; // make random new pipeID - Query query_for_new ("pipe("+new_pID+")"); + Query query_for_new{"pipe("+new_pID+")"}; CHECK (!find (query_for_new)); // check it doesn't exist PPipe pipe2 = Session::current->defaults (query_for_new); // triggers creation @@ -170,13 +170,13 @@ namespace test { void verifyRemoval() { - Symbol pID ("some_pipe"); - Query query_for_pID ("pipe("+pID+")"); + Symbol pID{"some_pipe"}; + Query query_for_pID{"pipe("+pID+")"}; size_t hash; { // create new pipe and declare it to be a default PPipe pipe1 = Struct::retrieve.newInstance (pID); - Session::current->defaults.define(pipe1); + Session::current->defaults.define (pipe1); CHECK (2 == pipe1.use_count()); // the pipe1 smart-ptr and the AssetManager hash = pipe1->getID(); diff --git a/tests/gui/abstract-tangible-test.cpp b/tests/gui/abstract-tangible-test.cpp index fdd077f52..2bc76eb68 100644 --- a/tests/gui/abstract-tangible-test.cpp +++ b/tests/gui/abstract-tangible-test.cpp @@ -179,6 +179,7 @@ namespace test { verify_mockManipulation(); invokeCommand(); markState(); + revealer(); notify(); mutate(); } @@ -196,7 +197,7 @@ namespace test { * the occurrence of expected events, invocations and responses. * * ### connectivity - * Any mock element will automatically connect against the [Test-Nexus](test/test-nexus.hpp), + * Any mock element will automatically connect against the [Test-Nexus](\ref test/test-nexus.hpp), * so to be suitably rigged for unit testing. This means, there is no _live connection_ * to the session, but any command- or other messages will be captured and can be * retrieved or verified from the test code. Since lifecycle and robustness in @@ -362,10 +363,6 @@ namespace test { * * The second part of this test _replays_ such a state mark, which causes * the`doMark()` operation on the UI element to be invoked. - * @todo maybe we'll even provide a default implementation for expand/collapse - * which then means that, by replaying the mentioned state marks, the - * `doExpand()` or `doCollapse()` should be re-invoked, of course - * without issuing a further notification * @note this test does not cover or even emulate the operation of the * "state manager", since the goal is to cover the _UI element_ * protocol. We'll just listen at the bus and replay messages. @@ -473,6 +470,90 @@ namespace test { } + /** @test configure a handler for the (optional) "reveal yourself" functionality. + * We install a lambda to supply the actual implementation action, which can then + * either be triggered by a signal/slot invocation, or by sending a "state mark". + */ + void + revealer () + { + MARK_TEST_FUN + EventLog nexusLog = gui::test::Nexus::startNewLog(); + + MockElm mock("target"); + ID targetID = mock.getID(); + + sigc::signal trigger_reveal; + trigger_reveal.connect (sigc::mem_fun(mock, &Tangible::slotReveal)); + + CHECK (not mock.isTouched()); + CHECK (not mock.isExpanded()); + CHECK (mock.ensureNot("reveal")); + CHECK (mock.ensureNot("expanded")); + CHECK (nexusLog.ensureNot("state-mark")); + + bool revealed = false; + mock.installRevealer([&]() + { // NOTE: our mock "implementation" of the »reveal yourself« functionality + mock.slotExpand(); // explicitly prompts the element to expand itself, + revealed = true; // and then via closure sets a flag we can verify. + }); + + trigger_reveal(); + + CHECK (true == revealed); + CHECK (mock.isExpanded()); + CHECK (mock.verifyEvent("create","target") + .beforeCall("reveal") + .beforeCall("expand").arg(true) + .beforeEvent("expanded")); + + // invoking the slotExpand() also emitted a state mark to persist that expansion state... + CHECK (nexusLog.verifyCall("note").arg(targetID, GenNode{"expand", true}) + .before("handling state-mark")); + + + // second test: the same can be achieved via UI-Bus message... + revealed = false; + auto stateMark = GenNode{"reveal", 47}; // (payload argument irrelevant) + auto& uiBus = gui::test::Nexus::testUI(); + CHECK (nexusLog.ensureNot("reveal")); + + uiBus.mark (targetID, stateMark); // send the state mark message to reveal the element + + CHECK (true == revealed); + CHECK (mock.verifyMark("reveal", 47) + .afterEvent("expanded") + .beforeCall("reveal") + .beforeCall("expand").arg(true)); + + CHECK (nexusLog.verifyCall("mark").arg(targetID, stateMark) + .after("handling state-mark") + .before("reveal") + .beforeEvent("delivered mark")); + + // Note the fine point: the target element /was/ already expanded + // and thus there is no second "expanded" event, nor is there a + // second state mark emitted into the UI-Bus... + CHECK (mock.ensureNot("expanded") + .afterCall("reveal") + .afterEvent("expanded")); + CHECK (nexusLog.ensureNot("note") + .afterCall("mark").arg(targetID, stateMark) + .after("handling state-mark")); + + + + cout << "____Event-Log_________________\n" + << util::join(mock.getLog(), "\n") + << "\n───╼━━━━━━━━━╾────────────────"<>Scope>> b") // recursively mutate second child - .beforeEvent("diff","++Attib++ π = 3.1415927")); // insert new attribute π within nested scope + .beforeEvent("diff","++Attrib++ π = 3.1415927")); // insert new attribute π within nested scope CHECK (nexusLog.verifyCall("routeAdd").arg(rootMock.getID(), memLocation(rootMock)) // rootMock was attached to Nexus diff --git a/tests/gui/bus-term-test.cpp b/tests/gui/bus-term-test.cpp index a4a0153f5..931f20711 100644 --- a/tests/gui/bus-term-test.cpp +++ b/tests/gui/bus-term-test.cpp @@ -30,6 +30,7 @@ #include "lib/sync.hpp" #include "lib/sync-classlock.hpp" #include "backend/thread-wrapper.hpp" +#include "include/ui-protocol.hpp" #include "gui/ctrl/bus-term.hpp" #include "gui/ctrl/state-manager.hpp" #include "proc/control/command.hpp" @@ -112,7 +113,7 @@ namespace test { /**************************************************************************//** * @test cover the standard node element (terminal element) within the UI-Bus, * with the help of an attached mock UI element. Contrary to the related - * [ui-element test](AbstractTangible_test), here we focus on the bus side + * [ui-element test](\ref AbstractTangible_test), here we focus on the bus side * of the standard interactions. * * This test enacts the fundamental generic communication patterns @@ -150,7 +151,7 @@ namespace test { /** @test build a new BusTerm and verify connectivity. * Every [tangible UI-element](\ref Tangible) bears an embedded BusTerm * member. Since the latter _requires another, up-link BusTerm_ on construction, - * connection to the [UI-Bus](ui-bus.hpp) is structurally ensured. Moreover, + * connection to the [UI-Bus](\ref ui-bus.hpp) is structurally ensured. Moreover, * when hooking up a new UI-element, the initialisation of the embedded BusTerm * will cause a down-link connection to be installed into the central routing * table within the \ref Nexus, the hub of the UI-Bus. Routing and addressing @@ -190,8 +191,8 @@ namespace test { CHECK (nexusLog.verifyCall("note").on("TestNexus").arg(elmID, "GenNode-ID(\"expand\")-DataCap|«bool»|true")); // send a state mark down to the mock element - gui::test::Nexus::testUI().mark (elmID, GenNode("Flash", 23)); - CHECK (nexusLog.verifyCall("mark").on("TestNexus").arg(elmID, "Flash") + gui::test::Nexus::testUI().mark (elmID, GenNode(string{MARK_Flash}, 23)); + CHECK (nexusLog.verifyCall("mark").on("TestNexus").arg(elmID, MARK_Flash) .beforeEvent("TestNexus", "mark to bID-zeitgeist")); CHECK (elmLog.verifyCall("doFlash").on("zeitgeist")); @@ -202,9 +203,9 @@ namespace test { CHECK (nexusLog.verifyCall("routeDetach").on("TestNexus").arg(elmID) .beforeEvent("TestNexus", "removed route to bID-zeitgeist")); - gui::test::Nexus::testUI().mark (elmID, GenNode("Flash", 88)); + gui::test::Nexus::testUI().mark (elmID, GenNode({MARK_Flash}, 88)); CHECK (nexusLog.verify("removed route to bID-zeitgeist") - .beforeCall("mark").on("TestNexus").arg(elmID, "Flash") + .beforeCall("mark").on("TestNexus").arg(elmID, MARK_Flash) .beforeEvent("warn","discarding mark to unknown bID-zeitgeist")); CHECK (elmLog.ensureNot("Flash") .afterEvent("destroy","zeitgeist")); @@ -367,8 +368,8 @@ namespace test { CHECK (not mockB.isTouched()); CHECK (not mockC.isTouched()); - uiBus.mark (alpha, GenNode{"Message", "Centauri"}); - uiBus.mark (bravo, GenNode{"Flash", true}); + uiBus.mark (alpha, GenNode{"Message", "Centauri"}); + uiBus.mark (bravo, GenNode{"Flash", true}); uiBus.mark (charly, GenNode{"Message", "Delta"}); uiBus.mark (charly, GenNode{"Error", "Echo"}); @@ -497,8 +498,8 @@ namespace test { auto& stateManager = gui::test::Nexus::getMockStateManager(); CHECK (stateManager.currentState(alpha, "expand") == GenNode("expand", false )); CHECK (stateManager.currentState(bravo, "expand") == GenNode("expand", true )); - CHECK (stateManager.currentState(charly, "expand") == Ref::NO); - CHECK (stateManager.currentState(charly, "Error") == GenNode("Error", "Echo")); // sticky error state was recorded + CHECK (stateManager.currentState(charly,"expand") == Ref::NO); + CHECK (stateManager.currentState(charly, "Error") == GenNode("Error", "Echo")); // sticky error state was recorded // reset error state(s) uiBus.markAll (GenNode{"clearErr", true}); @@ -509,9 +510,9 @@ namespace test { CHECK (stateManager.currentState(alpha, "expand") == GenNode("expand", false )); CHECK (stateManager.currentState(bravo, "expand") == GenNode("expand", true )); - CHECK (stateManager.currentState(charly, "expand") == Ref::NO); - CHECK (stateManager.currentState(charly, "Error") == Ref::NO); // sticky error state was cleared, - // because charly sent a clearErr state mark notification back + CHECK (stateManager.currentState(charly,"expand") == Ref::NO); + CHECK (stateManager.currentState(charly, "Error") == Ref::NO); // sticky error state was cleared, + // because charly sent a clearErr state mark notification back // send global sweeping reset uiBus.markAll (GenNode{"reset", true}); diff --git a/tests/gui/model/element-access-test.cpp b/tests/gui/model/element-access-test.cpp index c87ad38a4..281aa98c5 100644 --- a/tests/gui/model/element-access-test.cpp +++ b/tests/gui/model/element-access-test.cpp @@ -79,7 +79,7 @@ namespace test { * rather than on the real UI topology. * @see GenNodeLocationQuery * - * @see [Mock testing support](test-element-access.hpp) + * @see [Mock testing support](\ref test-element-access.hpp) * @see id-scheme.hpp * @see ViewLocator * @see ViewSpecDSL_test diff --git a/tests/gui/model/w-link-test.cpp b/tests/gui/model/w-link-test.cpp new file mode 100644 index 000000000..41074c6c0 --- /dev/null +++ b/tests/gui/model/w-link-test.cpp @@ -0,0 +1,219 @@ +/* + WLink(Test) - verify managed link to sigc::trackable widget + + Copyright (C) Lumiera.org + 2018, 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 w-link-test.cpp + ** unit test \ref WLink_test + */ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "gui/model/w-link.hpp" +#include "lib/util.hpp" + +#include +#include + + +using util::isSameObject; +using std::make_unique; +using std::move; + + +namespace gui { +namespace model{ +namespace test { + + + namespace { // Test fixture... + + template + struct DummyWidget + : public sigc::trackable + { + X val = 1 + rand() % 100; + }; + } + + + + + /******************************************************************************//** + * @test verify proper behaviour of a smart-link to a `sigc::trackable` GTK widget. + * @see w-link.hpp + */ + class WLink_test : public Test + { + + virtual void + run (Arg) + { + verify_standardUsage(); + verify_reconnect(); + verify_copy(); + } + + + /** @test the standard use case is to hold onto a widget + * owned by _someone else_, with automatic disconnection + */ + void + verify_standardUsage() + { + using Wint = DummyWidget; + auto widget = make_unique(); + int r = widget->val; + + WLink link{*widget}; + CHECK (link); + link->val += 23; + CHECK (r+23 == widget->val); + + // kill widget + widget.reset(); + CHECK (not link); + VERIFY_ERROR (BOTTOM_VALUE, link->val ); + } + + + /** @test registration is automatically maintained when re-assigning targets */ + void + verify_reconnect() + { + using Wint = DummyWidget; + + auto w1 = make_unique(); + auto w2 = make_unique(); + int r1 = w1->val; + int r2 = w2->val; + + WLink l1; + WLink l2{*w1}; + CHECK (not l1); + CHECK ( l2); + + l2->val = r1; + l1.connect(*l2); + ++l1->val; + CHECK (w1->val == r1+1); + CHECK (isSameObject (*l1, *l2)); + + l2.connect(*w2); + CHECK (not isSameObject (*l1, *l2)); + w2->val = r2; + CHECK (r1+1 == l1->val); + CHECK (r2 == l2->val); + + w1.reset(); // kill first widget + CHECK (not l1); + CHECK ( l2); + l2->val *= -10; + l2.clear(); + CHECK (not l1); + CHECK (not l2); + CHECK (-10*r2 == w2->val); + l1.connect(*w2); + l2.connect(*l1); + CHECK (-10*r2 == l2->val); + CHECK (isSameObject (*l1, *l2)); + CHECK (isSameObject (*l1, *w2)); + + // implicitly kill second widget + *w2 = Wint{}; + CHECK (not l1); + CHECK (not l2); + } + + + /** @test registration state is properly handled on copy, move and swap */ + void + verify_copy() + { + using Wint = DummyWidget; + auto w1 = make_unique(); + auto w2 = make_unique(); + + WLink l1; + WLink l2{l1}; + CHECK (not l2); + l2.connect(*w1); + + WLink l3{l2}; + CHECK (l3); + CHECK (w1->val == l3->val); + + CHECK (not l1); // they are statefull and independent + l1 = move(l2); + CHECK (not l2); + CHECK (l1); + CHECK (isSameObject (*l1, *l3)); + + l2 = WLink{WLink{*w2}}; + CHECK (w2->val == l2->val); + + l1 = l3; + CHECK (w1->val == l1->val); + WLink& ref{l1}; + l1 = move(ref); + CHECK (w1->val == l1->val); + CHECK (w1->val == l3->val); + + std::swap (l2, l3); + CHECK (w1->val == l1->val); + CHECK (w1->val == l2->val); + CHECK (w2->val == l3->val); + + w1.reset(); + CHECK (not l1); + CHECK (not l2); + CHECK (w2->val == l3->val); + + using Wuint = DummyWidget; + auto uu = make_unique(); + WLink lu{*uu}; + +///////////////does not compile... +// l1 = uu; +// l1.connect(*uu); + + // But it is a compile time check... + // At runtime, only the bare pointer is managed + l1 = reinterpret_cast&&> (lu); + CHECK ((int)uu->val == l1->val); + CHECK (not lu); // assignment was actually a move + + // even the subversively attached link is managed properly + uu.reset(); + CHECK (not l1); + + // others unaffected... + CHECK (not l2); + CHECK (l3); + } + }; + + + /** Register this test class... */ + LAUNCHER (WLink_test, "unit gui"); + + +}}} // namespace gui::model::test diff --git a/tests/gui/test/mock-elm.hpp b/tests/gui/test/mock-elm.hpp index 374e70fc6..09cf2e996 100644 --- a/tests/gui/test/mock-elm.hpp +++ b/tests/gui/test/mock-elm.hpp @@ -61,6 +61,7 @@ #include "lib/error.hpp" +#include "include/ui-protocol.hpp" #include "lib/test/event-log.hpp" #include "gui/model/tangible.hpp" #include "lib/diff/record.hpp" @@ -101,7 +102,7 @@ namespace test{ * on the [Tangible] interface, which we mock here for unit testing. * This special implementation is instrumented to [log](\ref lib::test::EventLog) * any invocation and any messages sent or received through the UI Backbone, - * which is formed by the [UI-Bus](ui-bus.hpp). + * which is formed by the [UI-Bus](\ref ui-bus.hpp). * * @todo some usage details * @see abstract-tangible-test.cpp @@ -126,7 +127,7 @@ namespace test{ virtual bool doReset() override { - log_.call(this->identify(), "reset"); + log_.call(this->identify(), string{MARK_reset}); if (virgin_) return false; // there was nothing to reset @@ -134,33 +135,22 @@ namespace test{ message_ = ""; expanded_ = false; virgin_ = true; - log_.event("reset"); + log_.event(string{MARK_reset}); return true; // we did indeed reset something } // and thus a state mark should be captured virtual bool doExpand (bool yes) override { - log_.call(this->identify(), "expand", yes); - if (expanded_ == yes) - return false; // nothing to change - - virgin_ = false; - expanded_ = yes; - log_.event (expanded_? "expanded" : "collapsed"); - return true; // record a state change + log_.call(this->identify(), string{MARK_expand}, yes); + return Tangible::doExpand (yes); } virtual void - doReveal (ID child) override + doReveal() override { - UNIMPLEMENTED ("mock doReveal"); - } - - virtual void - doRevealYourself() override - { - UNIMPLEMENTED ("mock doRevealYourself"); + log_.call(this->identify(), string{MARK_reveal}); + Tangible::doReveal(); // NOTE: without specific configuration this is NOP } virtual bool @@ -281,14 +271,14 @@ namespace test{ { string key{spec.idi.getSym()}, val{render(spec.data)}; - log_.event("diff", "++Attib++ "+key+" = "+val); + log_.event("diff", "++Attrib++ "+key+" = "+val); return {key, val}; }) .assignElement ([&](Attrib& target, GenNode const& spec) -> bool { string key{spec.idi.getSym()}, newVal{render (spec.data)}; - log_.event("diff", "set Attib "+key+" <-"+newVal); + log_.event("diff", "set Attrib "+key+" <-"+newVal); target.second = newVal; return true; }))); @@ -317,6 +307,13 @@ namespace test{ { log_.call (this->identify(), "ctor", identity, string(nexus)); log_.create (getID().getSym()); + installExpander ([&](){ return this->expanded_; } + ,[&](bool yes) + { + virgin_ = false; + expanded_ = yes; + log_.event (expanded_? "expanded" : "collapsed"); + }); } diff --git a/tests/gui/test/test-nexus.cpp b/tests/gui/test/test-nexus.cpp index 5deb85cda..597cc3cd8 100644 --- a/tests/gui/test/test-nexus.cpp +++ b/tests/gui/test/test-nexus.cpp @@ -26,12 +26,12 @@ ** This compilation unit provides the actual setup for running a faked ** user interface from unit tests. Test code is assumed to access those ** features through the [front-end](\ref gui::test::TestNexus), while the - ** actual implementation instances are placed [as singletons](depend.hpp) + ** actual implementation instances are placed [as singletons](\ref depend.hpp) ** - ** This test setup will mostly treat messages similar to the [real UI-Bus hub](nexus.hpp), + ** This test setup will mostly treat messages similar to the [real UI-Bus hub](\ref nexus.hpp), ** with additional [logging](\ref event-log.hpp). Since the TestNexus runs as singleton, - ** there is a single shared "nexus-log", which can be [accessed](test::Nexus::getLog) or - ** even [cleared](test::Nexus::startNewLog) through the [static front-end](test::Nexus). + ** there is a single shared "nexus-log", which can be [accessed](\ref test::Nexus::getLog) or + ** even [cleared](\ref test::Nexus::startNewLog) through the [static front-end](\ref test::Nexus). ** But there is no connection to any [core services](\ref core-service.hpp), so neither ** commands nor state marks will be processed in any way. In case the unit tests need to ** integrate with or verify these handling operations, we provide the ability to install @@ -420,7 +420,7 @@ namespace test{ } /** - * similar to the [custom command handler](Nexus::setCommandHandler) + * similar to the [custom command handler](\ref Nexus::setCommandHandler) * this hook allows to install a closure to intercept any * "state mark" messages passed over the test-UI-Bus */ diff --git a/tests/gui/test/test-nexus.hpp b/tests/gui/test/test-nexus.hpp index 73639ac6f..3cb9c8a15 100644 --- a/tests/gui/test/test-nexus.hpp +++ b/tests/gui/test/test-nexus.hpp @@ -23,7 +23,7 @@ /** @file test/test-nexus.hpp ** A fake UI backbone for investigations and unit testing. - ** Any relevant element within the Lumiera GTK UI is connected to the [UI-Bus](ui-bus.hpp) + ** Any relevant element within the Lumiera GTK UI is connected to the [UI-Bus](\ref ui-bus.hpp) ** So for testing and investigation we need a white room setup to provide an instrumented ** backbone to run any test probes against. The test::Nexus allows to [hook up](\ref testUI()) ** a generic interface element, to participate in a simulated interface interaction. @@ -64,7 +64,7 @@ namespace test{ /** * Mock UI backbone for unit testing. - * In the absence of a real UI, this simulated [UI-Bus](ui-bus.hpp) + * In the absence of a real UI, this simulated [UI-Bus](\ref ui-bus.hpp) * can be used to wire a [test probe](\ref MockElm) and address it in unit testing. * * @note behind the scenes, this is a singleton. Use the provided diff --git a/tests/library/diff/diff-ignore-changes-test.cpp b/tests/library/diff/diff-ignore-changes-test.cpp new file mode 100644 index 000000000..95c4f3a16 --- /dev/null +++ b/tests/library/diff/diff-ignore-changes-test.cpp @@ -0,0 +1,253 @@ +/* + DiffIgnoreChanges(Test) - apply diff to black hole + + Copyright (C) Lumiera.org + 2018, 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 diff-ignore-changes-test.cpp + ** unit test \ref DiffIgnoreChanges_test. + ** Covers the special case of a TreeMutator configured to accept + ** any diff without actually doing anything. + */ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/format-util.hpp" +#include "lib/diff/tree-diff-application.hpp" +#include "lib/diff/test-mutation-target.hpp" +#include "lib/format-string.hpp" +#include "lib/format-cout.hpp" +#include "lib/util.hpp" + +#include + +using lib::iter_stl::snapshot; +using lib::time::Time; +using std::string; + + +namespace lib { +namespace diff{ +namespace test{ + + namespace {//Test fixture.... + + // define some GenNode elements + // to act as templates within the concrete diff + // NOTE: everything in this diff language is by-value + const GenNode ATTRIB1("α", 1), // attribute α = 1 + ATTRIB2("β", int64_t(2)), // attribute α = 2L (int64_t) + ATTRIB3("γ", 3.45), // attribute γ = 3.45 (double) + TYPE_X("type", "ξ"), // a "magic" type attribute "Xi" + TYPE_Z("type", "ζ"), // + CHILD_A("a"), // unnamed string child node + CHILD_B('b'), // unnamed char child node + CHILD_T(Time(12,34,56,78)), // unnamed time value child + SUB_NODE = MakeRec().genNode(), // empty anonymous node used to open a sub scope + ATTRIB_NODE = MakeRec().genNode("δ"), // empty named node to be attached as attribute δ + GAMMA_PI("γ", 3.14159265); // happens to have the same identity (ID) as ATTRIB3 + + }//(End)Test fixture + + + + + + + + /**************************************************************************//** + * @test Special case: build a TreeMutator to accept any change and do nothing. + * + * @note for sake of symmetry, this test uses the same verb sequence used in + * all the other tree diff tests, assuming this sequence covers pretty + * much all features supported by the tree diff language. + * + * @see DiffComplexApplication_test test case which _indeed does a lot..._ + * @see TreeMutator_test base operations of the adapter + * @see diff-tree-application.hpp + * @see tree-diff.hpp + */ + class DiffIgnoreChanges_test + : public Test + , TreeDiffLanguage + { + using DiffSeq = iter_stl::IterSnapshot; + + DiffSeq + populationDiff() + { + return snapshot({ins(ATTRIB1) + , ins(ATTRIB3) + , ins(ATTRIB3) + , ins(CHILD_B) + , ins(CHILD_B) + , ins(CHILD_T) + }); + } // ==> ATTRIB1, ATTRIB3, (ATTRIB3), CHILD_B, CHILD_B, CHILD_T + + DiffSeq + reorderingDiff() + { + return snapshot({after(Ref::ATTRIBS) + , ins(ATTRIB2) + , del(CHILD_B) + , ins(SUB_NODE) + , find(CHILD_T) + , pick(CHILD_B) + , skip(CHILD_T) + }); + } // ==> ATTRIB1, ATTRIB3, (ATTRIB3), ATTRIB2, SUB_NODE, CHILD_T, CHILD_B + + DiffSeq + mutationDiff() + { + return snapshot({after(CHILD_B) + , after(Ref::END) + , set(GAMMA_PI) + , mut(SUB_NODE) + , ins(TYPE_X) + , ins(ATTRIB2) + , ins(CHILD_B) + , ins(CHILD_A) + , emu(SUB_NODE) + , ins(ATTRIB_NODE) + , mut(ATTRIB_NODE) + , ins(TYPE_Z) + , ins(CHILD_A) + , ins(CHILD_A) + , ins(CHILD_A) + , emu(ATTRIB_NODE) + }); + } // ==> ATTRIB1, ATTRIB3 := π, (ATTRIB3), ATTRIB2, + // ATTRIB_NODE{ type ζ, CHILD_A, CHILD_A, CHILD_A } + // SUB_NODE{ type ξ, ATTRIB2, CHILD_B, CHILD_A }, + // CHILD_T, CHILD_B + + + + + virtual void + run (Arg) + { + fail_or_ignore(); + fish_for_content(); + } + + + /** @test fail or ignore, depending on toggle. + * - the TreeMutator default implementation produces a failure, + * when it actually has to handle some diff verb + * - yet a custom TreeMutator can be configured to `ignoreAllChanges()`, + * in which case it will consume any diff without effect. + * This test also demonstrates that the actual TreeMutator is built anew + * for each diff application (TreeMutator is meant to be disposable). + * Thus we may alter the behaviour of the diff binding dynamically. + * @remark the actual use case for this is the TimelineGui, which + * either forwards changes to a TimelineWidget, or silently + * ignores them when the corresponding timeline is not opened. + */ + void + fail_or_ignore() + { + struct HappyBlackHole + { + bool diligent = true; + + void + buildMutator (TreeMutator::Handle buff) + { + if (diligent) + buff.create( + TreeMutator()); + else + buff.create( + TreeMutator::build() + .ignoreAllChanges()); + } + }; + + HappyBlackHole subject; + DiffApplicator application(subject); + + VERIFY_ERROR(DIFF_CONFLICT, application.consume(populationDiff()) ); + VERIFY_ERROR(DIFF_CONFLICT, application.consume(reorderingDiff()) ); + VERIFY_ERROR(DIFF_CONFLICT, application.consume(mutationDiff()) ); + + subject.diligent = false; + + application.consume(populationDiff()); + application.consume(reorderingDiff()); + application.consume(mutationDiff()); + } + + + /** @test fish some content and ignore everything else. + * While the BlackHoleMutation binding generated by `ignoreAllChanges()` + * must be used as bottom layer of a custom TreeMutator, it is possible + * to layer a partial diff binding on top. This test demonstrates this + * with a single attribute binding, which just "fishes" any value + * mentioned in the diff for that specific attribute. Any other + * changes are silently ignored non the less... + */ + void + fish_for_content() + { + struct Scrounger + { + double loot = 0.0; + + void + buildMutator (TreeMutator::Handle buff) + { + buff.create( + TreeMutator::build() + .ignoreAllChanges() + .change("γ", [&](double val) { loot = val; })); + } + }; + + Scrounger subject; + DiffApplicator application(subject); + + const auto NOTHING = 0.0; + const auto VAL_1 = ATTRIB3.data.get(); + const auto VAL_2 = GAMMA_PI.data.get(); + + CHECK (subject.loot == NOTHING); + + application.consume(populationDiff()); + CHECK (subject.loot == VAL_1); + + application.consume(reorderingDiff()); + CHECK (subject.loot == VAL_1); + + application.consume(mutationDiff()); + CHECK (subject.loot == VAL_2); + } + + }; + + + /** Register this test class... */ + LAUNCHER (DiffIgnoreChanges_test, "unit common"); + + + +}}} // namespace lib::diff::test diff --git a/tests/library/diff/gen-node-basic-test.cpp b/tests/library/diff/gen-node-basic-test.cpp index 2b73c72f8..5967d656b 100644 --- a/tests/library/diff/gen-node-basic-test.cpp +++ b/tests/library/diff/gen-node-basic-test.cpp @@ -211,6 +211,15 @@ namespace test{ // but while o4 was based on o2, // adding all the additional contents didn't mutate o2 CHECK (isnil(o2.data.get())); + + // special case: can create an (Attribute) GenNode with specifically crafted ID + idi::EntryID veryspecialID{"quasi niente"}; + auto o5 = MakeRec().genNode(veryspecialID); + CHECK (o5 != MakeRec().genNode()); + CHECK (o5 != MakeRec().genNode("quasi niente")); + CHECK (o5 == MakeRec().genNode(veryspecialID)); + CHECK (name(o5) == "quasi_niente"); // Note: EntryID sanitised the string + CHECK (o5.idi == veryspecialID); } diff --git a/tests/library/diff/mutation-message-test.cpp b/tests/library/diff/mutation-message-test.cpp index 6cd9a59cb..3bad00977 100644 --- a/tests/library/diff/mutation-message-test.cpp +++ b/tests/library/diff/mutation-message-test.cpp @@ -1,5 +1,5 @@ /* - MutationMessage(Test) - demonstrate the basics of tree diff representation + MutationMessage(Test) - verify diff mutation message container Copyright (C) Lumiera.org 2017, Hermann Vosseler diff --git a/tests/library/iter-chain-search-test.cpp b/tests/library/iter-chain-search-test.cpp new file mode 100644 index 000000000..714af1695 --- /dev/null +++ b/tests/library/iter-chain-search-test.cpp @@ -0,0 +1,225 @@ +/* + IterChainSearch(Test) - verify chained search operations with backtracking + + Copyright (C) Lumiera.org + 2018, 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 iter-chain-search-test.cpp + ** unit test \ref IterChainSearch_test + */ + + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/format-util.hpp" +#include "lib/iter-chain-search.hpp" +#include "lib/iter-cursor.hpp" +#include "lib/util.hpp" + +#include +#include + + + +namespace lib { +namespace iter{ +namespace test{ + + using ::Test; + using util::join; + using util::isnil; + using util::startsWith; + using util::isSameObject; + using std::vector; + using std::string; + + using lumiera::error::LERR_(ITER_EXHAUST); + + + namespace { // test fixture + + using Spam = vector; + + const Spam SPAM{"spam" + ,"sausage" + ,"spam" + ,"spam" + ,"bacon" + ,"spam" + ,"tomato" + ,"and" + ,"spam" + }; + + + /** Diagnostic helper: join all the elements from a _copy_ of the iterator */ + template + inline string + materialise (II&& ii) + { + return util::join (std::forward (ii), "-"); + } + }// (END)fixture + + + + + + + + /*****************************************************************//** + * @test verify a setup for consecutive searches with backtracking. + * - demonstrate simple consecutive matches and iterator behaviour + * - clear the filter underway and thus return to simple iteration + * - set up two conditions, the second one capturing state at the + * point where the first one matches + * - wrap a lib::IterCursor, which allows to toggle the search + * direction underway; this creates a situation where the + * first options picked do not lead to a successful solution, + * so the search has to track back, try further options and + * in each case re-apply all the following consecutive + * search conditions. + * + * @see iter-chain-search.hpp + * @see iter-cursor.hpp + * @see [usage example](\ref event-log.hpp) + */ + class IterChainSearch_test : public Test + { + + virtual void + run (Arg) + { + simpleSearch(); + chainedIteration(); + backtracking(); + } + + + /** @test simple basic use case. */ + void + simpleSearch () + { + auto search = chainSearch(SPAM) + .search("bacon") + .search("tomato"); + + CHECK (search); + CHECK (not isnil(search)); + CHECK ("tomato" == *search); + CHECK (isSameObject (*search, SPAM[6])); + + search.clearFilter(); + CHECK ("tomato" == *search); + ++search; + CHECK ("and" == *search); + search.search("spam"); + CHECK ("spam" == *search); + CHECK (isSameObject (*search, SPAM[8])); + + ++search; + CHECK (not search); + CHECK (isnil (search)); + VERIFY_ERROR (ITER_EXHAUST, *search); + } + + + + /** @test verify consecutive application of several functors on the underlying filter. + * In the general case, each step in the chain is a function working on a copy of the + * current filter state. Since each such step configures its own copy of the complete + * pipeline, it may reconfigure this filter pipeline in arbitrary ways. After exhausting + * the last layer, the evaluation returns to the previous layer, but immediately re-applies + * the configuration step on the then next element. + */ + void + chainedIteration () + { + auto search = chainSearch(SPAM) // Note: 1st filter step picks all s-words + .search([](string const& str){ return startsWith (str, "s"); }); + + CHECK (materialise (search) =="spam-sausage-spam-spam-spam-spam"); + CHECK ("spam" == *search); + + search.addStep([](auto& filter) + { // Note: pick the current value at the point + string currVal = *filter; // where the 2nd filter step is (re)applied + filter.setNewFilter ([=](string const& val) // ...and bake this value into the lambda closure + { + return val != currVal; + }); + }); + + CHECK ("sausage" == *search); + CHECK (materialise (search) + == "sausage-bacon-tomato-and-" // everything in the rest, which is not "spam" + "spam-spam-bacon-spam-tomato-and-spam-" // everything starting at "sausage" which is not "sausage" + "bacon-tomato-and-" // any non-spam behind the 2nd spam + "bacon-tomato-and-" // any non-spam behind the 3rd spam + "tomato-and" // any non-spam behind the 4th spam + ""); // and any non-spam behind the final spam + } + + + + /** @test verify a complex search with backtracking. + * This becomes relevant when a given search condition can be "too greedy" for the + * complete chain to succeed. Most notably this is the case when the search is fundamentally + * reconfigured in some steps, e.g. by switching the search orientation. To demonstrate this, + * we use a "gear switching" iterator, which allows us to reverse the direction and to search + * backwards from the current position. We configure the second condition in the chain such + * that it can not succeed when starting from the first match on the first condition + */ + void + backtracking () + { + using Cursor = IterCursor; + + auto search = chainSearch(Cursor{SPAM.begin(), SPAM.end()}) + .search("spam") + .addStep([](auto& filter) + { + filter.switchBackwards(); // switch search direction without moving the cursor + filter.flipFilter(); // flip from match on "spam" to match on non-spam + }) + .addStep([](auto& filter) + { // filter is still configured to search non-spam backwards + ++filter; // just "advance" this filter by one step (backward) + }); + + CHECK (materialise (search) // Layer-0: 1-3 spam fail altogether, too greedy. Try 4rd spam.... + // Layer-1: turn back, toggle to non-spam, find bacon + == "sausage-" // Layer-2: non-spam and one step further backwards yields sausage + // + // BACKTRACK to Layer-0: pick 5th (and last) spam... + // Layer-1: turn back, toggle to non-spam, find "and" + "tomato-bacon-sausage-" // Layer-2: non-spam and one step back yields tomato, next bacon, next sausage. + // BACKTRACK to Layer-1: take previous one, which is tomato + "bacon-sausage-" // Layer-2: non-spam and one step further back yields bacon, then next sausage. + // BACKTRACK to Layer-1: take previous non-spam, which is bacon + "sausage" // Layer-2: non-spam and one step further back yields sausage. + ""); // BACKTRACK to Layer-1: exhausted, BACKTRACK to Layer-0: exhausted. Done. + } + }; + + LAUNCHER (IterChainSearch_test, "unit common"); + + +}}} // namespace lib::iter::test diff --git a/tests/library/iter-cursor-test.cpp b/tests/library/iter-cursor-test.cpp index ef3b19195..b70f514d9 100644 --- a/tests/library/iter-cursor-test.cpp +++ b/tests/library/iter-cursor-test.cpp @@ -78,7 +78,7 @@ namespace test{ * * @see iter-cursor.hpp * @see iter-adapter.hpp - * @see [usage example](event-log.hpp) + * @see [usage example](\ref event-log.hpp) */ class IterCursor_test : public Test { diff --git a/tests/library/iter-tree-explorer-test.cpp b/tests/library/iter-tree-explorer-test.cpp index 3425789c6..cb8fc8470 100644 --- a/tests/library/iter-tree-explorer-test.cpp +++ b/tests/library/iter-tree-explorer-test.cpp @@ -218,42 +218,46 @@ namespace test{ /*******************************************************************//** * @test use a simple source iterator yielding numbers - * to build various functional evaluation structures, - * based on the \ref IterExplorer template. - * - the [state adapter](\ref verifyStateAdapter() ) - * iterator construction pattern - * - helper to [chain iterators](\ref verifyChainedIterators() ) - * - building [tree exploring structures](\ref verifyDepthFirstExploration()) - * - the [monadic nature](\ref verifyMonadOperator()) of IterExplorer - * - a [recursively self-integrating](\ref verifyRecrusiveSelfIntegration()) - * evaluation pattern + * to build various functional evaluation pipelines, + * based on the \ref TreeExplorer template. + * - the adapter to wrap the source, which can either + * [be a state core](\ref verify_wrappedState() ), or can + * [be a Lumiera Forward Iterator](\ref verify_wrappedIterator() ) + * - the defining use case for TreeExplorer is to build a + * [pipeline for depth-first exploration](\ref verify_expandOperation() ) + * of a (functional) tree structure. This "tree" is created by invoking + * a "expand functor", which can be defined in various ways. + * - the usual building blocks for functional evaluation pipelines, that is + * [filtering](\ref verify_FilterIterator() ) and + * [transforming](\ref verify_transformOperation() ) of + * the elements yielded by the wrapped source iterator. + * - building complex pipelines by combining the aforementioned building blocks + * - using an opaque source, hidden behind the IterSource interface, and + * an extension (sub interface) to allow for "tree exploration" without + * any knowledge regarding the concrete implementation of the data source. * * ## Explanation * - * Both this test and the IterExplorer template might be bewildering - * and cryptic, unless you know the *Monad design pattern*. »Monads« - * are heavily used in functional programming, actually they originate - * from Category Theory. Basically, Monad is a pattern where we combine - * several computation steps in a specific way; but instead of intermingling - * the individual computation steps and their combination, the goal is to - * isolate and separate the _mechanics of combination_, so we can focus on - * the actual _computation steps:_ The mechanics of combination are embedded - * into the Monad type, which acts as a kind of container, holding some entities - * to be processed. The actual processing steps are then attached to the monad as - * "function object" parameters. It is up to the monad to decide if, and when - * those processing steps are applied to the embedded values and how to combine - * the results into a new monad. + * These tests build a evaluation pipeline by _wrapping_ some kind of data source + * and then layering some evaluation stages on top. There are two motivations why + * one might want to build such a _filter pipeline:_ + * - on demand processing ("pull principle") + * - separation of source computation and "evaluation mechanics" + * when building complex search and backtracking algorithms. * - * Using the monad pattern is well suited when both the mechanics of - * combination and the individual computation steps tend to be complex. - * In such a situation, it is beneficial to develop and test both - * in isolation. The IterExplorer template applies this pattern - * to the task of processing a source sequence. Typically we use - * this in situations where we can't afford building elaborate - * data structures in (global) memory, but rather strive at - * doing everything on-the-fly. A typical example is the - * processing of a variably sized data set without - * using heap memory for intermediary results. + * This usage style is inspired from the *Monad design pattern*. In our case here, + * the Iterator pipeline would be the monad, and can be augmented and reshaped by + * attaching further processing steps. How those processing steps are to be applied + * remains an internal detail, defined by the processing pipeline. »Monads« are heavily + * used in functional programming, actually they originate from Category Theory. Basically, + * Monad is a pattern where we combine several computation steps in a specific way; but + * instead of intermingling the individual computation steps and their combination, + * the goal is to isolate and separate the _mechanics of combination_, so we can focus + * on the actual _computation steps:_ The mechanics of combination are embedded into the + * Monad type, which acts as a kind of container, holding some entities to be processed. + * The actual processing steps are then attached to the monad as "function object" parameters. + * It is up to the monad to decide if, and when those processing steps are applied to the + * embedded values and how to combine the results into a new monad. * * @see TreeExplorer * @see IterAdapter @@ -268,10 +272,13 @@ namespace test{ verify_wrappedIterator(); verify_expandOperation(); + verify_expand_rootCurrent(); verify_transformOperation(); verify_combinedExpandTransform(); + verify_customProcessingLayer(); verify_scheduledExpansion(); verify_FilterIterator(); + verify_FilterChanges(); verify_asIterSource(); verify_IterSource(); @@ -423,8 +430,6 @@ namespace test{ .expand([](CountDown core){ return NumberSequence{ core.yield() - 1}; }) // expand-functor: StateCore -> Iter ); -#if false /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1118 : GDB Segfault on loading the inferior - /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1118 : Generated code works just fine and passes Test though verify_treeExpandingIterator( treeExplore(CountDown{5}) .expand([](auto & it){ return CountDown{ *it - 1}; }) // generic Lambda: Iter& -> StateCore @@ -434,13 +439,12 @@ namespace test{ treeExplore(CountDown{5}) .expand([](auto it){ return decltype(it){ *it - 1}; }) // generic Lambda: Iter -> Iter ); -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1118 : GDB Segfault on loading the inferior } template void - verify_treeExpandingIterator(EXP ii) + verify_treeExpandingIterator (EXP ii) { CHECK (!isnil (ii)); CHECK (5 == *ii); @@ -484,6 +488,53 @@ namespace test{ } + /** @test special feature of the Expander to lock into current child sequence. + * This feature was added to support a specific use-case in the IterChainSearch component. + * After expanding several levels deep into a tree, it allows to turn the _current child sequence_ + * into a new root sequence and discard the whole rest of the tree, including the original root sequence. + * It is implemented by moving the current child sequence down into the root sequence. We demonstrate + * this behaviour with the simple standard setup from #verify_expandOperation() + */ + void + verify_expand_rootCurrent() + { + auto tree = treeExplore(CountDown{25}) + .expand([](uint j){ return CountDown{j-1}; }); + + CHECK (materialise(tree) == "25-24-23-22-21-20-19-18-17-16-15-14-13-12-11-10-9-8-7-6-5-4-3-2-1"); + + CHECK (0 == tree.depth()); + CHECK (25 == *tree); + ++tree; + ++tree; + ++tree; + ++tree; + CHECK (21 == *tree); + tree.expandChildren(); + CHECK (1 == tree.depth()); + ++tree; + ++tree; + ++tree; + ++tree; + ++tree; + CHECK (15 == *tree); + tree.expandChildren(); + ++tree; + ++tree; + CHECK (2 == tree.depth()); + CHECK (materialise(tree) == "12-11-10-9-8-7-6-5-4-3-2-1-" // this is the level-2 child sequence + "14-13-12-11-10-9-8-7-6-5-4-3-2-1-" // ...returning to the rest of the level-1 sequence + "20-19-18-17-16-15-14-13-12-11-10-9-8-7-6-5-4-3-2-1"); // ...followed by the rest of the original root sequence + CHECK (12 == *tree); + + tree.rootCurrent(); + CHECK (12 == *tree); + CHECK (materialise(tree) == "12-11-10-9-8-7-6-5-4-3-2-1"); // note: level-2 continues unaltered, but level-1 and the original root are gone. + CHECK (0 == tree.depth()); + } + + + /** @test pipe each result through a transformation function. * The _transforming iterator_ is added as a decorator, wrapping the original iterator, * TreeEplorer or state core. As you'd expect, the given functor is required to accept @@ -512,6 +563,7 @@ namespace test{ .transform(multiply) ; + CHECK (14 == *ii); CHECK (14 == *ii); ++ii; CHECK (12 == *ii); @@ -620,7 +672,7 @@ namespace test{ * matching algorithms. Even more so, when other operations like filtering are intermingled; * in that case it might even happen that the downstream consumer does not even see the * items resulting from child expansion, because they are evaluated and then filtered - * away by a transformer and filter placed in between. + * away by transformers and filters placed in between. * @note as a consequence of the flexible automatic adapting of bound functors, it is * possible for bound functors within different "layers" to collaborate, based on * additional knowledge regarding the embedded data source internals. This test @@ -669,6 +721,50 @@ namespace test{ } + /** + * demo of a custom processing layer + * interacting directly with the iteration mechanism. + * @note we can assume `SRC` is itself a Lumiera Iterator + */ + template + struct MagicTestRubbish + : public SRC + { + using SRC::SRC; + + void + iterNext() + { + ++(*this); + if (*this) + ++(*this); + } + }; + + /** @test extension point to inject a client-defined custom processing layer + * This special builder function allows to install a template, which needs to wrap + * a source iterator and expose a _state core like_ interface. We demonstrate this + * extension mechanism here by defining a processing layer which skips each other element. + */ + void + verify_customProcessingLayer() + { + CHECK (materialise( + treeExplore(CountDown{7}) + .processingLayer() + ) + == "7-5-3-1"); + + CHECK (materialise( + treeExplore(CountDown{7}) + .transform([](uint v){ return 2*v; }) + .processingLayer() + .filter([](int v){ return v % 3; }) + ) + == "14-10-2"); + } + + /** @test child expansion can be scheduled to happen on next iteration. * As such, _"child expansion"_ happens right away, thereby consuming a node @@ -779,6 +875,18 @@ namespace test{ + // contrived example to verify interplay of filtering and child expansion; + // especially note that the filter is re-evaluated after expansion happened. + CHECK (materialise ( + treeExplore(CountDown{10}) + .expand([](uint i){ return CountDown{i%4==0? i-1 : 0}; }) // generate subtree at 8 and 4 ==> 10-9-8-7-6-5-4-3-2-1-3-2-1-7-6-5-4-3-2-1-3-2-1 + .filter([](uint i){ return i%2 == 0; }) + .expandAll() // Note: sends the expandChildren down through the filter + ) + == "10-8-6-4-2-2-6-4-2-2"); + + + // another convoluted example to demonstrate // - a filter predicate with side-effect // - and moreover the predicate is a generic lambda @@ -831,6 +939,94 @@ namespace test{ + /** @test a special filter layer which can be re-configured on the fly */ + void + verify_FilterChanges() + { + auto seq = treeExplore(CountDown{20}) + .mutableFilter(); + + auto takeEve = [](uint i){ return i%2 == 0; }; + auto takeTrd = [](uint i){ return i%3 == 0; }; + + CHECK (20 == *seq); + ++seq; + CHECK (19 == *seq); + CHECK (19 == *seq); + + seq.andFilter (takeEve); + CHECK (18 == *seq); + ++seq; + CHECK (16 == *seq); + + seq.andFilter (takeTrd); + CHECK (12 == *seq); // is divisible (by 2 AND by 3) + + seq.flipFilter(); + CHECK (11 == *seq); // not divisible (by 2 AND by 3) + ++seq; + CHECK (10 == *seq); + + seq.setNewFilter (takeTrd); + CHECK ( 9 == *seq); + ++seq; + CHECK ( 6 == *seq); + + seq.orNotFilter (takeEve); + CHECK ( 6 == *seq); + ++seq; + CHECK ( 5 == *seq); // disjunctive condition actually weakens the filter + ++seq; + CHECK ( 3 == *seq); + + // NOTE: arbitrary functors can be used/combined, + // since they are adapted individually. + // To demonstrate this, we use a functor accessing and + // manipulating the state core by side effect... + string buff{"."}; + seq.andNotFilter ([&](CountDown& core) + { + buff += util::toString(core.p) + "."; + --core.p; // manipulate state core + return core.p % 2; // return a number, not bool + }); + + CHECK ( 2 == *seq); // value in the core has been manipulated + CHECK (".3." == buff); // the filter has been invoked once, and saw core == 3 + + ++seq; // core == 2 is filtered by the existing other filter (== not take even) + CHECK (".3.1." == buff); // the filter has been invoked again, and saw core == 1 + CHECK (0 == seq.p); // ...which he manipulated, so that core == 0 + CHECK (isnil (seq)); // .....and thus iteration end is detected + VERIFY_ERROR (ITER_EXHAUST, *seq ); + + + // verify enabling and disabling... + seq = treeExplore(CountDown{10}) + .mutableFilter(takeTrd); + + CHECK (9 == *seq); + seq.disableFilter(); + CHECK (9 == *seq); + ++seq; + CHECK (8 == *seq); + seq.andNotFilter (takeEve); + CHECK (7 == *seq); + ++seq; + CHECK (5 == *seq); + seq.disableFilter(); + CHECK (5 == *seq); + ++seq; + CHECK (4 == *seq); + ++seq; + CHECK (3 == *seq); + seq.flipFilter(); // everything rejected + CHECK (isnil (seq)); + } + + + + /** @test package the resulting Iterator as automatically managed, * polymorphic opaque entity implementing the IterSource interface. * The builder operations on TreeExplorer each generate a distinct, implementation @@ -1106,7 +1302,7 @@ namespace test{ bool checkPoint() const { - return src; + return bool{src}; } State& diff --git a/tests/library/test/test-event-log-test.cpp b/tests/library/test/test-event-log-test.cpp index 18ac7c2ac..f8293bbec 100644 --- a/tests/library/test/test-event-log-test.cpp +++ b/tests/library/test/test-event-log-test.cpp @@ -108,7 +108,16 @@ namespace test{ CHECK (log.verify("ham").after("spam").after("beans")); CHECK (log.verify("ham").after("beans").before("spam").before("ham")); CHECK (not log.verify("spam").after("beans").after("ham")); - } + + log.event("beans"); + CHECK (log.verify("beans").after("spam")); // Note: Backtracking! The first match on beans fails, + // only the match on second beans succeeds. + + // consecutive matches always move by at least one step + CHECK ( log.verify("beans").after("ham").after("spam") .after("baked")); + CHECK (not log.verify("beans").after("ham").after("spam").after("spam").after("baked")); + CHECK ( log.verify("beans").after("ham").after("spam").locate("spam").locate("spam").after("baked")); + } // ^^^^^^ locate re-applies at current pos without moving void @@ -339,14 +348,12 @@ namespace test{ "with shallots and aubergines garnished with truffle pate, brandy and with a fried egg on top and spam"); CHECK (log.verify("spam").before("(spam|").before("egg on top and spam")); + CHECK (log.verify("and spam").after("(spam|").after("spam!").before("bacon")); CHECK (log.ensureNot("and spam").after("(spam|").after("spam!").after("bacon")); - // note: each consecutive match starts with the same element, on which the previous one succeeded - CHECK (log.verify("spam").before("spam").before("spam").before("spam").before("spam").before("bacon")); - // RegExp on full String representation CHECK (log.verifyMatch("spam.+spam")); - CHECK (log.verifyMatch("spam.+spam").beforeMatch("spam(?!spam)")); + CHECK (log.verifyMatch("spam.+spam").beforeMatch("spam(?!.+spam)")); CHECK (log.verifyEvent("fatal","spam").afterMatch("(spam.*){15}")); // Cover all arguments with sequence of regular expressions diff --git a/tests/library/util-parse-bool-test.cpp b/tests/library/util-parse-bool-test.cpp new file mode 100644 index 000000000..10a7d4db6 --- /dev/null +++ b/tests/library/util-parse-bool-test.cpp @@ -0,0 +1,106 @@ +/* + UtilParseBool(Test) - derive bool value from text form + + Copyright (C) Lumiera.org + 2018, 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 util-parse-bool-test.cpp + ** unit test \ref UtilParseBool_test + */ + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/util.hpp" + +#include + + +using lumiera::error::LUMIERA_ERROR_INVALID; + + +namespace util { +namespace test { + + + class UtilParseBool_test : public Test + { + virtual void + run (Arg) + { + CHECK (boolVal ("true")); + CHECK (boolVal ("True")); + CHECK (boolVal ("TRUE")); + CHECK (boolVal ("yes")); + CHECK (boolVal ("Yes")); + CHECK (boolVal ("YES")); + CHECK (boolVal ("1")); + CHECK (boolVal ("+")); + + CHECK (not boolVal ("false")); + CHECK (not boolVal ("False")); + CHECK (not boolVal ("FALSE")); + CHECK (not boolVal ("no")); + CHECK (not boolVal ("No")); + CHECK (not boolVal ("NO")); + CHECK (not boolVal ("0")); + CHECK (not boolVal ("-")); + + CHECK (boolVal ("yes ")); + CHECK (boolVal (" Yes")); + CHECK (boolVal (" + ")); + CHECK (not boolVal (" \n0 ")); + + VERIFY_ERROR (INVALID, boolVal("") ); + VERIFY_ERROR (INVALID, boolVal(" ") ); + VERIFY_ERROR (INVALID, boolVal("yEs") ); + VERIFY_ERROR (INVALID, boolVal("tRuE") ); + VERIFY_ERROR (INVALID, boolVal("falsehood")); + VERIFY_ERROR (INVALID, boolVal("11") ); + VERIFY_ERROR (INVALID, boolVal("+1") ); + VERIFY_ERROR (INVALID, boolVal("↯") ); + + + CHECK (isYes ("true")); + CHECK (isYes ("True")); + CHECK (isYes ("TRUE")); + CHECK (isYes ("yes")); + CHECK (isYes ("Yes")); + CHECK (isYes ("1")); + CHECK (isYes ("+")); + + CHECK (isYes (" True ")); + CHECK (isYes (" \n\n 1 \t ")); + + CHECK (not isYes (" True and False")); + CHECK (not isYes ("tRuE")); + CHECK (not isYes ("+2")); + CHECK (not isYes ("no")); + CHECK (not isYes ("1010")); + CHECK (not isYes ("↯")); + CHECK (not isYes (" ")); + CHECK (not isYes ("")); + } + }; + + LAUNCHER (UtilParseBool_test, "unit common"); + + +}} // namespace util::test + diff --git a/tests/library/sanitised-identifier-test.cpp b/tests/library/util-sanitised-identifier-test.cpp similarity index 82% rename from tests/library/sanitised-identifier-test.cpp rename to tests/library/util-sanitised-identifier-test.cpp index 7d7ce6d18..bf8288715 100644 --- a/tests/library/sanitised-identifier-test.cpp +++ b/tests/library/util-sanitised-identifier-test.cpp @@ -1,5 +1,5 @@ /* - SanitizedIdentifier(Test) - remove non-standard-chars and punctuation + UtilSanitizedIdentifier(Test) - remove non-standard-chars and punctuation Copyright (C) Lumiera.org 2008, Hermann Vosseler @@ -20,8 +20,8 @@ * *****************************************************/ -/** @file sanitised-identifier-test.cpp - ** unit test \ref SanitizedIdentifier_test +/** @file util-sanitised-identifier-test.cpp + ** unit test \ref UtilSanitizedIdentifier_test */ @@ -38,7 +38,7 @@ namespace util { namespace test { - class SanitizedIdentifier_test : public Test + class UtilSanitizedIdentifier_test : public Test { virtual void run (Arg) { @@ -46,7 +46,7 @@ namespace test { print_clean ("a Sentence"); print_clean ("trailing Withespace\n \t"); print_clean ("with a \t lot\n of Whitespace"); - print_clean ("with\"much (punctuation)[]!"); + print_clean ("@with\".\'much ($punctuation)[]!"); print_clean ("§&Ω%€ leading garbage"); print_clean ("mixed Ω garbage"); print_clean ("Bääääh!!"); @@ -60,7 +60,7 @@ namespace test { } }; - LAUNCHER (SanitizedIdentifier_test, "unit common"); + LAUNCHER (UtilSanitizedIdentifier_test, "unit common"); }} // namespace util::test diff --git a/wiki/empty.html b/wiki/empty.html index edfdd9076..21452b8f8 100644 --- a/wiki/empty.html +++ b/wiki/empty.html @@ -3,7 +3,7 @@ @@ -46,7 +46,7 @@ DAMAGE. - Empty TiddlyWiki 2.8.1 - a local web scrapbook in a single HTML page +TiddlyWiki 2.9.1 (classic)
loading Proc-Layer devel doku ...

Requires Javascript.
Engine - Building a Render Nodes Network from Objects in the Session