lumiera_/research/gtk-style-experiment.cpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
 * there is no entity "Lumiera.org" which holds any copyrights
 * Lumiera source code is provided under the GPL Version 2+

== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''

The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!

The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00

401 lines
11 KiB
C++
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
gtk-style-experiment.cpp - simple application to investigate GTK styling
Copyright (C)
2019, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
*/
/** @file gtk-style-experiment.cpp
** Simple GTK Application frame to investigate some aspects of GTK's CSS implementation.
** Especially the goal is to _grab some existing CSS styling_ with a Gtk::StyleContext
** and use this for custom drawing. Within Lumiera, we use custom drawing for some parts
** of the Timeline UI presentation.
**
** # Setup for investigation
** - add a separate dummy Gtk::Frame widget as `testFrame_`
** - apply custom styling to that frame, by virtue of a CSS class '.experiment'
** - pick up the Gtk::StyleContext of that `testFrame_` to get the CSS path
** - build a "free standing" new Gtk::StyleContext and apply the CSS path found
** - use this style context to draw a custom frame and background onto the canvas
** - control extension of that custom drawing through the top margin CSS setting of testFrame_
**
** # Findings
** - it works as intended
** - however, care must be taken to apply CSS cascading properly (generic vs specific selectors)
** - the context_save/restore functionality seems to be broken on a "free standing" style context;
** however it works as expected on a style context associated to an existing and realised widget.
** - workaround is to add / remove classes explicitly.
** - CSS3 effects like box-shadow are applied with the StyleContext::render_background() function
** * first, an outset box-shadow is rendered _outside_ the box given as parameter to `render_background()`
** * then the box is filled with the background colour
** * and last, an inset box-shadow is rendered _inside_ the area of a would-be border,
** without rendering the border itself.
** * consequently we can not shade the border itself and we can not shade the content
**
** @see stage::timeline::BodyCanvasWidget
*/
#include "stage/gtk-base.hpp"
#include "lib/searchpath.hpp"
#include "lib/format-cout.hpp"
#include "lib/format-util.hpp"
#include "lib/error.hpp"
#include "lib/util.hpp"
#include <string>
using util::join;
using std::string;
namespace research {
namespace {
const string STYLESHEET_NAME{"gtk-style-experiment.css"};
const string RESOURCE_PATH {"$ORIGIN/gui"};
const string CLASS_experiment{"experiment"};
const string CLASS_slopeDeep1{"track-slope-deep1"};
string
slopeClassName(int depth)
{
switch (depth)
{
case 1: return "track-slope-deep1";
case 2: return "track-slope-deep2";
case 3: return "track-slope-deep3";
case 4: return "track-slope-deep4";
default:return "track-slope-verydeep";
}
}
}
using CairoC = stage::PCairoContext const&;
using StyleC = stage::PStyleContext const&;
using stage::PStyleContext;
class Canvas
: public Gtk::Layout
{
bool shallDraw_ = false;
bool recalcExtension_ = false;
StyleC style_;
public:
Canvas(StyleC refStyle)
: style_{refStyle}
{ }
void adjustSize();
void enableDraw (bool);
int xBorderSiz = 1;
int xObservedSize = -1;
string xObservedPath;
string xObservedClzz;
private:
virtual bool on_draw (Cairo::RefPtr<Cairo::Context> const&) override;
void determineExtension();
};
class StyleTestPanel
: public Gtk::Box
{
public:
StyleTestPanel();
private:
Gtk::Box twoParts_;
Gtk::ButtonBox buttons_;
Gtk::Button button_1_;
Gtk::Button button_2_;
Gtk::CheckButton toggleDraw_;
Gtk::Frame testFrame_;
Gtk::Frame frame_;
Gtk::ScrolledWindow scroller_;
PStyleContext pStyle_;
Canvas canvas_;
PStyleContext setupStyle(Gtk::Widget const&);
void experiment_1();
void experiment_2();
};
/* ===== Implementation ===== */
StyleTestPanel::StyleTestPanel()
: Box{}
, twoParts_{Gtk::ORIENTATION_VERTICAL}
, buttons_{}
, testFrame_{"testFrame"}
, frame_{"Gtk::StyleContext Experiments"}
, scroller_{}
, pStyle_{}
, canvas_{pStyle_}
{
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("_grow");
button_1_.set_use_underline();
button_1_.set_tooltip_markup("<b>Experiment 1</b>:\ngrow the border size");
button_1_.signal_clicked().connect(
mem_fun(*this, &StyleTestPanel::experiment_1));
buttons_.add(button_1_);
button_2_.set_label("_dump");
button_2_.set_use_underline();
button_2_.set_tooltip_markup("<b>Experiment 2</b>:\ndump style information");
button_2_.signal_clicked().connect(
mem_fun(*this, &StyleTestPanel::experiment_2));
buttons_.add(button_2_);
// a Gtk::Frame widget used as source for our StyleContext
testFrame_.get_style_context()->add_class(CLASS_experiment);
testFrame_.get_style_context()->add_class(CLASS_slopeDeep1);
buttons_.add(testFrame_);
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_);
pStyle_ = setupStyle (testFrame_);
canvas_.adjustSize();
// show everything....
this->add(twoParts_);
this->show_all();
}
PStyleContext
StyleTestPanel::setupStyle (Gtk::Widget const& srcWidget)
{
auto screen = Gdk::Screen::get_default();
auto css_provider = Gtk::CssProvider::create();
try
{
css_provider->load_from_path (lib::resolveModulePath (STYLESHEET_NAME, RESOURCE_PATH));
}
catch(Glib::Error const& failure)
{
WARN (stage, "Failure while loading stylesheet '%s': %s", cStr(STYLESHEET_NAME), cStr(failure.what()));
}
Gtk::StyleContext::add_provider_for_screen (screen, css_provider,
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
// auto style = srcWidget.get_style_context();
Gtk::WidgetPath path = srcWidget.get_path();
PStyleContext style = Gtk::StyleContext::create();
style->set_screen(Gdk::Screen::get_default());
style->set_path(path);
return style;
}
void
StyleTestPanel::experiment_1()
{
frame_.set_label("Experiment 1... GROW");
canvas_.xBorderSiz += 1;
canvas_.adjustSize();
}
void
StyleTestPanel::experiment_2()
{
frame_.set_label("Experiment 2... DUMP");
cout << "xBorderSize = "<<canvas_.xBorderSiz <<endl;
cout << "xClass = "<<canvas_.xObservedClzz <<endl;
cout << "style.path: " << canvas_.xObservedPath <<endl;
cout << "style.border.top = " << canvas_.xObservedSize <<endl;
cout << "................\n\n";
}
/* === Canvas drawing code === */
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=200, extV=200;
// ADD HERE: Code do find extension of the canvas dynamically...
//
recalcExtension_ = false;
set_size (extH, extV);
}
bool
Canvas::on_draw(CairoC 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();
//////////////////TEST drawing via Gtk::StyleContext
{
int marT = style_->get_margin().get_top();
if (xBorderSiz > 1) {
// style_->context_save(); // <<<---does not work. Asked on SO: https://stackoverflow.com/q/57342478
style_->add_class(slopeClassName(xBorderSiz));
}
xObservedSize = style_->get_border().get_top();
xObservedPath = string{style_->get_path().to_string()};
xObservedClzz = join(style_->list_classes(), " ");
int height = marT + 2 * xObservedSize;
style_->render_frame (cox
,20 // left start of the rectangle
,20 // top of the rectangle
,50 // width of the area
,height // height to fill
);
// NOTE: all box-shadow effects are rendered *here*
style_->render_background (cox
,40
,60
,80
,height
);
if (xBorderSiz > 1) {
// style_->context_restore();
style_->remove_class(slopeClassName(xBorderSiz));
}
}
//////////////////TEST drawing via Gtk::StyleContext
// 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);
}
class Window
: public Gtk::Window
{
StyleTestPanel demoPanel_;
public:
Window()
: demoPanel_{}
{
this->add (demoPanel_);
}
};
}//namespace research
int
main (int argc, char* argv[])
{
NOTICE (main, "*** GTK Style Experiment ***");
auto guiApp = Gtk::Application::create (argc, argv);
research::Window window;
return guiApp->run(window);
}