lumiera_documentation/src/gui/widget/error-log-display.hpp
Ichthyostega 12344ae9d8 NotificationDisplay: add an Error-State and implement signal to trigger on change
this is more or less gratitious functionality for now,
yet I consider it a proof-of-concept
2018-10-05 15:59:21 +02:00

337 lines
13 KiB
C++

/*
ERROR-LOG-DISPLAY.hpp - display of error messages in a text box
Copyright (C) Lumiera.org
2017, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file 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::tangible`, it is
** automatically detached from the WLink implementing this cross-attachment,
** 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 /////////////////////////////////////////////////////////////TICKET #1099 : build a prototype/dummy for UI <-> Proc integration
**
*/
#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 <utility>
#include <vector>
namespace gui {
namespace widget {
using util::_Fmt;
using lib::Literal;
using std::make_pair;
using std::vector;
using std::move;
namespace {
using Tag = Glib::RefPtr<Gtk::TextBuffer::Tag>;
/**
* @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<TextBuffer::TagTable> 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<Gtk::TextBuffer::Mark>;
using Entry = std::pair<Mark,Mark>;
using TextWidget = model::FlashDeco<Gtk::TextView>;
vector<Entry> errorMarks_;
TextWidget textLog_;
sigc::signal<void, bool> 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);
this->add (textLog_);
textLog_.set_editable (false);
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 = not errorMarks_.empty();
errorMarks_.clear();
size_t lineCnt = textLog_.get_buffer()->get_line_count();
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 (true);
}
/** 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 = errorMarks_.empty();
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<Entry> 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()
{
bool shallNotify = not errorMarks_.empty();
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();
if (shallNotify)
errorChangedSignal_.emit (true);
}
/** temporarily change display style to prompt for attention;
* set callback-timeout to 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<void,bool>
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 and set that text mark to 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*/