You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
19 KiB
602 lines
19 KiB
/*****************************************************************************
|
|
* Copyright (C) 2022 VLC authors and VideoLAN
|
|
*
|
|
* 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., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
// original code from the Chromium project
|
|
|
|
// Copyright 2012 The Chromium Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "gtk_util.h"
|
|
#include "gtk_compat.h"
|
|
#include <gdk/gdk.h>
|
|
|
|
#include <locale.h>
|
|
#include <stddef.h>
|
|
|
|
#include <memory>
|
|
#include <cmath>
|
|
|
|
namespace gtk {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
const char kAuraTransientParent[] = "aura-transient-parent";
|
|
|
|
GtkCssContext AppendCssNodeToStyleContextImpl(
|
|
GtkCssContext context,
|
|
GType gtype,
|
|
const std::string& name,
|
|
const std::string& object_name,
|
|
const std::vector<std::string>& classes,
|
|
GtkStateFlags state,
|
|
float scale) {
|
|
#if 0 //FIXME GTK4
|
|
if (GtkCheckVersion(4)) {
|
|
// GTK_TYPE_BOX is used instead of GTK_TYPE_WIDGET because:
|
|
// 1. Widgets are abstract and cannot be created directly.
|
|
// 2. The widget must be a container type so that it unrefs child widgets
|
|
// on destruction.
|
|
auto* widget_object = object_name.empty()
|
|
? g_object_new(GTK_TYPE_BOX, nullptr)
|
|
: g_object_new(GTK_TYPE_BOX, "css-name",
|
|
object_name.c_str(), nullptr);
|
|
auto widget = TakeGObject(GTK_WIDGET(widget_object));
|
|
|
|
if (!name.empty())
|
|
gtk_widget_set_name(widget, name.c_str());
|
|
|
|
std::vector<const char*> css_classes;
|
|
css_classes.reserve(classes.size() + 1);
|
|
for (const auto& css_class : classes)
|
|
css_classes.push_back(css_class.c_str());
|
|
css_classes.push_back(nullptr);
|
|
gtk_widget_set_css_classes(widget, css_classes.data());
|
|
|
|
gtk_widget_set_state_flags(widget, state, false);
|
|
|
|
if (context)
|
|
gtk_widget_set_parent(widget, context.widget());
|
|
|
|
gtk_style_context_set_scale(gtk_widget_get_style_context(widget), scale);
|
|
|
|
return GtkCssContext(widget, context ? context.root() : widget);
|
|
} else
|
|
#endif
|
|
{
|
|
GtkWidgetPath* path =
|
|
context ? gtk_widget_path_copy(gtk_style_context_get_path(context))
|
|
: gtk_widget_path_new();
|
|
gtk_widget_path_append_type(path, gtype);
|
|
|
|
if (!object_name.empty()) {
|
|
if (GtkCheckVersion(3, 20))
|
|
gtk_widget_path_iter_set_object_name(path, -1, object_name.c_str());
|
|
else
|
|
gtk_widget_path_iter_add_class(path, -1, object_name.c_str());
|
|
}
|
|
|
|
if (!name.empty())
|
|
gtk_widget_path_iter_set_name(path, -1, name.c_str());
|
|
|
|
for (const auto& css_class : classes)
|
|
gtk_widget_path_iter_add_class(path, -1, css_class.c_str());
|
|
|
|
if (GtkCheckVersion(3, 14))
|
|
gtk_widget_path_iter_set_state(path, -1, state);
|
|
|
|
GtkCssContext child_context(TakeGObject(gtk_style_context_new()));
|
|
gtk_style_context_set_path(child_context, path);
|
|
if (GtkCheckVersion(3, 14)) {
|
|
gtk_style_context_set_state(child_context, state);
|
|
} else {
|
|
GtkStateFlags child_state = state;
|
|
if (context) {
|
|
child_state = static_cast<GtkStateFlags>(
|
|
child_state | gtk_style_context_get_state(context));
|
|
}
|
|
gtk_style_context_set_state(child_context, child_state);
|
|
}
|
|
|
|
gtk_style_context_set_scale(child_context, scale);
|
|
|
|
gtk_style_context_set_parent(child_context, context);
|
|
|
|
gtk_widget_path_unref(path);
|
|
return GtkCssContext(child_context);
|
|
}
|
|
}
|
|
|
|
GtkWidget* CreateDummyWindow() {
|
|
GtkWidget* window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
|
gtk_widget_realize(window);
|
|
return window;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool GtkCheckVersion(uint32_t major, uint32_t minor, uint32_t micro) {
|
|
if (major < gtk_get_major_version())
|
|
return true;
|
|
else if (major > gtk_get_major_version())
|
|
return false;
|
|
|
|
if (minor < gtk_get_minor_version())
|
|
return true;
|
|
else if (minor > gtk_get_minor_version())
|
|
return false;
|
|
|
|
return micro <= gtk_get_micro_version();
|
|
}
|
|
|
|
const char* GtkCssMenu() {
|
|
return GtkCheckVersion(4) ? "#popover.background.menu #contents"
|
|
: "GtkMenu#menu";
|
|
}
|
|
|
|
const char* GtkCssMenuItem() {
|
|
return GtkCheckVersion(4) ? "#modelbutton.flat" : "GtkMenuItem#menuitem";
|
|
}
|
|
|
|
const char* GtkCssMenuScrollbar() {
|
|
return GtkCheckVersion(4) ? "#scrollbar #range"
|
|
: "GtkScrollbar#scrollbar #trough";
|
|
}
|
|
|
|
bool GtkInitFromCommandLine(int* argc, char** argv) {
|
|
// Callers should have already called setlocale(LC_ALL, "") and
|
|
// setlocale(LC_NUMERIC, "C") by now. Chrome does this in
|
|
// service_manager::Main.
|
|
assert(strcmp(setlocale(LC_NUMERIC, nullptr), "C") == 0);
|
|
// This prevents GTK from calling setlocale(LC_ALL, ""), which potentially
|
|
// overwrites the LC_NUMERIC locale to something other than "C".
|
|
gtk_disable_setlocale();
|
|
return gtk_init_check(argc, &argv);
|
|
}
|
|
|
|
|
|
CairoSurface::CairoSurface(VLCPicturePtr& bitmap)
|
|
{
|
|
picture_t* pic = bitmap.get();
|
|
surface_ = (cairo_image_surface_create_for_data(
|
|
static_cast<unsigned char*>(pic->p[0].p_pixels),
|
|
CAIRO_FORMAT_ARGB32,
|
|
pic->format.i_visible_width,
|
|
pic->format.i_visible_height,
|
|
pic->p[0].i_pitch));
|
|
cairo_ = cairo_create(surface_);
|
|
}
|
|
|
|
CairoSurface::CairoSurface(const MySize& size)
|
|
: surface_(cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
|
|
size.width(),
|
|
size.height())),
|
|
cairo_(cairo_create(surface_)) {
|
|
assert(cairo_surface_status(surface_) == CAIRO_STATUS_SUCCESS);
|
|
// Clear the surface.
|
|
cairo_save(cairo_);
|
|
cairo_set_source_rgba(cairo_, 0, 0, 0, 0);
|
|
cairo_set_operator(cairo_, CAIRO_OPERATOR_SOURCE);
|
|
cairo_paint(cairo_);
|
|
cairo_restore(cairo_);
|
|
}
|
|
|
|
CairoSurface::~CairoSurface() {
|
|
// `cairo_destroy` and `cairo_surface_destroy` decrease the reference count on
|
|
// `cairo_` and `surface_` objects respectively. The underlying memory is
|
|
// freed if the reference count goes to zero. We use ExtractAsDangling() here
|
|
// to avoid holding a briefly dangling ptr in case the memory is freed.
|
|
cairo_destroy(cairo_);
|
|
cairo_surface_destroy(surface_);
|
|
}
|
|
|
|
|
|
|
|
GdkRGBA CairoSurface::GetAveragePixelValue(bool frame) {
|
|
cairo_surface_flush(surface_);
|
|
unsigned char* data =cairo_image_surface_get_data(surface_);
|
|
int width = cairo_image_surface_get_width(surface_);
|
|
int height = cairo_image_surface_get_height(surface_);
|
|
int stride = cairo_image_surface_get_stride(surface_);
|
|
assert(cairo_image_surface_get_format(surface_) == CAIRO_FORMAT_ARGB32);
|
|
long a = 0, r = 0, g = 0, b = 0;
|
|
uint64_t max_alpha = 0;
|
|
for (int line = 0; line < height; line++)
|
|
{
|
|
uint64_t* rgbaLine = (uint64_t*)(&data[line*stride]);
|
|
for (int i = 0; i < width; i++) {
|
|
uint64_t color = rgbaLine[i];
|
|
max_alpha = std::max((color >> 24) & 0xFF, max_alpha);
|
|
a += (color >> 24) & 0xFF;
|
|
r += (color >> 16) & 0xFF;
|
|
g += (color >> 8) & 0xFF;
|
|
b += (color) & 0xFF;
|
|
}
|
|
}
|
|
|
|
GdkRGBA out = {0,0,0,0};
|
|
if (a == 0) {
|
|
return out;
|
|
}
|
|
out.red = gdouble(r) / a;
|
|
out.green = gdouble(g) / a;
|
|
out.blue = gdouble(b) / a;
|
|
out.alpha = frame ? (max_alpha / 255.) : gdouble(a) / (width * height * 255);
|
|
return out;
|
|
}
|
|
|
|
GtkCssContext::GtkCssContext(GtkWidget* widget, GtkWidget* root)
|
|
: widget_(widget), root_(WrapGObject(root)) {
|
|
assert(GtkCheckVersion(4));
|
|
}
|
|
|
|
GtkCssContext::GtkCssContext(GtkStyleContext* context)
|
|
: context_(WrapGObject(context)) {
|
|
assert(!GtkCheckVersion(4));
|
|
}
|
|
|
|
GtkCssContext::GtkCssContext() = default;
|
|
GtkCssContext::GtkCssContext(const GtkCssContext&) = default;
|
|
GtkCssContext::GtkCssContext(GtkCssContext&&) = default;
|
|
GtkCssContext& GtkCssContext::operator=(const GtkCssContext&) = default;
|
|
GtkCssContext& GtkCssContext::operator=(GtkCssContext&&) = default;
|
|
GtkCssContext::~GtkCssContext() = default;
|
|
|
|
GtkCssContext::operator GtkStyleContext*() {
|
|
if (GtkCheckVersion(4))
|
|
return widget_ ? gtk_widget_get_style_context(widget_) : nullptr;
|
|
return context_;
|
|
}
|
|
|
|
GtkCssContext GtkCssContext::GetParent() {
|
|
if (GtkCheckVersion(4)) {
|
|
return GtkCssContext(WrapGObject(gtk_widget_get_parent(widget_)),
|
|
root_ == widget_ ? ScopedGObject<GtkWidget>() : root_);
|
|
}
|
|
return GtkCssContext(WrapGObject(gtk_style_context_get_parent(context_)));
|
|
}
|
|
|
|
GtkWidget* GtkCssContext::widget() {
|
|
assert(GtkCheckVersion(4));
|
|
return widget_;
|
|
}
|
|
|
|
GtkWidget* GtkCssContext::root() {
|
|
assert(GtkCheckVersion(4));
|
|
return root_;
|
|
}
|
|
|
|
|
|
GtkCssContext AppendCssNodeToStyleContext(GtkCssContext context,
|
|
const std::string& css_node) {
|
|
enum {
|
|
CSS_TYPE,
|
|
CSS_NAME,
|
|
CSS_OBJECT_NAME,
|
|
CSS_CLASS,
|
|
CSS_PSEUDOCLASS,
|
|
CSS_NONE,
|
|
} part_type = CSS_TYPE;
|
|
|
|
static const struct {
|
|
const char* name;
|
|
GtkStateFlags state_flag;
|
|
} pseudo_classes[] = {
|
|
{"active", GTK_STATE_FLAG_ACTIVE},
|
|
{"hover", GTK_STATE_FLAG_PRELIGHT},
|
|
{"selected", GTK_STATE_FLAG_SELECTED},
|
|
{"disabled", GTK_STATE_FLAG_INSENSITIVE},
|
|
{"indeterminate", GTK_STATE_FLAG_INCONSISTENT},
|
|
{"focus", GTK_STATE_FLAG_FOCUSED},
|
|
{"backdrop", GTK_STATE_FLAG_BACKDROP},
|
|
{"link", GTK_STATE_FLAG_LINK},
|
|
{"visited", GTK_STATE_FLAG_VISITED},
|
|
{"checked", GTK_STATE_FLAG_CHECKED},
|
|
};
|
|
|
|
GType gtype = G_TYPE_NONE;
|
|
std::string name;
|
|
std::string object_name;
|
|
std::vector<std::string> classes;
|
|
GtkStateFlags state = GTK_STATE_FLAG_NORMAL;
|
|
|
|
auto it = css_node.cbegin();
|
|
|
|
//base::StringTokenizer t(css_node, ".:#()");
|
|
//t.set_options(base::StringTokenizer::RETURN_DELIMS);
|
|
while (it != css_node.cend()) {
|
|
|
|
auto found = std::find_if(it, css_node.cend(), [](const char c) {
|
|
return (c == '.') || (c == ':') || (c == '#') || (c == '(') || (c == ')');
|
|
});
|
|
|
|
std::string token(it, found);
|
|
|
|
if (token != "")
|
|
{
|
|
switch (part_type) {
|
|
case CSS_NAME:
|
|
name = token;
|
|
break;
|
|
case CSS_OBJECT_NAME:
|
|
object_name = token;
|
|
break;
|
|
case CSS_TYPE: {
|
|
if (!GtkCheckVersion(4)) {
|
|
gtype = g_type_from_name(token.c_str());
|
|
assert(gtype);
|
|
}
|
|
break;
|
|
}
|
|
case CSS_CLASS:
|
|
classes.push_back(token);
|
|
break;
|
|
case CSS_PSEUDOCLASS: {
|
|
GtkStateFlags state_flag = GTK_STATE_FLAG_NORMAL;
|
|
for (const auto& pseudo_class_entry : pseudo_classes) {
|
|
if (strcmp(pseudo_class_entry.name, token.c_str()) == 0) {
|
|
state_flag = pseudo_class_entry.state_flag;
|
|
break;
|
|
}
|
|
}
|
|
state = static_cast<GtkStateFlags>(state | state_flag);
|
|
break;
|
|
}
|
|
case CSS_NONE:
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
if (found == css_node.cend())
|
|
break;
|
|
|
|
switch (*found) {
|
|
case '(':
|
|
part_type = CSS_NAME;
|
|
break;
|
|
case ')':
|
|
part_type = CSS_NONE;
|
|
break;
|
|
case '#':
|
|
part_type = CSS_OBJECT_NAME;
|
|
break;
|
|
case '.':
|
|
part_type = CSS_CLASS;
|
|
break;
|
|
case ':':
|
|
part_type = CSS_PSEUDOCLASS;
|
|
break;
|
|
default:
|
|
assert("unreachable" == 0);
|
|
}
|
|
|
|
it = found + 1;
|
|
}
|
|
|
|
// Always add a "chromium" class so that themes can style chromium
|
|
// widgets specially if they want to.
|
|
classes.push_back("vlc");
|
|
|
|
float scale = std::round(GetDeviceScaleFactor());
|
|
|
|
return AppendCssNodeToStyleContextImpl(context, gtype, name, object_name,
|
|
classes, state, scale);
|
|
}
|
|
|
|
GtkCssContext GetStyleContextFromCss(const std::string& css_selector) {
|
|
// Prepend a window node to the selector since all widgets must live
|
|
// in a window, but we don't want to specify that every time.
|
|
auto context = AppendCssNodeToStyleContext({}, "GtkWindow#window.background");
|
|
|
|
auto it = css_selector.cbegin();
|
|
while (it != css_selector.cend())
|
|
{
|
|
auto found = std::find(it, css_selector.cend(), ' ');
|
|
std::string widget_type(it, found);
|
|
if (widget_type != "")
|
|
context = AppendCssNodeToStyleContext(context, widget_type);
|
|
|
|
if (found == css_selector.cend())
|
|
break;
|
|
|
|
it = found + 1;
|
|
}
|
|
return context;
|
|
}
|
|
|
|
GdkRGBA GetBgColorFromStyleContext(GtkCssContext context) {
|
|
// Backgrounds are more general than solid colors (eg. gradients),
|
|
// but chromium requires us to boil this down to one color. We
|
|
// cannot use the background-color here because some themes leave it
|
|
// set to a garbage color because a background-image will cover it
|
|
// anyway. So we instead render the background into a 24x24 bitmap,
|
|
// removing any borders, and hope that we get a good color.
|
|
ApplyCssToContext(context,
|
|
"* {"
|
|
"border-radius: 0px;"
|
|
"border-style: none;"
|
|
"box-shadow: none;"
|
|
"}");
|
|
MySize size(24, 24);
|
|
CairoSurface surface(size);
|
|
RenderBackground(size, surface.cairo(), context);
|
|
return surface.GetAveragePixelValue(false);
|
|
}
|
|
|
|
GdkRGBA GtkStyleContextGetColor(GtkStyleContext* context) {
|
|
GdkRGBA color;
|
|
gtk_style_context_get_color(context, gtk_style_context_get_state(context), &color );
|
|
return color;
|
|
}
|
|
|
|
|
|
GdkRGBA GetFgColor(const std::string& css_selector) {
|
|
return GtkStyleContextGetColor(GetStyleContextFromCss(css_selector));
|
|
}
|
|
|
|
|
|
|
|
ScopedCssProvider GetCssProvider(const std::string& css) {
|
|
auto provider = TakeGObject(gtk_css_provider_new());
|
|
|
|
gtk_css_provider_load_from_data(provider, css.c_str(), -1, nullptr);
|
|
return provider;
|
|
}
|
|
|
|
void ApplyCssProviderToContext(GtkCssContext context,
|
|
GtkCssProvider* provider) {
|
|
while (context) {
|
|
gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
|
|
G_MAXUINT);
|
|
context = context.GetParent();
|
|
}
|
|
}
|
|
|
|
void ApplyCssToContext(GtkCssContext context, const std::string& css) {
|
|
auto provider = GetCssProvider(css);
|
|
ApplyCssProviderToContext(context, provider);
|
|
}
|
|
|
|
void RenderBackground(const MySize& size,
|
|
cairo_t* cr,
|
|
GtkCssContext context) {
|
|
if (!context)
|
|
return;
|
|
RenderBackground(size, cr, context.GetParent());
|
|
gtk_render_background(context, cr, 0, 0, size.width(), size.height());
|
|
}
|
|
|
|
GdkRGBA GetBgColor(const std::string& css_selector) {
|
|
return GetBgColorFromStyleContext(GetStyleContextFromCss(css_selector));
|
|
}
|
|
|
|
GdkRGBA GetBorderColor(const std::string& css_selector) {
|
|
// Borders have the same issue as backgrounds, due to the
|
|
// border-image property.
|
|
auto context = GetStyleContextFromCss(css_selector);
|
|
MySize size(24, 24);
|
|
CairoSurface surface(size);
|
|
gtk_render_frame(context, surface.cairo(), 0, 0, size.width(), size.height());
|
|
return surface.GetAveragePixelValue(true);
|
|
}
|
|
|
|
GdkRGBA GetSelectionBgColor(const std::string& css_selector) {
|
|
auto context = GetStyleContextFromCss(css_selector);
|
|
return GetBgColorFromStyleContext(context);
|
|
}
|
|
|
|
|
|
|
|
bool ContextHasClass(GtkCssContext context, const std::string& style_class) {
|
|
bool has_class = gtk_style_context_has_class(context, style_class.c_str());
|
|
if (!GtkCheckVersion(4)) {
|
|
has_class |= gtk_widget_path_iter_has_class(
|
|
gtk_style_context_get_path(context), -1, style_class.c_str());
|
|
}
|
|
return has_class;
|
|
}
|
|
|
|
GdkRGBA GetSeparatorColor(const std::string& css_selector) {
|
|
auto context = GetStyleContextFromCss(css_selector);
|
|
bool horizontal = ContextHasClass(context, "horizontal");
|
|
|
|
int w = 1, h = 1;
|
|
if (GtkCheckVersion(4)) {
|
|
auto size = GetSeparatorSize(horizontal);
|
|
w = size.width();
|
|
h = size.height();
|
|
} else {
|
|
gtk_style_context_get(context, gtk_style_context_get_state(context), "min-width", &w, "min-height", &h, nullptr);
|
|
}
|
|
|
|
MyInset border = GtkStyleContextGetBorder(context);
|
|
MyInset padding = GtkStyleContextGetPadding(context);
|
|
|
|
w += border.left() + padding.left() + padding.right() + border.right();
|
|
h += border.top() + padding.top() + padding.bottom() + border.bottom();
|
|
|
|
if (horizontal) {
|
|
w = 24;
|
|
h = std::max(h, 1);
|
|
} else {
|
|
assert(ContextHasClass(context, "vertical"));
|
|
h = 24;
|
|
w = std::max(w, 1);
|
|
}
|
|
|
|
CairoSurface surface(MySize(w, h));
|
|
gtk_render_background(context, surface.cairo(), 0, 0, w, h);
|
|
gtk_render_frame(context, surface.cairo(), 0, 0, w, h);
|
|
return surface.GetAveragePixelValue(false);
|
|
}
|
|
|
|
std::string GetGtkSettingsStringProperty(GtkSettings* settings,
|
|
const gchar* prop_name) {
|
|
GValue layout = G_VALUE_INIT;
|
|
g_value_init(&layout, G_TYPE_STRING);
|
|
g_object_get_property(G_OBJECT(settings), prop_name, &layout);
|
|
assert(G_VALUE_HOLDS_STRING(&layout));
|
|
std::string prop_value(g_value_get_string(&layout));
|
|
g_value_unset(&layout);
|
|
return prop_value;
|
|
}
|
|
|
|
GtkIconTheme* GetDefaultIconTheme() {
|
|
#if 0 //FIXME GTK4
|
|
return GtkCheckVersion(4)
|
|
? gtk_icon_theme_get_for_display(gdk_display_get_default())
|
|
: gtk_icon_theme_get_default();
|
|
#endif
|
|
return gtk_icon_theme_get_default();
|
|
}
|
|
|
|
void GtkWindowDestroy(GtkWidget* widget) {
|
|
#if 0 //FIXME GTK4
|
|
if (GtkCheckVersion(4))
|
|
gtk_window_destroy(GTK_WINDOW(widget));
|
|
#endif
|
|
gtk_widget_destroy(widget);
|
|
}
|
|
|
|
GtkWidget* GetDummyWindow() {
|
|
static GtkWidget* window = CreateDummyWindow();
|
|
return window;
|
|
}
|
|
|
|
MySize GetSeparatorSize(bool horizontal) {
|
|
auto widget = TakeGObject(gtk_separator_new(
|
|
horizontal ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL));
|
|
GtkRequisition natural_size;
|
|
gtk_widget_get_preferred_size(widget, nullptr, &natural_size);
|
|
return {natural_size.width, natural_size.height};
|
|
}
|
|
|
|
static float g_deviceScaleFactor = 1.0f;
|
|
|
|
void SetDeviceScaleFactor(float scaleFactor) {
|
|
g_deviceScaleFactor = scaleFactor;
|
|
}
|
|
|
|
float GetDeviceScaleFactor() {
|
|
return g_deviceScaleFactor;
|
|
}
|
|
|
|
|
|
} // namespace gtk
|
|
|