3793 lines
94 KiB
C
3793 lines
94 KiB
C
////////////////////////////////////////////////////////////////////////////////
|
|
//3456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789
|
|
// 10 20 30 40 50 60 70 80
|
|
//
|
|
// notify-osd
|
|
//
|
|
// bubble.c - implements all the rendering of a notification bubble
|
|
//
|
|
// Copyright 2009 Canonical Ltd.
|
|
//
|
|
// Authors:
|
|
// Mirco "MacSlow" Mueller <mirco.mueller@canonical.com>
|
|
// David Barth <david.barth@canonical.com>
|
|
//
|
|
// Contributor(s):
|
|
// Frederic "fredp" Peters <fpeters@gnome.org> (icon-only fix, rev. 204)
|
|
// Eitan Isaacson <eitan@ascender.com> (ATK interface for a11y, rev. 351)
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License version 3, as published
|
|
// by the Free Software Foundation.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranties of
|
|
// MERCHANTABILITY, SATISFACTORY QUALITY, 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, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <X11/Xatom.h>
|
|
#include <glib.h>
|
|
#include <gtk/gtk.h>
|
|
//#include <gdk/gdkx.h>
|
|
#include <pixman.h>
|
|
#include <math.h>
|
|
|
|
#include <egg/egg-hack.h>
|
|
#include <egg/egg-alpha.h>
|
|
|
|
#include "bubble.h"
|
|
#include "defaults.h"
|
|
#include "stack.h"
|
|
#include "dbus.h"
|
|
#include "util.h"
|
|
#include "bubble-window.h"
|
|
#include "raico-blur.h"
|
|
#include "tile.h"
|
|
|
|
G_DEFINE_TYPE (Bubble, bubble, G_TYPE_OBJECT);
|
|
|
|
#define GET_PRIVATE(o) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE ((o), BUBBLE_TYPE, BubblePrivate))
|
|
|
|
struct _BubblePrivate {
|
|
BubbleLayout layout;
|
|
GtkWidget* widget;
|
|
gboolean visible;
|
|
guint timer_id;
|
|
gboolean mouse_over;
|
|
gfloat distance;
|
|
gchar* synchronous;
|
|
gboolean composited;
|
|
EggAlpha* alpha;
|
|
EggTimeline* timeline;
|
|
guint draw_handler_id;
|
|
guint pointer_update_id;
|
|
tile_t* tile_background_part;
|
|
tile_t* tile_background;
|
|
tile_t* tile_icon;
|
|
tile_t* tile_title;
|
|
tile_t* tile_body;
|
|
tile_t* tile_indicator;
|
|
gint title_width;
|
|
gint title_height;
|
|
gint body_width;
|
|
gint body_height;
|
|
gboolean append;
|
|
gboolean icon_only;
|
|
gint future_height;
|
|
gboolean prevent_fade;
|
|
|
|
// these will be replaced by class Notification later on
|
|
GString* title;
|
|
GString* message_body;
|
|
gboolean title_needs_refresh;
|
|
gboolean message_body_needs_refresh;
|
|
guint id;
|
|
GdkPixbuf* icon_pixbuf;
|
|
gint value; // "empty": -2, valid range: -1..101, -1/101 trigger "over/undershoot"-effect
|
|
gchar* sender;
|
|
guint timeout;
|
|
guint urgency;
|
|
//notification_t* notification;
|
|
|
|
// used to prevent unneeded updates of the tile-cache, for append-,
|
|
// update or replace-cases, needs to move into class Notification
|
|
GString* old_icon_filename;
|
|
};
|
|
|
|
enum
|
|
{
|
|
TIMED_OUT,
|
|
VALUE_CHANGED,
|
|
MESSAGE_BODY_DELETED,
|
|
MESSAGE_BODY_INSERTED,
|
|
LAST_SIGNAL
|
|
};
|
|
|
|
enum
|
|
{
|
|
R = 0,
|
|
G,
|
|
B,
|
|
A
|
|
};
|
|
|
|
#define TEMPORARY_ICON_PREFIX_WORKAROUND 1
|
|
#ifdef TEMPORARY_ICON_PREFIX_WORKAROUND
|
|
#warning "--== Using the icon-name-substitution! This is a temp. workaround not going to be maintained for long! ==--"
|
|
#define NOTIFY_OSD_ICON_PREFIX "notification"
|
|
#endif
|
|
|
|
// FIXME: this is in class Defaults already, but not yet hooked up so for the
|
|
// moment we use the macros here, these values reflect the visual-guideline
|
|
// for jaunty notifications
|
|
#define TEXT_TITLE_COLOR_R 1.0f
|
|
#define TEXT_TITLE_COLOR_G 1.0f
|
|
#define TEXT_TITLE_COLOR_B 1.0f
|
|
#define TEXT_TITLE_COLOR_A 1.0f
|
|
|
|
#define TEXT_BODY_COLOR_R 0.91f
|
|
#define TEXT_BODY_COLOR_G 0.91f
|
|
#define TEXT_BODY_COLOR_B 0.91f
|
|
#define TEXT_BODY_COLOR_A 1.0f
|
|
|
|
#define TEXT_SHADOW_COLOR_R 0.0f
|
|
#define TEXT_SHADOW_COLOR_G 0.0f
|
|
#define TEXT_SHADOW_COLOR_B 0.0f
|
|
#define TEXT_SHADOW_COLOR_A 1.0f
|
|
|
|
#define BUBBLE_BG_COLOR_R 0.15f
|
|
#define BUBBLE_BG_COLOR_G 0.15f
|
|
#define BUBBLE_BG_COLOR_B 0.15f
|
|
#define BUBBLE_BG_COLOR_A 0.9f
|
|
|
|
#define INDICATOR_UNLIT_R 1.0f
|
|
#define INDICATOR_UNLIT_G 1.0f
|
|
#define INDICATOR_UNLIT_B 1.0f
|
|
#define INDICATOR_UNLIT_A 0.3f
|
|
|
|
#define INDICATOR_LIT_R 1.0f
|
|
#define INDICATOR_LIT_G 1.0f
|
|
#define INDICATOR_LIT_B 1.0f
|
|
#define INDICATOR_LIT_A 1.0f
|
|
|
|
#define FPS 60
|
|
#define PROXIMITY_THRESHOLD 40
|
|
#define WINDOW_MIN_OPACITY 0.4f
|
|
#define WINDOW_MAX_OPACITY 1.0f
|
|
|
|
// text drop-shadow should _never_ be bigger than content blur-radius!!!
|
|
#define BUBBLE_CONTENT_BLUR_RADIUS 4
|
|
#define TEXT_DROP_SHADOW_SIZE 2
|
|
|
|
//-- private functions ---------------------------------------------------------
|
|
|
|
static guint g_bubble_signals[LAST_SIGNAL] = { 0 };
|
|
gint g_pointer[2];
|
|
|
|
static void
|
|
draw_round_rect (cairo_t* cr,
|
|
gdouble aspect, // aspect-ratio
|
|
gdouble x, // top-left corner
|
|
gdouble y, // top-left corner
|
|
gdouble corner_radius, // "size" of the corners
|
|
gdouble width, // width of the rectangle
|
|
gdouble height) // height of the rectangle
|
|
{
|
|
gdouble radius = corner_radius / aspect;
|
|
|
|
// top-left, right of the corner
|
|
cairo_move_to (cr, x + radius, y);
|
|
|
|
// top-right, left of the corner
|
|
cairo_line_to (cr,
|
|
x + width - radius,
|
|
y);
|
|
|
|
// top-right, below the corner
|
|
cairo_arc (cr,
|
|
x + width - radius,
|
|
y + radius,
|
|
radius,
|
|
-90.0f * G_PI / 180.0f,
|
|
0.0f * G_PI / 180.0f);
|
|
|
|
// bottom-right, above the corner
|
|
cairo_line_to (cr,
|
|
x + width,
|
|
y + height - radius);
|
|
|
|
// bottom-right, left of the corner
|
|
cairo_arc (cr,
|
|
x + width - radius,
|
|
y + height - radius,
|
|
radius,
|
|
0.0f * G_PI / 180.0f,
|
|
90.0f * G_PI / 180.0f);
|
|
|
|
// bottom-left, right of the corner
|
|
cairo_line_to (cr,
|
|
x + radius,
|
|
y + height);
|
|
|
|
// bottom-left, above the corner
|
|
cairo_arc (cr,
|
|
x + radius,
|
|
y + height - radius,
|
|
radius,
|
|
90.0f * G_PI / 180.0f,
|
|
180.0f * G_PI / 180.0f);
|
|
|
|
// top-left, below the corner
|
|
cairo_line_to (cr,
|
|
x,
|
|
y + radius);
|
|
|
|
// top-left, right of the corner
|
|
cairo_arc (cr,
|
|
x + radius,
|
|
y + radius,
|
|
radius,
|
|
180.0f * G_PI / 180.0f,
|
|
270.0f * G_PI / 180.0f);
|
|
}
|
|
|
|
// color-, alpha-, radius-, width-, height- and gradient-values were determined
|
|
// by very close obvervation of a SVG-mockup from the design-team
|
|
static void
|
|
_draw_value_indicator (cairo_t* cr,
|
|
gint value, // value to render: 0 - 100
|
|
gint start_x, // top of surrounding rect
|
|
gint start_y, // left of surrounding rect
|
|
gint width, // width of surrounding rect
|
|
gint height, // height of surrounding rect
|
|
gint outline_thickness) // outline-thickness
|
|
{
|
|
gdouble outline_radius;
|
|
gdouble outline_width;
|
|
gdouble outline_height;
|
|
gdouble bar_radius;
|
|
gdouble bar_width;
|
|
//gdouble bar_height;
|
|
cairo_pattern_t* gradient;
|
|
|
|
outline_radius = outline_thickness;
|
|
outline_width = width;
|
|
outline_height = height;
|
|
|
|
// draw bar-background
|
|
cairo_set_line_width (cr, outline_thickness);
|
|
cairo_set_source_rgba (cr, 0.0f, 0.0f, 0.0f, 0.5f);
|
|
draw_round_rect (cr,
|
|
1.0f,
|
|
start_x,
|
|
start_y,
|
|
outline_radius,
|
|
outline_width,
|
|
outline_height);
|
|
cairo_fill (cr);
|
|
gradient = cairo_pattern_create_linear (0.0f,
|
|
start_y + outline_thickness,
|
|
0.0f,
|
|
start_y +
|
|
outline_height -
|
|
2 * outline_thickness);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
0.0f,
|
|
0.866f,
|
|
0.866f,
|
|
0.866f,
|
|
0.3f);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
0.2f,
|
|
0.827f,
|
|
0.827f,
|
|
0.827f,
|
|
0.3f);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
0.3f,
|
|
0.772f,
|
|
0.772f,
|
|
0.772f,
|
|
0.3f);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
1.0f,
|
|
0.623f,
|
|
0.623f,
|
|
0.623f,
|
|
0.3f);
|
|
cairo_set_source (cr, gradient);
|
|
draw_round_rect (cr,
|
|
1.0f,
|
|
start_x + outline_thickness,
|
|
start_y + outline_thickness,
|
|
outline_radius,
|
|
outline_width - 2 * outline_thickness,
|
|
outline_height - 2 * outline_thickness);
|
|
cairo_fill (cr);
|
|
cairo_pattern_destroy (gradient);
|
|
|
|
bar_radius = outline_radius;
|
|
bar_width = outline_width - 2 * outline_radius;
|
|
//bar_height = outline_height - outline_radius;
|
|
|
|
// draw value-bar
|
|
if (value > 0)
|
|
{
|
|
gint corrected_value = value;
|
|
|
|
if (corrected_value > 100)
|
|
corrected_value = 100;
|
|
|
|
draw_round_rect (cr,
|
|
1.0f,
|
|
start_x + outline_thickness,
|
|
start_y + outline_thickness,
|
|
bar_radius,
|
|
bar_width / 100.0f * (gdouble) corrected_value,
|
|
outline_height - 2 * outline_thickness);
|
|
|
|
gradient = cairo_pattern_create_linear (0.0f,
|
|
start_y +
|
|
outline_thickness,
|
|
0.0f,
|
|
start_y +
|
|
outline_height -
|
|
2 * outline_thickness);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
0.0f,
|
|
0.9f,
|
|
0.9f,
|
|
0.9f,
|
|
1.0f);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
0.75f,
|
|
0.5f,
|
|
0.5f,
|
|
0.5f,
|
|
1.0f);
|
|
cairo_pattern_add_color_stop_rgba (gradient,
|
|
1.0f,
|
|
0.4f,
|
|
0.4f,
|
|
0.4f,
|
|
1.0f);
|
|
|
|
cairo_set_source (cr, gradient);
|
|
cairo_fill (cr);
|
|
cairo_pattern_destroy (gradient);
|
|
}
|
|
}
|
|
|
|
void
|
|
_draw_shadow (cairo_t* cr,
|
|
gdouble width,
|
|
gdouble height,
|
|
gint shadow_radius,
|
|
gint corner_radius)
|
|
{
|
|
cairo_surface_t* tmp_surface = NULL;
|
|
cairo_surface_t* new_surface = NULL;
|
|
cairo_pattern_t* pattern = NULL;
|
|
cairo_t* cr_surf = NULL;
|
|
cairo_matrix_t matrix;
|
|
raico_blur_t* blur = NULL;
|
|
|
|
tmp_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
4 * shadow_radius,
|
|
4 * shadow_radius);
|
|
if (cairo_surface_status (tmp_surface) != CAIRO_STATUS_SUCCESS) {
|
|
if (tmp_surface)
|
|
cairo_surface_destroy (tmp_surface);
|
|
return;
|
|
}
|
|
|
|
cr_surf = cairo_create (tmp_surface);
|
|
if (cairo_status (cr_surf) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (tmp_surface);
|
|
if (cr_surf)
|
|
cairo_destroy (cr_surf);
|
|
return;
|
|
}
|
|
|
|
cairo_scale (cr_surf, 1.0f, 1.0f);
|
|
cairo_set_operator (cr_surf, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr_surf);
|
|
cairo_set_operator (cr_surf, CAIRO_OPERATOR_OVER);
|
|
cairo_set_source_rgba (cr_surf, 0.0f, 0.0f, 0.0f, 1.0f);
|
|
cairo_arc (cr_surf,
|
|
2 * shadow_radius,
|
|
2 * shadow_radius,
|
|
1.75f * corner_radius,
|
|
0.0f,
|
|
360.0f * (G_PI / 180.f));
|
|
cairo_fill (cr_surf);
|
|
cairo_destroy (cr_surf);
|
|
|
|
// create and setup blur
|
|
blur = raico_blur_create (RAICO_BLUR_QUALITY_HIGH);
|
|
raico_blur_set_radius (blur, shadow_radius);
|
|
|
|
// now blur it
|
|
raico_blur_apply (blur, tmp_surface);
|
|
|
|
// blur no longer needed
|
|
raico_blur_destroy (blur);
|
|
|
|
new_surface = cairo_image_surface_create_for_data (
|
|
cairo_image_surface_get_data (tmp_surface),
|
|
cairo_image_surface_get_format (tmp_surface),
|
|
cairo_image_surface_get_width (tmp_surface) / 2,
|
|
cairo_image_surface_get_height (tmp_surface) / 2,
|
|
cairo_image_surface_get_stride (tmp_surface));
|
|
pattern = cairo_pattern_create_for_surface (new_surface);
|
|
if (cairo_pattern_status (pattern) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (tmp_surface);
|
|
cairo_surface_destroy (new_surface);
|
|
if (pattern)
|
|
cairo_pattern_destroy (pattern);
|
|
return;
|
|
}
|
|
|
|
// top left
|
|
cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);
|
|
cairo_set_source (cr, pattern);
|
|
cairo_rectangle (cr,
|
|
0.0f,
|
|
0.0f,
|
|
width - 2 * shadow_radius,
|
|
2 * shadow_radius);
|
|
cairo_fill (cr);
|
|
|
|
// bottom left
|
|
cairo_matrix_init_scale (&matrix, 1.0f, -1.0f);
|
|
cairo_matrix_translate (&matrix, 0.0f, -height);
|
|
cairo_pattern_set_matrix (pattern, &matrix);
|
|
cairo_rectangle (cr,
|
|
0.0f,
|
|
2 * shadow_radius,
|
|
2 * shadow_radius,
|
|
height - 2 * shadow_radius);
|
|
cairo_fill (cr);
|
|
|
|
// top right
|
|
cairo_matrix_init_scale (&matrix, -1.0f, 1.0f);
|
|
cairo_matrix_translate (&matrix, -width, 0.0f);
|
|
cairo_pattern_set_matrix (pattern, &matrix);
|
|
cairo_rectangle (cr,
|
|
width - 2 * shadow_radius,
|
|
0.0f,
|
|
2 * shadow_radius,
|
|
height - 2 * shadow_radius);
|
|
cairo_fill (cr);
|
|
|
|
// bottom right
|
|
cairo_matrix_init_scale (&matrix, -1.0f, -1.0f);
|
|
cairo_matrix_translate (&matrix, -width, -height);
|
|
cairo_pattern_set_matrix (pattern, &matrix);
|
|
cairo_rectangle (cr,
|
|
2 * shadow_radius,
|
|
height - 2 * shadow_radius,
|
|
width - 2 * shadow_radius,
|
|
2 * shadow_radius);
|
|
cairo_fill (cr);
|
|
|
|
// clean up
|
|
cairo_pattern_destroy (pattern);
|
|
cairo_surface_destroy (tmp_surface);
|
|
cairo_surface_destroy (new_surface);
|
|
}
|
|
|
|
static void
|
|
_draw_layout_grid (cairo_t* cr,
|
|
Bubble* bubble)
|
|
{
|
|
Defaults* d = bubble->defaults;
|
|
|
|
if (!cr)
|
|
return;
|
|
|
|
cairo_set_line_width (cr, 1.0f);
|
|
cairo_set_source_rgba (cr, 1.0f, 0.5f, 0.25f, 1.0f);
|
|
|
|
// all vertical grid lines
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (2 * defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (2 * defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
EM2PIXELS (defaults_get_margin_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
EM2PIXELS (defaults_get_margin_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
|
|
// all horizontal grid lines
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
EM2PIXELS (defaults_get_icon_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) -
|
|
EM2PIXELS (defaults_get_margin_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) -
|
|
EM2PIXELS (defaults_get_margin_size (d), d));
|
|
cairo_move_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_line_to (cr,
|
|
0.5f + EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
0.5f + (gdouble) bubble_get_height (bubble) -
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
|
|
cairo_stroke (cr);
|
|
}
|
|
|
|
void
|
|
_refresh_background (Bubble* self)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
Defaults* d = self->defaults;
|
|
cairo_t* cr = NULL;
|
|
cairo_surface_t* scratch = NULL;
|
|
cairo_surface_t* dummy = NULL;
|
|
cairo_surface_t* clone = NULL;
|
|
cairo_surface_t* normal = NULL;
|
|
cairo_surface_t* blurred = NULL;
|
|
raico_blur_t* blur = NULL;
|
|
gint width;
|
|
gint height;
|
|
|
|
bubble_get_size (self, &width, &height);
|
|
|
|
// create temp. scratch surface for top-left shadow/background part
|
|
if (priv->composited)
|
|
scratch = cairo_image_surface_create (
|
|
CAIRO_FORMAT_ARGB32,
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
else
|
|
scratch = cairo_image_surface_create (
|
|
CAIRO_FORMAT_RGB24,
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
|
|
g_return_if_fail (scratch);
|
|
|
|
if (cairo_surface_status (scratch) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
if (scratch)
|
|
cairo_surface_destroy (scratch);
|
|
|
|
return;
|
|
}
|
|
|
|
// create drawing context for that temp. scratch surface
|
|
cr = cairo_create (scratch);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (cr)
|
|
cairo_destroy (cr);
|
|
|
|
return;
|
|
}
|
|
|
|
// clear, render top-left part of shadow/background in scratch-surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
GdkRGBA color;
|
|
gchar* color_string = NULL;
|
|
color_string = defaults_get_bubble_bg_color (d);
|
|
gdk_rgba_parse (&color, color_string);
|
|
g_free (color_string);
|
|
|
|
if (priv->composited)
|
|
{
|
|
_draw_shadow (
|
|
cr,
|
|
width,
|
|
height,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
EM2PIXELS (defaults_get_bubble_corner_radius (d), d));
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
draw_round_rect (
|
|
cr,
|
|
1.0f,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
EM2PIXELS (defaults_get_bubble_corner_radius (d), d),
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
(gdouble) bubble_get_height (self) -
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_fill (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
cairo_set_source_rgba (cr,
|
|
0.75 * color.red,
|
|
0.75 * color.green,
|
|
0.75 * color.blue,
|
|
BUBBLE_BG_COLOR_A);
|
|
}
|
|
else
|
|
cairo_set_source_rgb (cr,
|
|
0.75 * color.red,
|
|
0.75 * color.green,
|
|
0.75 * color.blue);
|
|
|
|
draw_round_rect (
|
|
cr,
|
|
1.0f,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
EM2PIXELS (defaults_get_bubble_corner_radius (d), d),
|
|
EM2PIXELS (defaults_get_bubble_width (d), d),
|
|
(gdouble) bubble_get_height (self) -
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
cairo_fill (cr);
|
|
cairo_destroy (cr);
|
|
|
|
// create temp. clone of scratch surface
|
|
dummy = cairo_image_surface_create_for_data (
|
|
cairo_image_surface_get_data (scratch),
|
|
cairo_image_surface_get_format (scratch),
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
3 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
cairo_image_surface_get_stride (scratch));
|
|
clone = copy_surface (dummy);
|
|
cairo_surface_destroy (dummy);
|
|
|
|
// create normal surface from that surface-clone
|
|
dummy = cairo_image_surface_create_for_data (
|
|
cairo_image_surface_get_data (clone),
|
|
cairo_image_surface_get_format (clone),
|
|
2 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
2 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
cairo_image_surface_get_stride (clone));
|
|
normal = copy_surface (dummy);
|
|
cairo_surface_destroy (dummy);
|
|
|
|
// now blur the surface-clone
|
|
blur = raico_blur_create (RAICO_BLUR_QUALITY_LOW);
|
|
raico_blur_set_radius (blur, BUBBLE_CONTENT_BLUR_RADIUS);
|
|
raico_blur_apply (blur, clone);
|
|
raico_blur_destroy (blur);
|
|
|
|
// create blurred version from that blurred surface-clone
|
|
dummy = cairo_image_surface_create_for_data (
|
|
cairo_image_surface_get_data (clone),
|
|
cairo_image_surface_get_format (clone),
|
|
2 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
2 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d),
|
|
cairo_image_surface_get_stride (clone));
|
|
blurred = copy_surface (dummy);
|
|
cairo_surface_destroy (dummy);
|
|
destroy_cloned_surface (clone);
|
|
|
|
// finally create tile with top-left shadow/background part
|
|
if (priv->tile_background_part)
|
|
tile_destroy (priv->tile_background_part);
|
|
priv->tile_background_part = tile_new_for_padding (normal, blurred);
|
|
destroy_cloned_surface (normal);
|
|
destroy_cloned_surface (blurred);
|
|
|
|
// create surface(s) for full shadow/background tile
|
|
if (priv->composited)
|
|
{
|
|
// we need two RGBA-surfaces
|
|
normal = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
width,
|
|
height);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (normal)
|
|
cairo_surface_destroy (normal);
|
|
|
|
return;
|
|
}
|
|
|
|
blurred = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
|
|
width,
|
|
height);
|
|
if (cairo_surface_status (blurred) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (blurred)
|
|
cairo_surface_destroy (blurred);
|
|
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we need only one RGB-surface
|
|
normal = cairo_image_surface_create (CAIRO_FORMAT_RGB24,
|
|
width,
|
|
height);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (normal)
|
|
cairo_surface_destroy (normal);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// use tile for top-left background-part to fill the full bg-surface
|
|
if (priv->composited)
|
|
{
|
|
// create context for blurred surface
|
|
cr = cairo_create (blurred);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
cairo_surface_destroy (blurred);
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (cr)
|
|
cairo_destroy (cr);
|
|
|
|
return;
|
|
}
|
|
|
|
// clear blurred surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
// fill with blurred state of background-part tile
|
|
tile_paint_with_padding (priv->tile_background_part,
|
|
cr,
|
|
0.0f,
|
|
0.0f,
|
|
width,
|
|
height,
|
|
0.0f,
|
|
1.0f);
|
|
|
|
// get rid of context for blurred surface
|
|
cairo_destroy (cr);
|
|
}
|
|
|
|
// create context for normal surface
|
|
cr = cairo_create (normal);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
cairo_surface_destroy (scratch);
|
|
|
|
if (priv->composited)
|
|
cairo_surface_destroy (blurred);
|
|
|
|
return;
|
|
}
|
|
|
|
// clear normal surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
// fill with normal state of background-part tile
|
|
tile_paint_with_padding (priv->tile_background_part,
|
|
cr,
|
|
0.0f,
|
|
0.0f,
|
|
width,
|
|
height,
|
|
1.0f,
|
|
0.0f);
|
|
|
|
// get rid of context for normal surface
|
|
cairo_destroy (cr);
|
|
|
|
// finally create tile for full background
|
|
if (priv->tile_background)
|
|
tile_destroy (priv->tile_background);
|
|
if (priv->composited)
|
|
priv->tile_background = tile_new_for_padding (normal, blurred);
|
|
else
|
|
priv->tile_background = tile_new_for_padding (normal, normal);
|
|
|
|
// clean up
|
|
if (priv->composited)
|
|
cairo_surface_destroy (blurred);
|
|
|
|
cairo_surface_destroy (normal);
|
|
cairo_surface_destroy (scratch);
|
|
}
|
|
|
|
void
|
|
_refresh_icon (Bubble* self)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
Defaults* d = self->defaults;
|
|
cairo_surface_t* normal = NULL;
|
|
cairo_t* cr = NULL;
|
|
|
|
if (!priv->icon_pixbuf)
|
|
return;
|
|
|
|
// create temp. scratch surface
|
|
normal = cairo_image_surface_create (
|
|
CAIRO_FORMAT_ARGB32,
|
|
EM2PIXELS (defaults_get_icon_size (d), d) +
|
|
2 * BUBBLE_CONTENT_BLUR_RADIUS,
|
|
EM2PIXELS (defaults_get_icon_size (d), d) +
|
|
2 * BUBBLE_CONTENT_BLUR_RADIUS);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
return;
|
|
|
|
// create context for normal surface
|
|
cr = cairo_create (normal);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
return;
|
|
}
|
|
|
|
// clear normal surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
// render icon into normal surface
|
|
gdk_cairo_set_source_pixbuf (cr,
|
|
priv->icon_pixbuf,
|
|
BUBBLE_CONTENT_BLUR_RADIUS,
|
|
BUBBLE_CONTENT_BLUR_RADIUS);
|
|
cairo_paint (cr);
|
|
|
|
// create the surface/blur-cache from the normal surface
|
|
if (priv->tile_icon)
|
|
tile_destroy (priv->tile_icon);
|
|
priv->tile_icon = tile_new (normal, BUBBLE_CONTENT_BLUR_RADIUS/2);
|
|
|
|
// clean up
|
|
cairo_destroy (cr);
|
|
cairo_surface_destroy (normal);
|
|
}
|
|
|
|
void
|
|
_refresh_title (Bubble* self)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
Defaults* d = self->defaults;
|
|
cairo_surface_t* normal = NULL;
|
|
cairo_t* cr = NULL;
|
|
PangoFontDescription* desc = NULL;
|
|
PangoLayout* layout = NULL;
|
|
raico_blur_t* blur = NULL;
|
|
gchar* text_font_face = NULL;
|
|
|
|
// create temp. scratch surface
|
|
normal = cairo_image_surface_create (
|
|
CAIRO_FORMAT_ARGB32,
|
|
priv->title_width + 2 * BUBBLE_CONTENT_BLUR_RADIUS,
|
|
priv->title_height + 2 * BUBBLE_CONTENT_BLUR_RADIUS);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
return;
|
|
|
|
// create context for normal surface
|
|
cr = cairo_create (normal);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
return;
|
|
}
|
|
|
|
// clear normal surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
// create pango desc/layout
|
|
layout = pango_cairo_create_layout (cr);
|
|
text_font_face = defaults_get_text_font_face (d);
|
|
desc = pango_font_description_from_string (text_font_face);
|
|
g_free ((gpointer) text_font_face);
|
|
|
|
pango_font_description_set_size (desc,
|
|
defaults_get_system_font_size (d) *
|
|
defaults_get_text_title_size (d) *
|
|
PANGO_SCALE);
|
|
|
|
pango_font_description_set_weight (desc,
|
|
defaults_get_text_title_weight (d));
|
|
pango_layout_set_font_description (layout, desc);
|
|
pango_font_description_free (desc);
|
|
|
|
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
|
pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END);
|
|
pango_layout_set_width (layout, priv->title_width * PANGO_SCALE);
|
|
pango_layout_set_height (layout, priv->title_height * PANGO_SCALE);
|
|
|
|
// print and layout string (pango-wise)
|
|
pango_layout_set_text (layout, priv->title->str, priv->title->len);
|
|
|
|
// make sure system-wide font-options like hinting, antialiasing etc.
|
|
// are taken into account
|
|
pango_cairo_context_set_font_options (
|
|
pango_layout_get_context (layout),
|
|
gdk_screen_get_font_options (
|
|
gtk_widget_get_screen (priv->widget)));
|
|
pango_cairo_context_set_resolution (pango_layout_get_context (layout),
|
|
defaults_get_screen_dpi (d));
|
|
pango_layout_context_changed (layout);
|
|
|
|
// draw text for drop-shadow and ...
|
|
cairo_move_to (cr,
|
|
BUBBLE_CONTENT_BLUR_RADIUS,
|
|
BUBBLE_CONTENT_BLUR_RADIUS);
|
|
cairo_set_source_rgba (cr,
|
|
TEXT_SHADOW_COLOR_R,
|
|
TEXT_SHADOW_COLOR_G,
|
|
TEXT_SHADOW_COLOR_B,
|
|
TEXT_SHADOW_COLOR_A);
|
|
pango_cairo_show_layout (cr, layout);
|
|
|
|
// ... blur it
|
|
blur = raico_blur_create (RAICO_BLUR_QUALITY_LOW);
|
|
raico_blur_set_radius (blur, TEXT_DROP_SHADOW_SIZE);
|
|
raico_blur_apply (blur, normal);
|
|
raico_blur_destroy (blur);
|
|
|
|
// now draw normal (non-blurred) text over drop-shadow
|
|
cairo_set_source_rgba (cr,
|
|
TEXT_TITLE_COLOR_R,
|
|
TEXT_TITLE_COLOR_G,
|
|
TEXT_TITLE_COLOR_B,
|
|
TEXT_TITLE_COLOR_A);
|
|
pango_cairo_show_layout (cr, layout);
|
|
|
|
g_object_unref (layout);
|
|
|
|
// create the surface/blur-cache from the normal surface
|
|
if (priv->tile_title)
|
|
tile_destroy (priv->tile_title);
|
|
priv->tile_title = tile_new (normal, BUBBLE_CONTENT_BLUR_RADIUS);
|
|
|
|
// clean up
|
|
cairo_destroy (cr);
|
|
cairo_surface_destroy (normal);
|
|
|
|
// reset refresh-flag
|
|
priv->title_needs_refresh = FALSE;
|
|
}
|
|
|
|
void
|
|
_refresh_body (Bubble* self)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
Defaults* d = self->defaults;
|
|
cairo_surface_t* normal = NULL;
|
|
cairo_t* cr = NULL;
|
|
PangoFontDescription* desc = NULL;
|
|
PangoLayout* layout = NULL;
|
|
raico_blur_t* blur = NULL;
|
|
gchar* text_font_face = NULL;
|
|
|
|
// create temp. scratch surface
|
|
normal = cairo_image_surface_create (
|
|
CAIRO_FORMAT_ARGB32,
|
|
priv->body_width + 2 * BUBBLE_CONTENT_BLUR_RADIUS,
|
|
priv->body_height + 2 * BUBBLE_CONTENT_BLUR_RADIUS);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
return;
|
|
|
|
// create context for normal surface
|
|
cr = cairo_create (normal);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
return;
|
|
}
|
|
|
|
// clear normal surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
// create pango desc/layout
|
|
layout = pango_cairo_create_layout (cr);
|
|
text_font_face = defaults_get_text_font_face (d);
|
|
desc = pango_font_description_from_string (text_font_face);
|
|
g_free ((gpointer) text_font_face);
|
|
|
|
pango_font_description_set_size (desc,
|
|
defaults_get_system_font_size (d) *
|
|
defaults_get_text_body_size (d) *
|
|
PANGO_SCALE);
|
|
|
|
pango_font_description_set_weight (desc,
|
|
defaults_get_text_body_weight (d));
|
|
pango_layout_set_font_description (layout, desc);
|
|
pango_font_description_free (desc);
|
|
|
|
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
|
pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END);
|
|
pango_layout_set_width (layout, priv->body_width * PANGO_SCALE);
|
|
pango_layout_set_height (layout, priv->body_height * PANGO_SCALE);
|
|
|
|
// print and layout string (pango-wise)
|
|
pango_layout_set_text (layout,
|
|
priv->message_body->str,
|
|
priv->message_body->len);
|
|
|
|
// make sure system-wide font-options like hinting, antialiasing etc.
|
|
// are taken into account
|
|
pango_cairo_context_set_font_options (
|
|
pango_layout_get_context (layout),
|
|
gdk_screen_get_font_options (
|
|
gtk_widget_get_screen (priv->widget)));
|
|
pango_cairo_context_set_resolution (pango_layout_get_context (layout),
|
|
defaults_get_screen_dpi (d));
|
|
pango_layout_context_changed (layout);
|
|
|
|
// draw text for drop-shadow and ...
|
|
cairo_move_to (cr,
|
|
BUBBLE_CONTENT_BLUR_RADIUS,
|
|
BUBBLE_CONTENT_BLUR_RADIUS);
|
|
cairo_set_source_rgba (cr,
|
|
TEXT_SHADOW_COLOR_R,
|
|
TEXT_SHADOW_COLOR_G,
|
|
TEXT_SHADOW_COLOR_B,
|
|
TEXT_SHADOW_COLOR_A);
|
|
pango_cairo_show_layout (cr, layout);
|
|
|
|
// ... blur it
|
|
blur = raico_blur_create (RAICO_BLUR_QUALITY_LOW);
|
|
raico_blur_set_radius (blur, TEXT_DROP_SHADOW_SIZE);
|
|
raico_blur_apply (blur, normal);
|
|
raico_blur_destroy (blur);
|
|
|
|
// now draw normal (non-blurred) text over drop-shadow
|
|
cairo_set_source_rgba (cr,
|
|
TEXT_BODY_COLOR_R,
|
|
TEXT_BODY_COLOR_G,
|
|
TEXT_BODY_COLOR_B,
|
|
TEXT_BODY_COLOR_A);
|
|
pango_cairo_show_layout (cr, layout);
|
|
|
|
g_object_unref (layout);
|
|
|
|
// create the surface/blur-cache from the normal surface
|
|
if (priv->tile_body)
|
|
tile_destroy (priv->tile_body);
|
|
priv->tile_body = tile_new (normal, BUBBLE_CONTENT_BLUR_RADIUS);
|
|
|
|
// clean up
|
|
cairo_destroy (cr);
|
|
cairo_surface_destroy (normal);
|
|
|
|
// reset refresh-flag
|
|
priv->message_body_needs_refresh = FALSE;
|
|
}
|
|
|
|
void
|
|
_refresh_indicator (Bubble* self)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
Defaults* d = self->defaults;
|
|
cairo_surface_t* normal = NULL;
|
|
cairo_t* cr = NULL;
|
|
|
|
// create temp. scratch surface
|
|
normal = cairo_image_surface_create (
|
|
CAIRO_FORMAT_ARGB32,
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
3 * EM2PIXELS (defaults_get_margin_size (d), d) -
|
|
EM2PIXELS (defaults_get_icon_size (d), d)
|
|
+ 2 * BUBBLE_CONTENT_BLUR_RADIUS,
|
|
EM2PIXELS (defaults_get_icon_size (d), d) / 5.0f
|
|
+ 2 * BUBBLE_CONTENT_BLUR_RADIUS);
|
|
if (cairo_surface_status (normal) != CAIRO_STATUS_SUCCESS)
|
|
return;
|
|
|
|
// create context for normal surface
|
|
cr = cairo_create (normal);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS)
|
|
{
|
|
cairo_surface_destroy (normal);
|
|
return;
|
|
}
|
|
|
|
// clear normal surface
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
_draw_value_indicator (
|
|
cr,
|
|
priv->value,
|
|
BUBBLE_CONTENT_BLUR_RADIUS,
|
|
BUBBLE_CONTENT_BLUR_RADIUS,
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
3 * EM2PIXELS (defaults_get_margin_size (d), d) -
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
EM2PIXELS (defaults_get_gauge_size (d), d),
|
|
EM2PIXELS (defaults_get_gauge_outline_width (d), d));
|
|
|
|
// create the surface/blur-cache from the normal surface
|
|
if (priv->tile_indicator)
|
|
tile_destroy (priv->tile_indicator);
|
|
priv->tile_indicator = tile_new (normal, BUBBLE_CONTENT_BLUR_RADIUS/2);
|
|
|
|
// clean up
|
|
cairo_destroy (cr);
|
|
cairo_surface_destroy (normal);
|
|
}
|
|
|
|
void
|
|
_render_background (Bubble* self,
|
|
cairo_t* cr,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
Defaults* d = self->defaults;
|
|
|
|
tile_paint (self->priv->tile_background,
|
|
cr,
|
|
0.0f,
|
|
0.0f,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
|
|
// layout-grid and urgency-indication bar
|
|
if (g_getenv ("DEBUG"))
|
|
{
|
|
// for debugging layout and positioning issues
|
|
_draw_layout_grid (cr, self);
|
|
|
|
switch (bubble_get_urgency (self))
|
|
{
|
|
// low urgency-bar is painted blue
|
|
case 0:
|
|
cairo_set_source_rgb (cr, 0.25f, 0.5f, 1.0f);
|
|
break;
|
|
|
|
// normal urgency-bar is painted green
|
|
case 1:
|
|
cairo_set_source_rgb (cr, 0.0f, 1.0f, 0.0f);
|
|
break;
|
|
|
|
// urgent urgency-bar is painted red
|
|
case 2:
|
|
cairo_set_source_rgb (cr, 1.0f, 0.0f, 0.0f);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
draw_round_rect (
|
|
cr,
|
|
1.0f,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) + 2.0f,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) + 2.0f,
|
|
EM2PIXELS (defaults_get_bubble_corner_radius (d), d) - 2.0f,
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) - 4.0f,
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d) - 2.0f);
|
|
cairo_fill (cr);
|
|
|
|
cairo_set_source_rgb (cr, 0.0f, 0.0f, 0.0f);
|
|
cairo_set_font_size (
|
|
cr,
|
|
EM2PIXELS (defaults_get_text_body_size (d), d));
|
|
cairo_move_to (
|
|
cr,
|
|
EM2PIXELS (defaults_get_text_body_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
2.0f,
|
|
EM2PIXELS (defaults_get_text_body_size (d), d) +
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d) +
|
|
2.0f +
|
|
((2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d) - 2.0f) -
|
|
EM2PIXELS (defaults_get_text_body_size (d), d)) / 2);
|
|
|
|
switch (bubble_get_urgency (self))
|
|
{
|
|
case 0:
|
|
cairo_show_text (
|
|
cr,
|
|
"low - report incorrect urgency?");
|
|
break;
|
|
|
|
case 1:
|
|
cairo_show_text (
|
|
cr,
|
|
"normal - report incorrect urgency?");
|
|
break;
|
|
|
|
case 2:
|
|
cairo_show_text (
|
|
cr,
|
|
"urgent - report incorrect urgency?");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
_render_icon (Bubble* self,
|
|
cairo_t* cr,
|
|
gint x,
|
|
gint y,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
cairo_pattern_t* pattern;
|
|
|
|
tile_paint (priv->tile_icon,
|
|
cr,
|
|
x,
|
|
y,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
|
|
if (priv->alpha)
|
|
{
|
|
cairo_push_group (cr);
|
|
tile_paint (priv->tile_icon,
|
|
cr,
|
|
x,
|
|
y,
|
|
0.0f,
|
|
(gfloat) egg_alpha_get_alpha (priv->alpha) /
|
|
(gfloat) EGG_ALPHA_MAX_ALPHA);
|
|
pattern = cairo_pop_group (cr);
|
|
if (priv->value == -1)
|
|
cairo_set_source_rgba (cr, 0.0f, 0.0f, 0.0f, 1.0f);
|
|
if (priv->value == 101)
|
|
cairo_set_source_rgba (cr, 1.0f, 1.0f, 1.0f, 1.0f);
|
|
cairo_mask (cr, pattern);
|
|
cairo_pattern_destroy (pattern);
|
|
}
|
|
}
|
|
|
|
void
|
|
_render_title (Bubble* self,
|
|
cairo_t* cr,
|
|
gint x,
|
|
gint y,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
tile_paint (self->priv->tile_title,
|
|
cr,
|
|
x,
|
|
y,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
}
|
|
|
|
void
|
|
_render_body (Bubble* self,
|
|
cairo_t* cr,
|
|
gint x,
|
|
gint y,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
tile_paint (self->priv->tile_body,
|
|
cr,
|
|
x,
|
|
y,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
}
|
|
|
|
void
|
|
_render_indicator (Bubble* self,
|
|
cairo_t* cr,
|
|
gint x,
|
|
gint y,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
BubblePrivate* priv = GET_PRIVATE (self);
|
|
cairo_pattern_t* pattern;
|
|
|
|
tile_paint (priv->tile_indicator,
|
|
cr,
|
|
x,
|
|
y,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
|
|
if (priv->alpha)
|
|
{
|
|
cairo_push_group (cr);
|
|
tile_paint (priv->tile_indicator,
|
|
cr,
|
|
x,
|
|
y,
|
|
0.0f,
|
|
(gfloat) egg_alpha_get_alpha (priv->alpha) /
|
|
(gfloat) EGG_ALPHA_MAX_ALPHA);
|
|
pattern = cairo_pop_group (cr);
|
|
if (priv->value == -1)
|
|
cairo_set_source_rgba (cr, 0.0f, 0.0f, 0.0f, 1.0f);
|
|
if (priv->value == 101)
|
|
cairo_set_source_rgba (cr, 1.0f, 1.0f, 1.0f, 1.0f);
|
|
cairo_mask (cr, pattern);
|
|
cairo_pattern_destroy (pattern);
|
|
}
|
|
}
|
|
|
|
void
|
|
_render_layout (Bubble* self,
|
|
cairo_t* cr,
|
|
gdouble alpha_normal,
|
|
gdouble alpha_blur)
|
|
{
|
|
Defaults* d = self->defaults;
|
|
gint shadow = EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
gint icon_half = EM2PIXELS (defaults_get_icon_size (d), d) / 2;
|
|
gint width_half = EM2PIXELS (defaults_get_bubble_width (d), d) / 2;
|
|
gint height_half = EM2PIXELS (defaults_get_bubble_min_height (d), d) / 2;
|
|
gint gauge_half = EM2PIXELS (defaults_get_gauge_size (d), d) / 2;
|
|
gint margin = EM2PIXELS (defaults_get_margin_size (d), d);
|
|
|
|
switch (bubble_get_layout (self))
|
|
{
|
|
case LAYOUT_ICON_ONLY:
|
|
_render_icon (self,
|
|
cr,
|
|
shadow + width_half - icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + height_half - icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_ICON_INDICATOR:
|
|
_render_icon (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
_render_indicator (self,
|
|
cr,
|
|
shadow + 2 * margin + 2 * icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin + icon_half - gauge_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_ICON_TITLE:
|
|
_render_icon (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
_render_title (self,
|
|
cr,
|
|
shadow + 2 * margin + 2 * icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin + icon_half - self->priv->title_height / 2 - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_ICON_TITLE_BODY:
|
|
_render_icon (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
_render_title (self,
|
|
cr,
|
|
shadow + 2 * margin + 2 * icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
_render_body (self,
|
|
cr,
|
|
shadow + 2 * margin + 2 * icon_half - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin + GET_PRIVATE (self)->title_height - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_TITLE_BODY:
|
|
_render_title (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
_render_body (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin + GET_PRIVATE (self)->title_height - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_TITLE_ONLY:
|
|
_render_title (self,
|
|
cr,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
shadow + margin - BUBBLE_CONTENT_BLUR_RADIUS,
|
|
alpha_normal,
|
|
alpha_blur);
|
|
break;
|
|
|
|
case LAYOUT_NONE:
|
|
// should be intercepted by stack_notify_handler()
|
|
g_warning ("WARNING: No layout defined!!!\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// the behind-bubble blur only works with the enabled/working compiz-plugin blur
|
|
// by setting the hint _COMPIZ_WM_WINDOW_BLUR on the bubble-window
|
|
void
|
|
_set_bg_blur (GtkWidget* window,
|
|
gboolean set_blur,
|
|
gint shadow_size)
|
|
{
|
|
glong data[8];
|
|
GtkAllocation a;
|
|
GdkWindow *gdkwindow;
|
|
|
|
// sanity check
|
|
if (!window)
|
|
return;
|
|
|
|
gtk_widget_get_allocation (window, &a);
|
|
gdkwindow = gtk_widget_get_window (window);
|
|
|
|
// this is meant to tell the blur-plugin what and how to blur, somehow
|
|
// the y-coords are interpreted as being CenterGravity, I wonder why
|
|
data[0] = 2; // threshold
|
|
data[1] = 0; // filter
|
|
data[2] = NorthWestGravity; // gravity of top-left
|
|
data[3] = shadow_size; // x-coord of top-left
|
|
data[4] = (-a.height / 2) + shadow_size; // y-coord of top-left
|
|
data[5] = NorthWestGravity; // gravity of bottom-right
|
|
data[6] = a.width - shadow_size; // bottom-right x-coord
|
|
data[7] = (a.height / 2) - shadow_size; // bottom-right y-coord
|
|
|
|
if (set_blur)
|
|
{
|
|
XChangeProperty (GDK_WINDOW_XDISPLAY (gdkwindow),
|
|
GDK_WINDOW_XID (gdkwindow),
|
|
XInternAtom (GDK_WINDOW_XDISPLAY (gdkwindow),
|
|
"_COMPIZ_WM_WINDOW_BLUR",
|
|
FALSE),
|
|
XA_INTEGER,
|
|
32,
|
|
PropModeReplace,
|
|
(guchar *) data,
|
|
8);
|
|
}
|
|
else
|
|
{
|
|
XDeleteProperty (GDK_WINDOW_XDISPLAY (gdkwindow),
|
|
GDK_WINDOW_XID (gdkwindow),
|
|
XInternAtom (GDK_WINDOW_XDISPLAY (gdkwindow),
|
|
"_COMPIZ_WM_WINDOW_BLUR",
|
|
FALSE));
|
|
}
|
|
}
|
|
|
|
static
|
|
void
|
|
screen_changed_handler (GtkWindow* window,
|
|
GdkScreen* old_screen,
|
|
gpointer data)
|
|
{
|
|
GdkScreen* screen = gtk_widget_get_screen (GTK_WIDGET (window));
|
|
GdkVisual* visual = gdk_screen_get_rgba_visual (screen);
|
|
|
|
if (!visual)
|
|
visual = gdk_screen_get_system_visual (screen);
|
|
|
|
gtk_widget_set_visual (GTK_WIDGET (window), visual);
|
|
}
|
|
|
|
static
|
|
void
|
|
update_input_shape (GtkWidget* window)
|
|
{
|
|
cairo_region_t* region = NULL;
|
|
const cairo_rectangle_int_t rect = {0, 0, 1, 1};
|
|
|
|
// sanity check
|
|
if (!window)
|
|
return;
|
|
|
|
// set an 1x1 input-region to allow click-through
|
|
region = cairo_region_create_rectangle (&rect);
|
|
if (cairo_region_status (region) == CAIRO_STATUS_SUCCESS)
|
|
{
|
|
gtk_widget_input_shape_combine_region (window, NULL);
|
|
gtk_widget_input_shape_combine_region (window, region);
|
|
}
|
|
cairo_region_destroy (region);
|
|
}
|
|
|
|
static void
|
|
update_shape (Bubble* self)
|
|
{
|
|
gint width;
|
|
gint height;
|
|
BubblePrivate* priv;
|
|
|
|
// sanity test
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// do we actually need a shape-mask at all?
|
|
if (priv->composited)
|
|
{
|
|
gtk_widget_shape_combine_region (priv->widget, NULL);
|
|
return;
|
|
}
|
|
|
|
// we're not-composited, so deal with mouse-over differently
|
|
if (bubble_is_mouse_over (self))
|
|
{
|
|
cairo_region_t* region = NULL;
|
|
|
|
region = cairo_region_create ();
|
|
gdk_window_shape_combine_region (gtk_widget_get_window (priv->widget),
|
|
region,
|
|
0,
|
|
0);
|
|
cairo_region_destroy (region);
|
|
}
|
|
else
|
|
{
|
|
gtk_widget_get_size_request (priv->widget, &width, &height);
|
|
const cairo_rectangle_int_t rects[] = {{2, 0, width - 4, height},
|
|
{1, 1, width - 2, height - 2},
|
|
{0, 2, width, height - 4}};
|
|
cairo_region_t* region = NULL;
|
|
|
|
region = cairo_region_create_rectangles (rects, 3);
|
|
if (cairo_region_status (region) == CAIRO_STATUS_SUCCESS)
|
|
{
|
|
gtk_widget_shape_combine_region (priv->widget, NULL);
|
|
gtk_widget_shape_combine_region (priv->widget, region);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
composited_changed_handler (GtkWidget* window,
|
|
gpointer data)
|
|
{
|
|
Bubble* bubble;
|
|
|
|
bubble = (Bubble*) G_OBJECT (data);
|
|
|
|
GET_PRIVATE (bubble)->composited = gdk_screen_is_composited (
|
|
gtk_widget_get_screen (window));
|
|
|
|
update_shape (bubble);
|
|
}
|
|
|
|
static
|
|
gboolean
|
|
expose_handler (GtkWidget* window,
|
|
GdkEventExpose* event,
|
|
gpointer data)
|
|
{
|
|
Bubble* bubble;
|
|
cairo_t* cr;
|
|
Defaults* d;
|
|
BubblePrivate* priv;
|
|
|
|
bubble = (Bubble*) G_OBJECT (data);
|
|
|
|
d = bubble->defaults;
|
|
priv = GET_PRIVATE (bubble);
|
|
|
|
cr = gdk_cairo_create (gtk_widget_get_window (window));
|
|
|
|
// clear bubble-background
|
|
cairo_scale (cr, 1.0f, 1.0f);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR);
|
|
cairo_paint (cr);
|
|
cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
|
|
|
|
if (priv->prevent_fade || !priv->composited)
|
|
{
|
|
// render drop-shadow and bubble-background
|
|
_render_background (bubble, cr, 1.0f, 0.0f);
|
|
|
|
// render content of bubble depending on layout
|
|
_render_layout (bubble, cr, 1.0f, 0.0f);
|
|
}
|
|
else
|
|
{
|
|
// render drop-shadow and bubble-background
|
|
_render_background (bubble,
|
|
cr,
|
|
priv->distance,
|
|
1.0f - priv->distance);
|
|
|
|
// render content of bubble depending on layout
|
|
_render_layout (bubble,
|
|
cr,
|
|
priv->distance,
|
|
1.0f - priv->distance);
|
|
}
|
|
|
|
cairo_destroy (cr);
|
|
|
|
_set_bg_blur (window,
|
|
TRUE,
|
|
EM2PIXELS (defaults_get_bubble_shadow_size (d), d));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
redraw_handler (Bubble* bubble)
|
|
{
|
|
GtkWindow* window;
|
|
BubblePrivate* priv;
|
|
|
|
if (!bubble)
|
|
return FALSE;
|
|
|
|
if (!bubble_is_visible (bubble))
|
|
return FALSE;
|
|
|
|
priv = GET_PRIVATE (bubble);
|
|
|
|
if (!priv->composited)
|
|
return TRUE;
|
|
|
|
window = GTK_WINDOW (priv->widget);
|
|
|
|
if (!GTK_IS_WINDOW (window))
|
|
return FALSE;
|
|
|
|
if (priv->alpha == NULL)
|
|
{
|
|
if (priv->distance < 1.0f && !priv->prevent_fade)
|
|
{
|
|
gtk_window_set_opacity (window,
|
|
WINDOW_MIN_OPACITY +
|
|
priv->distance *
|
|
(WINDOW_MAX_OPACITY -
|
|
WINDOW_MIN_OPACITY));
|
|
bubble_refresh (bubble);
|
|
}
|
|
else
|
|
gtk_window_set_opacity (window, WINDOW_MAX_OPACITY);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static
|
|
GdkPixbuf*
|
|
load_icon (const gchar* filename,
|
|
gint icon_size)
|
|
{
|
|
GdkPixbuf* buffer = NULL;
|
|
GdkPixbuf* pixbuf = NULL;
|
|
GtkIconTheme* theme = NULL;
|
|
GError* error = NULL;
|
|
|
|
/* sanity check */
|
|
g_return_val_if_fail (filename, NULL);
|
|
|
|
/* Images referenced must always be local files. */
|
|
if (!strncmp (filename, "file://", 7))
|
|
filename += 7;
|
|
|
|
if (filename[0] == '/')
|
|
{
|
|
/* load image into pixbuf */
|
|
pixbuf = gdk_pixbuf_new_from_file_at_scale (filename,
|
|
icon_size,
|
|
icon_size,
|
|
TRUE,
|
|
NULL);
|
|
} else {
|
|
/* TODO: rewrite, check for SVG support, raise apport
|
|
** notification for low-res icons */
|
|
theme = gtk_icon_theme_get_default ();
|
|
buffer = gtk_icon_theme_load_icon (theme,
|
|
filename,
|
|
icon_size,
|
|
GTK_ICON_LOOKUP_FORCE_SVG |
|
|
GTK_ICON_LOOKUP_GENERIC_FALLBACK |
|
|
GTK_ICON_LOOKUP_FORCE_SIZE,
|
|
&error);
|
|
if (error)
|
|
{
|
|
g_print ("loading icon '%s' caused error: '%s'",
|
|
filename,
|
|
error->message);
|
|
g_error_free (error);
|
|
error = NULL;
|
|
pixbuf = NULL;
|
|
}
|
|
else
|
|
{
|
|
/* copy and unref buffer so on an icon-theme change old
|
|
** icons are not kept in memory due to dangling
|
|
** references, this also makes sure we do not need to
|
|
** connect to GtkWidget::style-set signal for the
|
|
** GdkPixbuf we get from gtk_icon_theme_load_icon() */
|
|
pixbuf = gdk_pixbuf_copy (buffer);
|
|
g_object_unref (buffer);
|
|
}
|
|
}
|
|
|
|
return pixbuf;
|
|
}
|
|
|
|
static
|
|
gboolean
|
|
pointer_update (Bubble* bubble)
|
|
{
|
|
gint pointer_rel_x;
|
|
gint pointer_rel_y;
|
|
gint pointer_abs_x;
|
|
gint pointer_abs_y;
|
|
gint win_x;
|
|
gint win_y;
|
|
gint width;
|
|
gint height;
|
|
GtkWidget* window;
|
|
BubblePrivate* priv;
|
|
|
|
if (!bubble)
|
|
return FALSE;
|
|
|
|
if (!bubble_is_visible (bubble))
|
|
return FALSE;
|
|
|
|
priv = GET_PRIVATE (bubble);
|
|
window = priv->widget;
|
|
|
|
if (!GTK_IS_WINDOW (window))
|
|
return FALSE;
|
|
|
|
if (gtk_widget_get_realized (window))
|
|
{
|
|
gint distance_x = 0;
|
|
gint distance_y = 0;
|
|
|
|
gtk_widget_get_pointer (window, &pointer_rel_x, &pointer_rel_y);
|
|
gtk_window_get_position (GTK_WINDOW (window), &win_x, &win_y);
|
|
pointer_abs_x = win_x + pointer_rel_x;
|
|
pointer_abs_y = win_y + pointer_rel_y;
|
|
gtk_window_get_size (GTK_WINDOW (window), &width, &height);
|
|
if (pointer_abs_x >= win_x &&
|
|
pointer_abs_x <= win_x + width &&
|
|
pointer_abs_y >= win_y &&
|
|
pointer_abs_y <= win_y + height)
|
|
{
|
|
bubble_set_mouse_over (bubble, TRUE);
|
|
}
|
|
else
|
|
{
|
|
bubble_set_mouse_over (bubble, FALSE);
|
|
}
|
|
|
|
if (pointer_abs_x >= win_x &&
|
|
pointer_abs_x <= win_x + width)
|
|
distance_x = 0;
|
|
else
|
|
{
|
|
if (pointer_abs_x < win_x)
|
|
distance_x = abs (pointer_rel_x);
|
|
|
|
if (pointer_abs_x > win_x + width)
|
|
distance_x = abs (pointer_rel_x - width);
|
|
}
|
|
|
|
if (pointer_abs_y >= win_y &&
|
|
pointer_abs_y <= win_y + height)
|
|
distance_y = 0;
|
|
else
|
|
{
|
|
if (pointer_abs_y < win_y)
|
|
distance_y = abs (pointer_rel_y);
|
|
|
|
if (pointer_abs_y > win_y + height)
|
|
distance_y = abs (pointer_rel_y - height);
|
|
}
|
|
|
|
priv->distance = sqrt (distance_x * distance_x +
|
|
distance_y * distance_y) /
|
|
(double) PROXIMITY_THRESHOLD;
|
|
|
|
// mark mouse-pointer having left bubble and proximity-area
|
|
// after inital show-up of bubble
|
|
if (priv->prevent_fade && priv->distance > 1.0f)
|
|
priv->prevent_fade = FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
//-- internal API --------------------------------------------------------------
|
|
|
|
static void
|
|
bubble_dispose (GObject* gobject)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!gobject || !IS_BUBBLE (gobject))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (gobject);
|
|
|
|
if (GTK_IS_WIDGET (priv->widget))
|
|
{
|
|
gtk_widget_destroy (GTK_WIDGET (priv->widget));
|
|
priv->widget = NULL;
|
|
}
|
|
|
|
if (priv->title)
|
|
{
|
|
g_string_free ((gpointer) priv->title,
|
|
TRUE);
|
|
priv->title = NULL;
|
|
}
|
|
|
|
if (priv->message_body)
|
|
{
|
|
g_string_free ((gpointer) priv->message_body,
|
|
TRUE);
|
|
priv->message_body = NULL;
|
|
}
|
|
|
|
if (priv->synchronous)
|
|
{
|
|
g_free ((gpointer) priv->synchronous);
|
|
priv->synchronous = NULL;
|
|
}
|
|
|
|
if (priv->sender)
|
|
{
|
|
g_free ((gpointer) priv->sender);
|
|
priv->sender = NULL;
|
|
}
|
|
|
|
if (priv->icon_pixbuf)
|
|
{
|
|
g_object_unref (priv->icon_pixbuf);
|
|
priv->icon_pixbuf = NULL;
|
|
}
|
|
|
|
if (priv->alpha)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
if (priv->timeline)
|
|
{
|
|
g_object_unref (priv->timeline);
|
|
priv->timeline = NULL;
|
|
}
|
|
|
|
if (priv->pointer_update_id)
|
|
{
|
|
g_source_remove (priv->pointer_update_id);
|
|
priv->pointer_update_id = 0;
|
|
}
|
|
|
|
if (priv->draw_handler_id)
|
|
{
|
|
g_source_remove (priv->draw_handler_id);
|
|
priv->draw_handler_id = 0;
|
|
}
|
|
|
|
if (priv->timer_id)
|
|
{
|
|
g_source_remove (priv->timer_id);
|
|
priv->timer_id = 0;
|
|
}
|
|
|
|
if (priv->tile_background_part)
|
|
{
|
|
tile_destroy (priv->tile_background_part);
|
|
priv->tile_background_part = NULL;
|
|
}
|
|
|
|
if (priv->tile_background)
|
|
{
|
|
tile_destroy (priv->tile_background);
|
|
priv->tile_background = NULL;
|
|
}
|
|
|
|
if (priv->tile_icon)
|
|
{
|
|
tile_destroy (priv->tile_icon);
|
|
priv->tile_icon = NULL;
|
|
}
|
|
|
|
if (priv->tile_title)
|
|
{
|
|
tile_destroy (priv->tile_title);
|
|
priv->tile_title = NULL;
|
|
}
|
|
|
|
if (priv->tile_body)
|
|
{
|
|
tile_destroy (priv->tile_body);
|
|
priv->tile_body = NULL;
|
|
}
|
|
|
|
if (priv->tile_indicator)
|
|
{
|
|
tile_destroy (priv->tile_indicator);
|
|
priv->tile_indicator = NULL;
|
|
}
|
|
|
|
if (priv->old_icon_filename)
|
|
{
|
|
g_string_free ((gpointer) priv->old_icon_filename, TRUE);
|
|
priv->old_icon_filename = NULL;
|
|
}
|
|
|
|
// chain up to the parent class
|
|
G_OBJECT_CLASS (bubble_parent_class)->dispose (gobject);
|
|
}
|
|
|
|
static void
|
|
bubble_finalize (GObject* gobject)
|
|
{
|
|
// chain up to the parent class
|
|
G_OBJECT_CLASS (bubble_parent_class)->finalize (gobject);
|
|
}
|
|
|
|
static void
|
|
bubble_init (Bubble* self)
|
|
{
|
|
// If you need specific construction properties to complete
|
|
// initialization, delay initialization completion until the
|
|
// property is set.
|
|
|
|
BubblePrivate *priv;
|
|
|
|
self->priv = priv = GET_PRIVATE (self);
|
|
priv->layout = LAYOUT_NONE;
|
|
priv->title = NULL;
|
|
priv->message_body = NULL;
|
|
priv->title_needs_refresh = FALSE;
|
|
priv->message_body_needs_refresh = FALSE;
|
|
priv->visible = FALSE;
|
|
priv->icon_pixbuf = NULL;
|
|
priv->value = -2;
|
|
priv->synchronous = NULL;
|
|
priv->sender = NULL;
|
|
priv->draw_handler_id = 0;
|
|
priv->pointer_update_id = 0;
|
|
}
|
|
|
|
static void
|
|
bubble_get_property (GObject* gobject,
|
|
guint prop,
|
|
GValue* value,
|
|
GParamSpec* spec)
|
|
{
|
|
switch (prop)
|
|
{
|
|
default :
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop, spec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
bubble_class_init (BubbleClass* klass)
|
|
{
|
|
GObjectClass* gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
g_type_class_add_private (klass, sizeof (BubblePrivate));
|
|
|
|
gobject_class->dispose = bubble_dispose;
|
|
gobject_class->finalize = bubble_finalize;
|
|
gobject_class->get_property = bubble_get_property;
|
|
|
|
g_bubble_signals[TIMED_OUT] = g_signal_new (
|
|
"timed-out",
|
|
G_OBJECT_CLASS_TYPE (gobject_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (BubbleClass, timed_out),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE,
|
|
0);
|
|
|
|
g_bubble_signals[VALUE_CHANGED] = g_signal_new (
|
|
"value-changed",
|
|
G_OBJECT_CLASS_TYPE (gobject_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (BubbleClass, value_changed),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__INT,
|
|
G_TYPE_NONE,
|
|
1,
|
|
G_TYPE_INT);
|
|
|
|
g_bubble_signals[MESSAGE_BODY_DELETED] = g_signal_new (
|
|
"message-body-deleted",
|
|
G_OBJECT_CLASS_TYPE (gobject_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (BubbleClass, message_body_deleted),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__STRING,
|
|
G_TYPE_NONE,
|
|
1,
|
|
G_TYPE_STRING);
|
|
|
|
g_bubble_signals[MESSAGE_BODY_INSERTED] = g_signal_new (
|
|
"message-body-inserted",
|
|
G_OBJECT_CLASS_TYPE (gobject_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (BubbleClass, message_body_inserted),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__STRING,
|
|
G_TYPE_NONE,
|
|
1,
|
|
G_TYPE_STRING);
|
|
}
|
|
|
|
//-- public API ----------------------------------------------------------------
|
|
|
|
Bubble*
|
|
bubble_new (Defaults* defaults)
|
|
{
|
|
Bubble* this = NULL;
|
|
GtkWidget* window = NULL;
|
|
BubblePrivate* priv;
|
|
GdkRGBA transparent = {0.0, 0.0, 0.0, 0.0};
|
|
|
|
this = g_object_new (BUBBLE_TYPE, NULL);
|
|
if (!this)
|
|
return NULL;
|
|
|
|
this->defaults = defaults;
|
|
priv = GET_PRIVATE (this);
|
|
|
|
priv->widget = bubble_window_new();
|
|
window = priv->widget;
|
|
if (!window)
|
|
return NULL;
|
|
|
|
g_object_set_data (G_OBJECT(window), "bubble", (gpointer) this);
|
|
|
|
gtk_window_set_type_hint (GTK_WINDOW (window),
|
|
GDK_WINDOW_TYPE_HINT_NOTIFICATION);
|
|
gtk_window_set_skip_pager_hint (GTK_WINDOW (window), TRUE);
|
|
gtk_window_set_skip_taskbar_hint (GTK_WINDOW (window), TRUE);
|
|
gtk_window_stick (GTK_WINDOW (window));
|
|
|
|
gtk_widget_add_events (window,
|
|
GDK_POINTER_MOTION_MASK |
|
|
GDK_BUTTON_PRESS_MASK |
|
|
GDK_BUTTON_RELEASE_MASK);
|
|
|
|
// hook up input/event handlers to window
|
|
g_signal_connect (G_OBJECT (window),
|
|
"screen-changed",
|
|
G_CALLBACK (screen_changed_handler),
|
|
NULL);
|
|
g_signal_connect (G_OBJECT (window),
|
|
"composited-changed",
|
|
G_CALLBACK (composited_changed_handler),
|
|
this);
|
|
|
|
gtk_window_move (GTK_WINDOW (window), 0, 0);
|
|
|
|
// make sure the window opens with a RGBA-visual
|
|
screen_changed_handler (GTK_WINDOW (window), NULL, NULL);
|
|
gtk_widget_realize (window);
|
|
gdk_window_set_background_rgba (gtk_widget_get_window (window),
|
|
&transparent);
|
|
|
|
// hook up window-event handlers to window
|
|
g_signal_connect (G_OBJECT (window),
|
|
"draw",
|
|
G_CALLBACK (expose_handler),
|
|
this);
|
|
|
|
// "clear" input-mask, set title/icon/attributes
|
|
gtk_widget_set_app_paintable (window, TRUE);
|
|
gtk_window_set_title (GTK_WINDOW (window), "notify-osd");
|
|
gtk_window_set_decorated (GTK_WINDOW (window), FALSE);
|
|
gtk_window_set_keep_above (GTK_WINDOW (window), TRUE);
|
|
gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
|
|
gtk_window_set_focus_on_map (GTK_WINDOW (window), FALSE);
|
|
gtk_window_set_accept_focus (GTK_WINDOW (window), FALSE);
|
|
gtk_window_set_opacity (GTK_WINDOW (window), 0.0f);
|
|
|
|
// TODO: fold some of that back into bubble_init
|
|
this->priv = GET_PRIVATE (this);
|
|
this->priv->layout = LAYOUT_NONE;
|
|
this->priv->widget = window;
|
|
this->priv->title = g_string_new ("");
|
|
this->priv->message_body = g_string_new ("");
|
|
this->priv->title_needs_refresh = FALSE;
|
|
this->priv->message_body_needs_refresh = FALSE;
|
|
this->priv->icon_pixbuf = NULL;
|
|
this->priv->value = -2;
|
|
this->priv->visible = FALSE;
|
|
this->priv->timeout = 5000;
|
|
this->priv->mouse_over = FALSE;
|
|
this->priv->distance = 1.0f;
|
|
this->priv->composited = gdk_screen_is_composited (
|
|
gtk_widget_get_screen (window));
|
|
this->priv->alpha = NULL;
|
|
this->priv->timeline = NULL;
|
|
this->priv->title_width = 0;
|
|
this->priv->title_height = 0;
|
|
this->priv->body_width = 0;
|
|
this->priv->body_height = 0;
|
|
this->priv->append = FALSE;
|
|
this->priv->icon_only = FALSE;
|
|
this->priv->tile_background_part = NULL;
|
|
this->priv->tile_background = NULL;
|
|
this->priv->tile_icon = NULL;
|
|
this->priv->tile_title = NULL;
|
|
this->priv->tile_body = NULL;
|
|
this->priv->tile_indicator = NULL;
|
|
this->priv->prevent_fade = FALSE;
|
|
this->priv->old_icon_filename = g_string_new ("");
|
|
|
|
update_input_shape (window);
|
|
|
|
return this;
|
|
}
|
|
|
|
gchar*
|
|
bubble_get_synchronous (Bubble* self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), NULL);
|
|
|
|
return GET_PRIVATE (self)->synchronous;
|
|
}
|
|
|
|
gchar*
|
|
bubble_get_sender (Bubble* self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), NULL);
|
|
|
|
return GET_PRIVATE (self)->sender;
|
|
}
|
|
|
|
void
|
|
bubble_set_title (Bubble* self,
|
|
const gchar* title)
|
|
{
|
|
gchar* text;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// convert any newline to space
|
|
text = newline_to_space (title);
|
|
|
|
if (priv->title)
|
|
{
|
|
if (g_strcmp0 (priv->title->str, text))
|
|
priv->title_needs_refresh = TRUE;
|
|
|
|
g_string_free (priv->title, TRUE);
|
|
}
|
|
|
|
priv->title = g_string_new (text);
|
|
|
|
g_object_notify (
|
|
G_OBJECT (gtk_widget_get_accessible (GET_PRIVATE(self)->widget)),
|
|
"accessible-name");
|
|
|
|
g_free (text);
|
|
}
|
|
|
|
const gchar*
|
|
bubble_get_title (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return NULL;
|
|
|
|
return GET_PRIVATE (self)->title->str;
|
|
}
|
|
|
|
void
|
|
bubble_set_message_body (Bubble* self,
|
|
const gchar* body)
|
|
{
|
|
gchar* text;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// filter out any HTML/markup if possible
|
|
text = filter_text (body);
|
|
|
|
if (priv->message_body)
|
|
if (g_strcmp0 (priv->message_body->str, text))
|
|
priv->message_body_needs_refresh = TRUE;
|
|
|
|
if (priv->message_body && priv->message_body->len != 0)
|
|
{
|
|
g_signal_emit (self,
|
|
g_bubble_signals[MESSAGE_BODY_DELETED],
|
|
0,
|
|
priv->message_body->str);
|
|
g_string_free (priv->message_body, TRUE);
|
|
}
|
|
|
|
priv->message_body = g_string_new (text);
|
|
|
|
g_signal_emit (self, g_bubble_signals[MESSAGE_BODY_INSERTED], 0, text);
|
|
g_object_notify (G_OBJECT (gtk_widget_get_accessible (priv->widget)),
|
|
"accessible-description");
|
|
|
|
g_free (text);
|
|
}
|
|
|
|
const gchar*
|
|
bubble_get_message_body (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return NULL;
|
|
|
|
return GET_PRIVATE (self)->message_body->str;
|
|
}
|
|
|
|
void
|
|
bubble_set_icon_from_path (Bubble* self,
|
|
const gchar* filepath)
|
|
{
|
|
Defaults* d;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self) || !g_strcmp0 (filepath, ""))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// check if an app tries to set the same file as icon again, this check
|
|
// avoids superfluous regeneration of the tile/blur-cache for the icon,
|
|
// thus it improves performance in update- and append-cases
|
|
if (!g_strcmp0 (priv->old_icon_filename->str, filepath))
|
|
return;
|
|
|
|
// store the new icon-basename
|
|
g_string_assign (priv->old_icon_filename, filepath);
|
|
|
|
if (priv->icon_pixbuf)
|
|
{
|
|
g_object_unref (priv->icon_pixbuf);
|
|
priv->icon_pixbuf = NULL;
|
|
}
|
|
|
|
d = self->defaults;
|
|
priv->icon_pixbuf = load_icon (filepath,
|
|
EM2PIXELS (defaults_get_icon_size (d), d));
|
|
|
|
_refresh_icon (self);
|
|
}
|
|
|
|
void
|
|
bubble_set_icon (Bubble* self,
|
|
const gchar* filename)
|
|
{
|
|
Defaults* d;
|
|
BubblePrivate* priv;
|
|
#ifdef TEMPORARY_ICON_PREFIX_WORKAROUND
|
|
gchar* notify_osd_iconname;
|
|
#endif
|
|
|
|
if (!self || !IS_BUBBLE (self) || !g_strcmp0 (filename, ""))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
//basename = g_path_get_basename (filename);
|
|
|
|
// check if an app tries to set the same file as icon again, this check
|
|
// avoids superfluous regeneration of the tile/blur-cache for the icon,
|
|
// thus it improves performance in update- and append-cases
|
|
if (!g_strcmp0 (priv->old_icon_filename->str, filename))
|
|
return;
|
|
|
|
// store the new icon-basename
|
|
g_string_assign (priv->old_icon_filename, filename);
|
|
|
|
if (priv->icon_pixbuf)
|
|
{
|
|
g_object_unref (priv->icon_pixbuf);
|
|
priv->icon_pixbuf = NULL;
|
|
}
|
|
|
|
d = self->defaults;
|
|
|
|
#ifdef TEMPORARY_ICON_PREFIX_WORKAROUND
|
|
notify_osd_iconname = g_strdup_printf (NOTIFY_OSD_ICON_PREFIX "-%s",
|
|
filename);
|
|
priv->icon_pixbuf = load_icon (notify_osd_iconname,
|
|
EM2PIXELS (defaults_get_icon_size (d),
|
|
d));
|
|
g_free (notify_osd_iconname);
|
|
#endif
|
|
|
|
// fallback to non-notify-osd name
|
|
if (!priv->icon_pixbuf)
|
|
priv->icon_pixbuf = load_icon (filename,
|
|
EM2PIXELS (defaults_get_icon_size (d), d));
|
|
|
|
_refresh_icon (self);
|
|
}
|
|
|
|
static GdkPixbuf *
|
|
scale_pixbuf (const GdkPixbuf *pixbuf, gint size)
|
|
{
|
|
GdkPixbuf *scaled_icon;
|
|
GdkPixbuf *new_icon;
|
|
gint w, h, dest_x, dest_y, new_width, new_height, max_edge;
|
|
|
|
dest_x = dest_y = 0;
|
|
|
|
w = gdk_pixbuf_get_width (pixbuf);
|
|
h = gdk_pixbuf_get_height (pixbuf);
|
|
|
|
max_edge = MAX (w, h);
|
|
|
|
// temporarily cast to float so we don't end up missing fractional parts
|
|
// from the division, especially nasty for 0.something :)
|
|
// e.g.: 99 / 100 = 0 but 99.0 / 100.0 = 0.99
|
|
new_width = size * ((gfloat) w / (gfloat) max_edge);
|
|
new_height = size * ((gfloat) h / (gfloat) max_edge);
|
|
|
|
/* Scale the pixbuf down, preserving the aspect ratio */
|
|
scaled_icon = gdk_pixbuf_scale_simple (pixbuf,
|
|
new_width,
|
|
new_height,
|
|
GDK_INTERP_BILINEAR);
|
|
|
|
if (w == h)
|
|
return scaled_icon;
|
|
|
|
/* Create a square pixbuf with an alpha channel */
|
|
new_icon = gdk_pixbuf_new (gdk_pixbuf_get_colorspace (scaled_icon),
|
|
TRUE,
|
|
gdk_pixbuf_get_bits_per_sample (scaled_icon),
|
|
size, size);
|
|
|
|
/* Clear the pixbuf so it is transparent */
|
|
gdk_pixbuf_fill (new_icon, 0x00000000);
|
|
|
|
/* Center the rectangular pixbuf inside the transparent square */
|
|
if (new_width > new_height)
|
|
dest_y = (new_width - new_height) / 2;
|
|
else
|
|
dest_x = (new_height - new_width) / 2;
|
|
|
|
/* Copy the rectangular pixbuf into the new pixbuf at a centered position */
|
|
gdk_pixbuf_copy_area (scaled_icon,
|
|
0, 0,
|
|
gdk_pixbuf_get_width (scaled_icon),
|
|
gdk_pixbuf_get_height (scaled_icon),
|
|
new_icon,
|
|
dest_x, dest_y);
|
|
g_object_unref (scaled_icon);
|
|
|
|
return new_icon;
|
|
}
|
|
|
|
void
|
|
bubble_set_icon_from_pixbuf (Bubble* self,
|
|
GdkPixbuf* pixbuf)
|
|
{
|
|
GdkPixbuf* scaled;
|
|
gint height;
|
|
gint width;
|
|
Defaults* d;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self) || !pixbuf)
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// "reset" the stored the icon-filename, fixes LP: #451086
|
|
g_string_assign (priv->old_icon_filename, "\0");
|
|
|
|
if (priv->icon_pixbuf)
|
|
{
|
|
g_object_unref (priv->icon_pixbuf);
|
|
priv->icon_pixbuf = NULL;
|
|
}
|
|
|
|
height = gdk_pixbuf_get_height (pixbuf);
|
|
width = gdk_pixbuf_get_width (pixbuf);
|
|
|
|
d = self->defaults;
|
|
|
|
if (width != defaults_get_icon_size (d) ||
|
|
height != defaults_get_icon_size (d))
|
|
{
|
|
scaled = scale_pixbuf (pixbuf, EM2PIXELS (defaults_get_icon_size (d), d));
|
|
g_object_unref (pixbuf);
|
|
pixbuf = scaled;
|
|
}
|
|
|
|
priv->icon_pixbuf = pixbuf;
|
|
|
|
_refresh_icon (self);
|
|
}
|
|
|
|
GdkPixbuf*
|
|
bubble_get_icon_pixbuf (Bubble *self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), NULL);
|
|
|
|
return GET_PRIVATE (self)->icon_pixbuf;
|
|
}
|
|
|
|
void
|
|
bubble_set_value (Bubble* self,
|
|
gint value)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// only really cause a refresh (blur, recreating tile-/blur-cache) if
|
|
// a different value has been set, this helps improve performance when
|
|
// a user e.g. presses and holds down keys for Volume-Up/Down or
|
|
// Brightness-Up/Down and apps like gnome-settings-daemon or
|
|
// gnome-power-daemon fire continues notification-updates due to the
|
|
// key-repeat events
|
|
if (priv->value != value)
|
|
{
|
|
priv->value = value;
|
|
g_signal_emit (self, g_bubble_signals[VALUE_CHANGED], 0, value);
|
|
_refresh_indicator (self);
|
|
}
|
|
}
|
|
|
|
gint
|
|
bubble_get_value (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return -2;
|
|
|
|
return GET_PRIVATE (self)->value;
|
|
}
|
|
|
|
void
|
|
bubble_set_size (Bubble* self,
|
|
gint width,
|
|
gint height)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
gtk_widget_set_size_request (GET_PRIVATE(self)->widget, width, height);
|
|
}
|
|
|
|
void
|
|
bubble_get_size (Bubble* self,
|
|
gint* width,
|
|
gint* height)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
gtk_widget_get_size_request (GET_PRIVATE(self)->widget, width, height);
|
|
}
|
|
|
|
void
|
|
bubble_set_timeout (Bubble* self,
|
|
guint timeout)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
GET_PRIVATE (self)->timeout = timeout;
|
|
}
|
|
|
|
/* a timeout of 0 doesn't make much sense now does it, thus 0 indicates an
|
|
** error */
|
|
guint
|
|
bubble_get_timeout (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
return GET_PRIVATE(self)->timeout;
|
|
}
|
|
|
|
void
|
|
bubble_set_timer_id (Bubble* self,
|
|
guint timer_id)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
GET_PRIVATE(self)->timer_id = timer_id;
|
|
}
|
|
|
|
/* a valid GLib timer-id is always > 0, thus 0 indicates an error */
|
|
guint
|
|
bubble_get_timer_id (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
return GET_PRIVATE(self)->timer_id;
|
|
}
|
|
|
|
void
|
|
bubble_set_mouse_over (Bubble* self,
|
|
gboolean flag)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
/* did anything change? */
|
|
if (priv->mouse_over != flag)
|
|
{
|
|
priv->mouse_over = flag;
|
|
update_shape (self);
|
|
|
|
if (flag) {
|
|
g_debug("mouse entered bubble, supressing timeout");
|
|
bubble_clear_timer(self);
|
|
} else {
|
|
g_debug("mouse left bubble, continuing timeout");
|
|
bubble_set_timeout(self, 3000);
|
|
bubble_start_timer(self, TRUE);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gboolean
|
|
bubble_is_mouse_over (Bubble* self)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return FALSE;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
if (priv->prevent_fade)
|
|
return FALSE;
|
|
|
|
return priv->mouse_over;
|
|
}
|
|
|
|
void
|
|
bubble_move (Bubble* self,
|
|
gint x,
|
|
gint y)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
gtk_window_move (GTK_WINDOW (GET_PRIVATE (self)->widget), x, y);
|
|
}
|
|
|
|
static void
|
|
glow_completed_cb (EggTimeline *timeline,
|
|
Bubble *bubble)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (bubble));
|
|
|
|
priv = GET_PRIVATE (bubble);
|
|
|
|
/* get rid of the alpha, so that the mouse-over algorithm notices */
|
|
if (priv->alpha)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
if (priv->timeline)
|
|
{
|
|
g_object_unref (priv->timeline);
|
|
priv->timeline = NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
glow_cb (EggTimeline *timeline,
|
|
gint frame_no,
|
|
Bubble *bubble)
|
|
{
|
|
g_return_if_fail (IS_BUBBLE (bubble));
|
|
|
|
bubble_refresh (bubble);
|
|
}
|
|
|
|
static void
|
|
bubble_start_glow_effect (Bubble *self,
|
|
guint msecs)
|
|
{
|
|
EggTimeline* timeline;
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
timeline = egg_timeline_new_for_duration (msecs);
|
|
egg_timeline_set_speed (timeline, FPS);
|
|
priv->timeline = timeline;
|
|
|
|
if (priv->alpha != NULL)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
priv->alpha = egg_alpha_new_full (timeline,
|
|
EGG_ALPHA_RAMP_DEC,
|
|
NULL,
|
|
NULL);
|
|
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"completed",
|
|
G_CALLBACK (glow_completed_cb),
|
|
self);
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"new-frame",
|
|
G_CALLBACK (glow_cb),
|
|
self);
|
|
|
|
egg_timeline_start (timeline);
|
|
}
|
|
|
|
void
|
|
bubble_show (Bubble* self)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
priv->visible = TRUE;
|
|
gtk_widget_show_all (priv->widget);
|
|
|
|
// check if mouse-pointer is over bubble (and proximity-area) initially
|
|
pointer_update (self);
|
|
if (priv->distance <= 1.0f)
|
|
priv->prevent_fade = TRUE;
|
|
else
|
|
priv->prevent_fade = FALSE;
|
|
|
|
// FIXME: do nasty busy-polling rendering in the drawing-area
|
|
priv->draw_handler_id = g_timeout_add (1000/FPS,
|
|
(GSourceFunc) redraw_handler,
|
|
self);
|
|
|
|
// FIXME: read out current mouse-pointer position every 1/25 second
|
|
priv->pointer_update_id = g_timeout_add (1000/FPS,
|
|
(GSourceFunc) pointer_update,
|
|
self);
|
|
}
|
|
|
|
/* mostly called when we change the content of the bubble
|
|
and need to force a redraw
|
|
*/
|
|
void
|
|
bubble_refresh (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
/* force a redraw */
|
|
gtk_widget_queue_draw (GET_PRIVATE (self)->widget);
|
|
}
|
|
|
|
static inline gboolean
|
|
bubble_is_composited (Bubble *bubble)
|
|
{
|
|
/* no g_return_if_fail(), the caller should have already
|
|
checked that */
|
|
return gtk_widget_is_composited (GET_PRIVATE (bubble)->widget);
|
|
}
|
|
|
|
static inline GtkWindow*
|
|
bubble_get_window (Bubble *bubble)
|
|
{
|
|
return GTK_WINDOW (GET_PRIVATE (bubble)->widget);
|
|
}
|
|
|
|
static void
|
|
fade_cb (EggTimeline *timeline,
|
|
gint frame_no,
|
|
Bubble *bubble)
|
|
{
|
|
float opacity;
|
|
|
|
g_return_if_fail (IS_BUBBLE (bubble));
|
|
|
|
opacity = (float)egg_alpha_get_alpha (GET_PRIVATE (bubble)->alpha)
|
|
/ (float)EGG_ALPHA_MAX_ALPHA
|
|
* WINDOW_MAX_OPACITY;
|
|
|
|
if (bubble_is_mouse_over (bubble))
|
|
gtk_window_set_opacity (bubble_get_window (bubble),
|
|
WINDOW_MIN_OPACITY);
|
|
else
|
|
gtk_window_set_opacity (bubble_get_window (bubble), opacity);
|
|
}
|
|
|
|
static void
|
|
fade_out_completed_cb (EggTimeline *timeline,
|
|
Bubble *bubble)
|
|
{
|
|
g_return_if_fail (IS_BUBBLE (bubble));
|
|
|
|
bubble_hide (bubble);
|
|
|
|
dbus_send_close_signal (bubble_get_sender (bubble),
|
|
bubble_get_id (bubble),
|
|
1);
|
|
g_signal_emit (bubble, g_bubble_signals[TIMED_OUT], 0);
|
|
}
|
|
|
|
|
|
static void
|
|
fade_in_completed_cb (EggTimeline* timeline,
|
|
Bubble* bubble)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (bubble));
|
|
|
|
priv = GET_PRIVATE (bubble);
|
|
|
|
/* get rid of the alpha, so that the mouse-over algorithm notices */
|
|
if (priv->alpha)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
if (priv->timeline)
|
|
{
|
|
g_object_unref (priv->timeline);
|
|
priv->timeline = NULL;
|
|
}
|
|
|
|
if (bubble_is_mouse_over (bubble))
|
|
gtk_window_set_opacity (bubble_get_window (bubble),
|
|
WINDOW_MIN_OPACITY);
|
|
else
|
|
gtk_window_set_opacity (bubble_get_window (bubble),
|
|
WINDOW_MAX_OPACITY);
|
|
|
|
bubble_start_timer (bubble, TRUE);
|
|
}
|
|
|
|
void
|
|
bubble_fade_in (Bubble* self,
|
|
guint msecs)
|
|
{
|
|
EggTimeline* timeline;
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
if (!bubble_is_composited (self)
|
|
|| msecs == 0)
|
|
{
|
|
bubble_show (self);
|
|
bubble_start_timer (self, TRUE);
|
|
return;
|
|
}
|
|
|
|
timeline = egg_timeline_new_for_duration (msecs);
|
|
egg_timeline_set_speed (timeline, FPS);
|
|
|
|
if (priv->alpha != NULL)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
priv->alpha = egg_alpha_new_full (timeline,
|
|
EGG_ALPHA_RAMP_INC,
|
|
NULL,
|
|
NULL);
|
|
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"completed",
|
|
G_CALLBACK (fade_in_completed_cb),
|
|
self);
|
|
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"new-frame",
|
|
G_CALLBACK (fade_cb),
|
|
self);
|
|
|
|
egg_timeline_start (timeline);
|
|
|
|
gtk_window_set_opacity (bubble_get_window (self), 0.0f);
|
|
|
|
bubble_show (self);
|
|
}
|
|
|
|
void
|
|
bubble_fade_out (Bubble* self,
|
|
guint msecs)
|
|
{
|
|
EggTimeline* timeline;
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
timeline = egg_timeline_new_for_duration (msecs);
|
|
egg_timeline_set_speed (timeline, FPS);
|
|
priv->timeline = timeline;
|
|
|
|
if (priv->alpha != NULL)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
|
|
priv->alpha = egg_alpha_new_full (timeline,
|
|
EGG_ALPHA_RAMP_DEC,
|
|
NULL,
|
|
NULL);
|
|
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"completed",
|
|
G_CALLBACK (fade_out_completed_cb),
|
|
self);
|
|
g_signal_connect (G_OBJECT (timeline),
|
|
"new-frame",
|
|
G_CALLBACK (fade_cb),
|
|
self);
|
|
|
|
egg_timeline_start (timeline);
|
|
}
|
|
|
|
gboolean
|
|
bubble_timed_out (Bubble* self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), FALSE);
|
|
|
|
if (GET_PRIVATE (self)->composited)
|
|
{
|
|
bubble_fade_out (self, 300);
|
|
return FALSE;
|
|
}
|
|
|
|
bubble_hide (self);
|
|
|
|
dbus_send_close_signal (bubble_get_sender (self),
|
|
bubble_get_id (self),
|
|
1);
|
|
g_signal_emit (self, g_bubble_signals[TIMED_OUT], 0);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
void
|
|
bubble_hide (Bubble* self)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self) || !bubble_is_visible (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
priv->visible = FALSE;
|
|
gtk_widget_hide (priv->widget);
|
|
|
|
priv->timeout = 0;
|
|
|
|
if (priv->timeline)
|
|
{
|
|
egg_timeline_stop (priv->timeline);
|
|
g_object_unref (priv->timeline);
|
|
priv->timeline = NULL;
|
|
}
|
|
|
|
if (priv->alpha)
|
|
{
|
|
g_object_unref (priv->alpha);
|
|
priv->alpha = NULL;
|
|
}
|
|
}
|
|
|
|
void
|
|
bubble_set_id (Bubble* self,
|
|
guint id)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
GET_PRIVATE (self)->id = id;
|
|
}
|
|
|
|
guint
|
|
bubble_get_id (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
return GET_PRIVATE (self)->id;
|
|
}
|
|
|
|
gboolean
|
|
bubble_is_visible (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return FALSE;
|
|
|
|
return GET_PRIVATE (self)->visible;
|
|
}
|
|
|
|
void
|
|
bubble_start_timer (Bubble* self,
|
|
gboolean trigger)
|
|
{
|
|
guint timer_id;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
timer_id = bubble_get_timer_id (self);
|
|
if (timer_id > 0)
|
|
g_source_remove (timer_id);
|
|
|
|
/* and now let the timer tick... we use g_timeout_add_seconds() here
|
|
** because for the bubble timeouts microsecond-precise resolution is not
|
|
** needed, furthermore this also allows glib more room for optimizations
|
|
** and improve system-power-usage to be more efficient */
|
|
bubble_set_timer_id (
|
|
self,
|
|
g_timeout_add_seconds (bubble_get_timeout (self) / 1000,
|
|
(GSourceFunc) bubble_timed_out,
|
|
self));
|
|
|
|
/* if the bubble is displaying a value that is out of bounds
|
|
trigger a dim/glow animation */
|
|
if (trigger)
|
|
if (priv->value == -1 || priv->value == 101)
|
|
bubble_start_glow_effect (self, 500);
|
|
}
|
|
|
|
void
|
|
bubble_clear_timer (Bubble* self)
|
|
{
|
|
guint timer_id;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
timer_id = GET_PRIVATE(self)->timer_id;
|
|
|
|
if (timer_id > 0) {
|
|
g_source_remove (timer_id);
|
|
bubble_set_timer_id(self, 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
bubble_get_position (Bubble* self,
|
|
gint* x,
|
|
gint* y)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
gtk_window_get_position (GTK_WINDOW (GET_PRIVATE (self)->widget),
|
|
x, y);
|
|
}
|
|
|
|
gint
|
|
bubble_get_height (Bubble *self)
|
|
{
|
|
gint width;
|
|
gint height;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
gtk_window_get_size (GTK_WINDOW (GET_PRIVATE (self)->widget),
|
|
&width,
|
|
&height);
|
|
|
|
return height;
|
|
}
|
|
|
|
gint
|
|
bubble_get_future_height (Bubble *self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
return GET_PRIVATE (self)->future_height;
|
|
}
|
|
|
|
gint
|
|
_calc_title_height (Bubble* self,
|
|
gint title_width /* requested text-width in pixels */)
|
|
{
|
|
Defaults* d;
|
|
cairo_surface_t* surface;
|
|
cairo_t* cr;
|
|
PangoFontDescription* desc = NULL;
|
|
PangoLayout* layout = NULL;
|
|
PangoRectangle log_rect = {0, 0, 0, 0};
|
|
gint title_height;
|
|
BubblePrivate* priv;
|
|
gchar* text_font_face = NULL;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
d = self->defaults;
|
|
priv = GET_PRIVATE (self);
|
|
|
|
surface = cairo_image_surface_create (CAIRO_FORMAT_A1, 1, 1);
|
|
if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) {
|
|
if (surface)
|
|
cairo_surface_destroy (surface);
|
|
return 0;
|
|
}
|
|
|
|
cr = cairo_create (surface);
|
|
cairo_surface_destroy (surface);
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS) {
|
|
if (cr)
|
|
cairo_destroy (cr);
|
|
return 0;
|
|
}
|
|
|
|
layout = pango_cairo_create_layout (cr);
|
|
text_font_face = defaults_get_text_font_face (d);
|
|
desc = pango_font_description_from_string (text_font_face);
|
|
g_free ((gpointer) text_font_face);
|
|
|
|
// make sure system-wide font-options like hinting, antialiasing etc.
|
|
// are taken into account
|
|
pango_cairo_context_set_font_options (
|
|
pango_layout_get_context (layout),
|
|
gdk_screen_get_font_options (
|
|
gtk_widget_get_screen (priv->widget)));
|
|
pango_cairo_context_set_resolution (pango_layout_get_context (layout),
|
|
defaults_get_screen_dpi (d));
|
|
pango_layout_context_changed (layout);
|
|
|
|
pango_font_description_set_size (desc,
|
|
defaults_get_system_font_size (d) *
|
|
defaults_get_text_title_size (d) *
|
|
PANGO_SCALE);
|
|
|
|
pango_font_description_set_weight (
|
|
desc,
|
|
defaults_get_text_title_weight (d));
|
|
|
|
pango_layout_set_font_description (layout, desc);
|
|
pango_font_description_free (desc);
|
|
|
|
pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END);
|
|
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
|
pango_layout_set_width (layout, title_width * PANGO_SCALE);
|
|
pango_layout_set_height (layout, -3);
|
|
|
|
pango_layout_set_text (layout, priv->title->str, priv->title->len);
|
|
|
|
pango_layout_get_extents (layout, NULL, &log_rect);
|
|
title_height = PANGO_PIXELS (log_rect.height);
|
|
g_object_unref (layout);
|
|
cairo_destroy (cr);
|
|
|
|
return title_height;
|
|
}
|
|
|
|
gint
|
|
_calc_body_height (Bubble* self,
|
|
gint body_width /* requested text-width in pixels */)
|
|
{
|
|
Defaults* d;
|
|
cairo_t* cr;
|
|
PangoFontDescription* desc = NULL;
|
|
PangoLayout* layout = NULL;
|
|
PangoRectangle log_rect;
|
|
gint body_height;
|
|
BubblePrivate* priv;
|
|
gchar* text_font_face = NULL;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return 0;
|
|
|
|
d = self->defaults;
|
|
priv = GET_PRIVATE (self);
|
|
|
|
cr = gdk_cairo_create (gtk_widget_get_window (priv->widget));
|
|
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS) {
|
|
if (cr)
|
|
cairo_destroy (cr);
|
|
return 0;
|
|
}
|
|
|
|
layout = pango_cairo_create_layout (cr);
|
|
text_font_face = defaults_get_text_font_face (d);
|
|
desc = pango_font_description_from_string (text_font_face);
|
|
g_free ((gpointer) text_font_face);
|
|
|
|
// make sure system-wide font-options like hinting, antialiasing etc.
|
|
// are taken into account
|
|
pango_cairo_context_set_font_options (
|
|
pango_layout_get_context (layout),
|
|
gdk_screen_get_font_options (
|
|
gtk_widget_get_screen (priv->widget)));
|
|
pango_cairo_context_set_resolution (pango_layout_get_context (layout),
|
|
defaults_get_screen_dpi (d));
|
|
pango_layout_context_changed (layout);
|
|
|
|
pango_font_description_set_size (desc,
|
|
defaults_get_system_font_size (d) *
|
|
defaults_get_text_body_size (d) *
|
|
PANGO_SCALE);
|
|
|
|
pango_font_description_set_weight (
|
|
desc,
|
|
defaults_get_text_body_weight (d));
|
|
|
|
pango_layout_set_font_description (layout, desc);
|
|
|
|
pango_layout_set_wrap (layout, PANGO_WRAP_WORD_CHAR);
|
|
pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_MIDDLE);
|
|
pango_layout_set_width (layout, body_width * PANGO_SCALE);
|
|
pango_layout_set_height (layout, -10);
|
|
|
|
pango_layout_set_text (layout,
|
|
priv->message_body->str,
|
|
priv->message_body->len);
|
|
|
|
/* enforce the 10 line-limit, usually only triggered by appended
|
|
** body-message text due to the newline-characters added with each
|
|
** append */
|
|
if (pango_layout_get_line_count (layout) > 10)
|
|
{
|
|
/* get it down to 9 lines, because we add our own ...-line */
|
|
while (pango_layout_get_line_count (layout) > 9)
|
|
{
|
|
GString* string = NULL;
|
|
|
|
/* cut leading chunk of text up to the first newline */
|
|
string = g_string_new (g_strstr_len (priv->message_body->str,
|
|
priv->message_body->len,
|
|
"\n"));
|
|
|
|
/* also cut the first newline itself */
|
|
string = g_string_erase (string, 0, 1);
|
|
|
|
/* copy that stripped text back to the body-message */
|
|
g_string_assign (priv->message_body, string->str);
|
|
|
|
/* set the new body-message text to the pango-layout */
|
|
pango_layout_set_text (layout,
|
|
priv->message_body->str,
|
|
priv->message_body->len);
|
|
|
|
/* clean up */
|
|
g_string_free (string, TRUE);
|
|
}
|
|
|
|
/* add our own ellipsize-line */
|
|
g_string_prepend (priv->message_body, "...\n");
|
|
|
|
/* set final body-message text to the pango-layout again */
|
|
pango_layout_set_text (layout,
|
|
priv->message_body->str,
|
|
priv->message_body->len);
|
|
}
|
|
|
|
pango_layout_get_extents (layout, NULL, &log_rect);
|
|
body_height = PANGO_PIXELS (log_rect.height);
|
|
|
|
pango_font_description_free (desc);
|
|
g_object_unref (layout);
|
|
cairo_destroy (cr);
|
|
|
|
return body_height;
|
|
}
|
|
|
|
void
|
|
bubble_recalc_size (Bubble *self)
|
|
{
|
|
gint old_bubble_width = 0;
|
|
gint old_bubble_height = 0;
|
|
gint new_bubble_width = 0;
|
|
gint new_bubble_height = 0;
|
|
Defaults* d;
|
|
BubblePrivate* priv;
|
|
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
d = self->defaults;
|
|
priv = GET_PRIVATE (self);
|
|
|
|
/* FIXME: a quick fix to rescale an icon (e.g. user changed font-size or
|
|
** DPI while a bubble is displayed, thus bubble is re-rendered and the
|
|
** icon needs to adapt to the new size) */
|
|
if (priv->icon_pixbuf)
|
|
{
|
|
GdkPixbuf *pixbuf;
|
|
pixbuf = gdk_pixbuf_scale_simple (
|
|
priv->icon_pixbuf,
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
EM2PIXELS (defaults_get_icon_size (d), d),
|
|
GDK_INTERP_BILINEAR);
|
|
g_object_unref (priv->icon_pixbuf);
|
|
priv->icon_pixbuf = pixbuf;
|
|
}
|
|
|
|
bubble_determine_layout (self);
|
|
|
|
new_bubble_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) +
|
|
2 * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
|
|
switch (priv->layout)
|
|
{
|
|
case LAYOUT_ICON_ONLY:
|
|
case LAYOUT_ICON_INDICATOR:
|
|
case LAYOUT_ICON_TITLE:
|
|
priv->title_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
3 * EM2PIXELS (defaults_get_margin_size (d), d) -
|
|
EM2PIXELS (defaults_get_icon_size (d), d);
|
|
|
|
priv->title_height = _calc_title_height (
|
|
self,
|
|
GET_PRIVATE (self)->title_width);
|
|
|
|
new_bubble_height =
|
|
EM2PIXELS (defaults_get_bubble_min_height (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
break;
|
|
|
|
case LAYOUT_ICON_TITLE_BODY:
|
|
{
|
|
gdouble available_height = 0.0f;
|
|
gdouble bubble_height = 0.0f;
|
|
|
|
priv->title_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
3 * EM2PIXELS (defaults_get_margin_size (d), d) -
|
|
EM2PIXELS (defaults_get_icon_size (d), d);
|
|
|
|
priv->title_height = _calc_title_height (
|
|
self,
|
|
GET_PRIVATE (self)->title_width);
|
|
|
|
priv->body_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
3 * EM2PIXELS (defaults_get_margin_size (d), d) -
|
|
EM2PIXELS (defaults_get_icon_size (d), d);
|
|
|
|
priv->body_height = _calc_body_height (
|
|
self,
|
|
priv->body_width);
|
|
|
|
available_height = PIXELS2EM (defaults_get_desktop_height (d), d) -
|
|
defaults_get_desktop_bottom_gap (d) -
|
|
defaults_get_bubble_min_height (d) -
|
|
2.0f * defaults_get_bubble_vert_gap (d);
|
|
|
|
bubble_height =
|
|
PIXELS2EM (priv->title_height, d) +
|
|
PIXELS2EM (priv->body_height, d) +
|
|
2.0f * defaults_get_margin_size (d);
|
|
|
|
if (bubble_height >= available_height)
|
|
{
|
|
new_bubble_height = EM2PIXELS (available_height, d);
|
|
}
|
|
else
|
|
{
|
|
if (priv->body_height +
|
|
priv->title_height <
|
|
EM2PIXELS (defaults_get_icon_size (d), d))
|
|
{
|
|
new_bubble_height =
|
|
EM2PIXELS (defaults_get_icon_size (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
}
|
|
else
|
|
{
|
|
new_bubble_height =
|
|
priv->body_height +
|
|
priv->title_height +
|
|
2.0f * EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case LAYOUT_TITLE_BODY:
|
|
{
|
|
gdouble available_height = 0.0f;
|
|
gdouble bubble_height = 0.0f;
|
|
|
|
priv->title_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
2 * EM2PIXELS (defaults_get_margin_size (d), d);
|
|
|
|
priv->title_height = _calc_title_height (
|
|
self,
|
|
priv->title_width);
|
|
|
|
priv->body_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
2 * EM2PIXELS (defaults_get_margin_size (d), d);
|
|
|
|
priv->body_height = _calc_body_height (
|
|
self,
|
|
priv->body_width);
|
|
|
|
available_height = PIXELS2EM (defaults_get_desktop_height (d), d) -
|
|
defaults_get_desktop_bottom_gap (d) -
|
|
defaults_get_bubble_min_height (d) -
|
|
2.0f * defaults_get_bubble_vert_gap (d);
|
|
|
|
bubble_height =
|
|
PIXELS2EM (priv->title_height, d) +
|
|
PIXELS2EM (priv->body_height, d) +
|
|
2.0f * defaults_get_margin_size (d);
|
|
|
|
if (bubble_height >= available_height)
|
|
{
|
|
new_bubble_height = EM2PIXELS (available_height, d);
|
|
}
|
|
else
|
|
{
|
|
new_bubble_height =
|
|
priv->body_height +
|
|
priv->title_height +
|
|
2.0f * EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case LAYOUT_TITLE_ONLY:
|
|
{
|
|
priv->title_width =
|
|
EM2PIXELS (defaults_get_bubble_width (d), d) -
|
|
2 * EM2PIXELS (defaults_get_margin_size (d), d);
|
|
|
|
priv->title_height = _calc_title_height (
|
|
self,
|
|
priv->title_width);
|
|
|
|
new_bubble_height = priv->title_height +
|
|
2.0f * EM2PIXELS (defaults_get_margin_size (d), d) +
|
|
2.0f * EM2PIXELS (defaults_get_bubble_shadow_size (d), d);
|
|
}
|
|
break;
|
|
|
|
case LAYOUT_NONE:
|
|
g_warning ("bubble_recalc_size(): WARNING, no layout!!!\n");
|
|
break;
|
|
}
|
|
priv->future_height = new_bubble_height;
|
|
|
|
bubble_get_size (self, &old_bubble_width, &old_bubble_height);
|
|
bubble_set_size (self, new_bubble_width, new_bubble_height);
|
|
|
|
// don't refresh tile/blur-caches for background, title or body if the
|
|
// size or contents have not changed, this improves performance for the
|
|
// update- and replace-cases
|
|
if (old_bubble_width != new_bubble_width ||
|
|
old_bubble_height != new_bubble_height)
|
|
_refresh_background (self);
|
|
|
|
if (priv->title_needs_refresh)
|
|
_refresh_title (self);
|
|
|
|
if (priv->message_body_needs_refresh)
|
|
_refresh_body (self);
|
|
|
|
update_shape (self);
|
|
}
|
|
|
|
void
|
|
bubble_set_synchronous (Bubble *self,
|
|
const gchar *sync)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
if (priv->synchronous != NULL)
|
|
g_free (priv->synchronous);
|
|
|
|
priv->synchronous = g_strdup (sync);
|
|
}
|
|
|
|
void
|
|
bubble_set_sender (Bubble *self,
|
|
const gchar *sender)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
if (priv->sender != NULL)
|
|
g_free (priv->sender);
|
|
|
|
priv->sender = g_strdup (sender);
|
|
}
|
|
|
|
gboolean
|
|
bubble_is_synchronous (Bubble *self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return FALSE;
|
|
|
|
return (GET_PRIVATE (self)->synchronous != NULL);
|
|
}
|
|
|
|
gboolean
|
|
bubble_is_urgent (Bubble *self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), FALSE);
|
|
|
|
return (GET_PRIVATE (self)->urgency == 2);
|
|
}
|
|
|
|
guint
|
|
bubble_get_urgency (Bubble *self)
|
|
{
|
|
g_return_val_if_fail (IS_BUBBLE (self), 0);
|
|
|
|
return GET_PRIVATE (self)->urgency;
|
|
}
|
|
|
|
void
|
|
bubble_set_urgency (Bubble *self,
|
|
guint urgency)
|
|
{
|
|
g_return_if_fail (IS_BUBBLE (self));
|
|
|
|
GET_PRIVATE (self)->urgency = urgency;
|
|
}
|
|
|
|
void
|
|
bubble_determine_layout (Bubble* self)
|
|
{
|
|
BubblePrivate* priv;
|
|
|
|
/* sanity test */
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
/* set a sane default */
|
|
priv->layout = LAYOUT_NONE;
|
|
|
|
/* icon-only layout-case, e.g. eject */
|
|
if (priv->icon_only && priv->icon_pixbuf != NULL)
|
|
{
|
|
priv->layout = LAYOUT_ICON_ONLY;
|
|
return;
|
|
}
|
|
|
|
/* icon + indicator layout-case, e.g. volume */
|
|
if ((priv->icon_pixbuf != NULL) &&
|
|
(priv->title->len != 0) &&
|
|
(priv->message_body->len == 0) &&
|
|
(priv->value >= -1))
|
|
{
|
|
priv->layout = LAYOUT_ICON_INDICATOR;
|
|
return;
|
|
}
|
|
|
|
/* icon + title layout-case, e.g. "Wifi signal lost" */
|
|
if ((priv->icon_pixbuf != NULL) &&
|
|
(priv->title->len != 0) &&
|
|
(priv->message_body->len == 0) &&
|
|
(priv->value == -2))
|
|
{
|
|
priv->layout = LAYOUT_ICON_TITLE;
|
|
return;
|
|
}
|
|
|
|
/* icon/avatar + title + body/message layout-case, e.g. IM-message */
|
|
if ((priv->icon_pixbuf != NULL) &&
|
|
(priv->title->len != 0) &&
|
|
(priv->message_body->len != 0) &&
|
|
(priv->value == -2))
|
|
{
|
|
priv->layout = LAYOUT_ICON_TITLE_BODY;
|
|
return;
|
|
}
|
|
|
|
/* title + body/message layout-case, e.g. IM-message without avatar */
|
|
if ((priv->icon_pixbuf == NULL) &&
|
|
(priv->title->len != 0) &&
|
|
(priv->message_body->len != 0) &&
|
|
(priv->value == -2))
|
|
{
|
|
priv->layout = LAYOUT_TITLE_BODY;
|
|
return;
|
|
}
|
|
|
|
/* title-only layout-case, use discouraged but needs to be supported */
|
|
if ((priv->icon_pixbuf == NULL) &&
|
|
(priv->title->len != 0) &&
|
|
(priv->message_body->len == 0) &&
|
|
(priv->value == -2))
|
|
{
|
|
priv->layout = LAYOUT_TITLE_ONLY;
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
BubbleLayout
|
|
bubble_get_layout (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return LAYOUT_NONE;
|
|
|
|
return GET_PRIVATE (self)->layout;
|
|
}
|
|
|
|
void
|
|
bubble_set_icon_only (Bubble* self,
|
|
gboolean allowed)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
GET_PRIVATE (self)->icon_only = allowed;
|
|
}
|
|
|
|
void
|
|
bubble_set_append (Bubble* self,
|
|
gboolean allowed)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return;
|
|
|
|
GET_PRIVATE (self)->append = allowed;
|
|
}
|
|
|
|
|
|
gboolean
|
|
bubble_is_append_allowed (Bubble* self)
|
|
{
|
|
if (!self || !IS_BUBBLE (self))
|
|
return FALSE;
|
|
|
|
return GET_PRIVATE (self)->append;
|
|
}
|
|
|
|
void
|
|
bubble_append_message_body (Bubble* self,
|
|
const gchar* append_body)
|
|
{
|
|
gboolean result = FALSE;
|
|
gchar* text = NULL;
|
|
GError* error = NULL;
|
|
BubblePrivate* priv = NULL;
|
|
|
|
if (!self || !IS_BUBBLE (self) || !append_body)
|
|
return;
|
|
|
|
priv = GET_PRIVATE (self);
|
|
|
|
// filter out any HTML/markup if possible
|
|
result = pango_parse_markup (append_body,
|
|
-1,
|
|
0, // no accel-marker needed
|
|
NULL, // no PangoAttr needed
|
|
&text,
|
|
NULL, // no accel-marker-return needed
|
|
&error);
|
|
if (error && !result)
|
|
{
|
|
g_warning ("%s(): Got error \"%s\"\n",
|
|
G_STRFUNC,
|
|
error->message);
|
|
g_error_free (error);
|
|
error = NULL;
|
|
|
|
if (text)
|
|
g_free (text);
|
|
}
|
|
|
|
if (text)
|
|
{
|
|
// append text to current message-body
|
|
g_string_append (priv->message_body, text);
|
|
priv->message_body_needs_refresh = TRUE;
|
|
|
|
g_signal_emit (self,
|
|
g_bubble_signals[MESSAGE_BODY_INSERTED],
|
|
0,
|
|
text);
|
|
|
|
g_object_notify (
|
|
G_OBJECT (gtk_widget_get_accessible (priv->widget)),
|
|
"accessible-description");
|
|
|
|
g_free (text);
|
|
}
|
|
}
|
|
|
|
void
|
|
bubble_sync_with (Bubble *self,
|
|
Bubble *other)
|
|
{
|
|
g_return_if_fail (IS_BUBBLE (self) && IS_BUBBLE (other));
|
|
|
|
bubble_set_timeout (self,
|
|
bubble_get_timeout (other));
|
|
bubble_start_timer (self, FALSE);
|
|
bubble_start_timer (other, FALSE);
|
|
}
|