2
Copyright (C) 2012 Lucas Baudin <xapantu@gmail.com>
3
Copyright (C) 2012-2014 Victor Martinez <victoreduardm@gmail.com>
5
This program or library is free software; you can redistribute it
6
and/or modify it under the terms of the GNU Lesser General Public
7
License as published by the Free Software Foundation; either
8
version 3 of the License, or (at your option) any later version.
10
This library is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
Lesser General Public License for more details.
15
You should have received a copy of the GNU Lesser General
16
Public License along with this library; if not, write to the
17
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18
Boston, MA 02110-1301 USA.
22
* A star rating widget.
26
public class PhotoRatingWidget : Gtk.EventBox {
28
* Emitted when a new rating is selected.
30
* @param new_rating The new rating.
33
public signal void rating_changed (int new_rating);
36
* Whether to use symbolic star icons.
40
public bool symbolic {
42
return renderer.symbolic;
44
renderer.symbolic = value;
49
* Pixel size of star icons.
53
public int icon_size {
55
return renderer.icon_size;
57
renderer.icon_size = value;
62
* Total number of stars. It also represents the maximum rating possible.
63
* That is, possible ratings are between 0 and //n_stars//.
65
* Allowed values: >= 0. Default: 5.
70
return renderer.n_stars;
72
renderer.n_stars = value;
77
* Spacing inserted between star icons.
81
public int star_spacing {
83
return renderer.star_spacing;
85
renderer.star_spacing = value;
90
private int _rating = 0;
93
* Current selected rating.
101
_rating = value.clamp (0, n_stars);
102
update_rating (_rating);
106
internal double rating_offset {
108
return renderer.rating_offset;
110
renderer.rating_offset = value;
114
internal int item_width {
116
return renderer.item_width;
121
* Whether the widget will be centered with respect to its allocation.
125
public bool centered {
131
private PhotoRatingRenderer renderer;
132
private int hover_rating = 0;
135
* Creates a new Rating widget.
137
* @param centered Whether the widget should be centered with respect to its allocation.
138
* @param size Pixel size of star icons.
139
* @param symbolic Whether to use symbolic icons.
142
public PhotoRatingWidget (bool centered, int size, bool symbolic = false) {
143
this.centered = centered;
144
this.renderer = new PhotoRatingRenderer (size, symbolic, get_style_context ());
145
visible_window = false;
147
add_events (Gdk.EventMask.BUTTON_PRESS_MASK
148
| Gdk.EventMask.BUTTON_RELEASE_MASK
149
| Gdk.EventMask.POINTER_MOTION_MASK
150
| Gdk.EventMask.LEAVE_NOTIFY_MASK);
152
state_flags_changed.connect_after (() => {
156
renderer.render.connect_after (() => {
162
private void compute_size () {
163
this.set_size_request (renderer.width, renderer.height);
166
public override bool motion_notify_event (Gdk.EventMotion event) {
171
get_allocation (out al);
172
x_offset = (al.width - width_request) / 2;
175
hover_rating = renderer.get_new_rating (event.x - x_offset);
176
update_rating (hover_rating);
180
public override bool button_press_event (Gdk.EventButton event) {
181
rating = hover_rating;
182
rating_changed (rating);
186
public override bool leave_notify_event (Gdk.EventCrossing ev) {
187
update_rating (rating);
191
internal void update_rating (int fake_rating) {
192
renderer.rating = fake_rating;
196
public override bool draw (Cairo.Context context) {
198
get_allocation (out al);
200
Gdk.cairo_set_source_pixbuf (context,
202
centered ? (al.width - width_request) / 2 : 0,
203
centered ? (al.height - height_request) / 2 : 0);
213
* A menu item that contains a rating widget.
217
public class PhotoRatingMenuItem : Gtk.MenuItem {
219
* Current displayed rating. Note that you should read this value
220
* after the {@link Gtk.MenuItem.activate} signal is emitted.
224
public int rating_value {
226
return rating.rating;
228
rating.rating = value;
232
private PhotoRatingWidget rating;
235
* Creates a new rating menu item.
239
public PhotoRatingMenuItem (int icon_size = 16) {
240
rating = new PhotoRatingWidget (false, icon_size, false);
243
// Workaround. Move the offset one star to the left for menuitems.
244
rating.rating_offset = - (double) rating.item_width - (double) rating.star_spacing;
246
this.state_flags_changed.connect (() => {
247
// Suppress SELECTED and PRELIGHT states, since these are usually obtrusive
248
var selected_flags = Gtk.StateFlags.SELECTED | Gtk.StateFlags.PRELIGHT;
249
if ((get_state_flags () & selected_flags) != 0)
250
unset_state_flags (selected_flags);
254
public override bool motion_notify_event (Gdk.EventMotion ev) {
255
rating.motion_notify_event (ev);
256
rating.queue_draw ();
260
public override bool button_press_event (Gdk.EventButton ev) {
261
rating.button_press_event (ev);
266
public override bool leave_notify_event (Gdk.EventCrossing ev) {
267
rating.update_rating (rating_value);
273
* This class makes setting the rating from a cell possible. Unlike the other widgets,
274
* it only allows doing so by clicking over a star.
276
* When the rating changes by activating (i.e. clicking) the cell renderer, it doesn't re-draw itself
277
* automatically and/or apply the new rating right away, since there could be client code wanting
278
* to check the new value. Instead, it passes the responsability off to the rating_changed signal handler.
279
* That signal handler ''must'' take care of setting the new rating on the proper cell, and only then
280
* the new value will take effect.
284
public class PhotoCellRendererRating : Gtk.CellRendererPixbuf {
286
* New rating was set. It is only emmited when the rating changes by activating the renderer.
288
* @param new_rating new selected rating.
289
* @param widget Widget that captured the event (e.g. a {@link Gtk.TreeView}).
290
* @param path string representation of the tree path for the cell where the event occurred.
293
public signal void rating_changed (int new_rating, Gtk.Widget widget, string path);
295
private PhotoRatingRenderer renderer;
298
* Creates a new rating cell renderer.
300
* @param icon_size Pixel size of the star icons
303
public PhotoCellRendererRating (int icon_size = 16) {
305
this.mode = Gtk.CellRendererMode.ACTIVATABLE;
307
renderer = new PhotoRatingRenderer (icon_size, true, null);
309
// We'll only redraw from render() for performance reasons
310
renderer.delayed_render_mode = true;
312
// Set rating to 1 star and render to init the 'width' property
319
* Spacing inserted between star icons.
323
public int star_spacing {
325
return renderer.star_spacing;
327
renderer.star_spacing = value;
331
private uint _rating = 0;
335
* Current displayed rating.
344
renderer.rating = _rating;
349
* Total number of stars. It also represents the maximum rating possible.
350
* That is, possible ratings are between 0 and //n_stars//.
352
* Allowed values: >= 0. Default: 5.
357
return renderer.n_stars;
359
renderer.n_stars = value;
363
private void update_pixbuf () {
364
this.pixbuf = renderer.canvas;
365
this.set_fixed_size (this.pixbuf.width, this.pixbuf.height);
368
public override void render (Cairo.Context ctx, Gtk.Widget widget,
369
Gdk.Rectangle background_area, Gdk.Rectangle cell_area,
370
Gtk.CellRendererState flags) {
371
var style_context = widget.get_style_context ();
372
var state = style_context.get_state ();
373
int old_n_stars = n_stars;
375
// Only draw stars of 0-rating if the cursor is over the cell
376
if (_rating == 0 && (state & (Gtk.StateFlags.SELECTED | Gtk.StateFlags.PRELIGHT)) == 0)
379
// Only show the filled stars if the row is neither selected nor mouseovered
380
if (0 < _rating && (state & (Gtk.StateFlags.SELECTED | Gtk.StateFlags.PRELIGHT)) == 0)
381
n_stars = (int) rating;
383
renderer.style_context = style_context;
386
base.render (ctx, widget, background_area, cell_area, flags);
387
n_stars = old_n_stars;
390
public override bool activate (Gdk.Event event, Gtk.Widget widget, string path,
391
Gdk.Rectangle background_area, Gdk.Rectangle cell_area,
392
Gtk.CellRendererState flags) {
393
int old_rating = (int) rating;
394
int new_rating = renderer.get_new_rating (event.button.x - cell_area.x);
396
// If the user clicks again over the same star, decrease the rating (i.e. "unset" the star)
397
if (new_rating == old_rating && new_rating > 0)
401
rating_changed (new_rating, widget, path);
407
public class PhotoRatingRenderer : Object {
409
* Whether to delay the rendering of the rating until the next call
410
* to render() after a property change. This is recommended in cases
411
* where there's an extensive amount of drawing and the renderer's
412
* properties are constantly changing; for example, when used by
413
* a Gtk.CellRenderer in a Gtk.TreeView; in such case, it's desirable
414
* to have the renderer re-draw its pixbuf only on the next call to
415
* Gtk.CellRenderer.render.
417
* Default value: false.
420
public bool delayed_render_mode {
427
* The pixbuf containing the stars. Should not be modified.
429
* To listen for changes on this property, connect to the render() signal.
430
* If you need to modify this pixbuf, create a copy first.
434
public Gdk.Pixbuf canvas {
440
* Rating value to render.
452
* Maximum possible rating. This represents the total number of stars
453
* that will be rendered.
465
* The number of pixels inserted between star icons.
470
public int star_spacing {
477
* Total width of rating pixbuf (in pixels).
488
* Total height of rating pixbuf (in pixels).
499
* The width of each rating star (in pixels).
503
public int item_width {
510
* The height of each rating star (in pixels).
514
public int item_height {
521
* Pixel size of rating icons.
525
public int icon_size {
532
* Whether to use symbolic star icons.
536
public bool symbolic {
543
* Whether to append a //plus// icon at the end of the star rating.
545
* Default value: false.
548
public bool add_plus_sign {
557
* Default value: transparent.
560
public Gdk.RGBA background_color {
566
// Convert RGBA from 0-1.0 value range to 0-255 (8-bit representation) by
567
// simply multiplying values. Then we move the bits to their correct positions,
568
// since we'll use a 32 bit integer to represent four 8-bit portions.
569
// Please note the loss of precision.
570
uint r = ((uint) (value.red * 255)) << 24;
571
uint g = ((uint) (value.green * 255)) << 16;
572
uint b = ((uint) (value.blue * 255)) << 8;
573
uint a = ((uint) (value.alpha * 255));
575
// bit positions do not overlap, so this is safe.
576
pixel_color_rgba = r | g | b | a;
581
* Style context to use as reference for drawing.
585
public Gtk.StyleContext? style_context {
587
return current_context;
589
if (value != current_context) {
590
if (current_context != null)
591
current_context.changed.disconnect (on_style_changed);
593
current_context = value;
595
if (current_context != null)
596
current_context.changed.connect (on_style_changed);
603
internal double rating_offset {
609
// Icon cache. It stores the pixbufs rendered for every state until the
610
// style information changes.
611
private Gee.HashMap<int, Gdk.Pixbuf> starred_pixbufs;
612
private Gee.HashMap<int, Gdk.Pixbuf> not_starred_pixbufs;
613
private Gee.HashMap<int, Gdk.Pixbuf> plus_sign_pixbufs;
614
private Gdk.RGBA bg_color;
615
private uint pixel_color_rgba;
616
private Gtk.StyleContext? current_context;
617
// Whether a property has changed or not. Used to avoid unnecessary work in render()
618
private bool property_changed = true;
621
* Creates a rating renderer.
623
* @param icon_size pixel size of star icons
624
* @param symbolic Whether to use symbolic icons
625
* @param context style context to use as reference for rendering, or //null//.
628
public PhotoRatingRenderer (int icon_size, bool symbolic, Gtk.StyleContext? context) {
629
starred_pixbufs = new Gee.HashMap<int, Gdk.Pixbuf> ();
630
not_starred_pixbufs = new Gee.HashMap<int, Gdk.Pixbuf> ();
631
plus_sign_pixbufs = new Gee.HashMap<int, Gdk.Pixbuf> ();
633
this.symbolic = symbolic;
634
this.icon_size = icon_size;
635
this.style_context = context;
636
this.background_color = { 0, 0, 0, 0 };
638
// Initial rendering. This is important; it will connect a handler
639
// to the notify() signal, and will also init some properties, such
640
// as item_width, item_height, width, height, etc.
641
assert (property_changed);
643
assert (!property_changed);
646
public virtual signal void render () {
647
if (!property_changed)
650
disable_property_notify ();
652
Gtk.StateFlags state = Gtk.StateFlags.NORMAL;
654
// Only consider actual state if the stars should be symbolic.
655
// Otherwise we consider the single state (NORMAL) set above.
656
if (symbolic && style_context != null)
657
state = style_context.get_state ();
659
var starred_pix = starred_pixbufs.get (state);
660
var not_starred_pix = not_starred_pixbufs.get (state);
661
var plus_sign_pix = plus_sign_pixbufs.get (state);
663
// if no cached star pixbufs were found, render them.
664
var factory = Granite.Services.IconFactory.get_default ();
666
if (starred_pix == null) {
667
string starred = symbolic ? "starred-symbolic" : "starred";
668
starred_pix = factory.load_symbolic_icon (style_context, starred, icon_size);
669
starred_pixbufs.set (state, starred_pix);
672
if (not_starred_pix == null) {
673
string not_starred = symbolic ? "non-starred-symbolic" : "non-starred";
674
not_starred_pix = factory.load_symbolic_icon (style_context, not_starred, icon_size);
675
not_starred_pixbufs.set (state, not_starred_pix);
678
if (add_plus_sign && plus_sign_pix == null)
679
plus_sign_pix = factory.load_symbolic_icon (style_context, "list-add-symbolic", icon_size);
681
if (starred_pix != null && not_starred_pix != null) {
683
item_width = int.max (starred_pix.width, not_starred_pix.width);
684
item_height = int.max (starred_pix.height, not_starred_pix.height);
686
int new_width = (item_width + star_spacing) * n_stars - star_spacing;
687
int new_height = item_height;
690
new_width += star_spacing + plus_sign_pix.width;
691
new_height = int.max (new_height, plus_sign_pix.height);
694
// Generate canvas pixbuf
695
if (canvas == null || new_width != width || new_height != height) {
698
canvas = new Gdk.Pixbuf (Gdk.Colorspace.RGB, true, 8, width, height);
701
if (canvas != null) {
702
var star_canvas = canvas.copy ();
703
star_canvas.fill ((uint) 0xffffff00);
706
for (int i = 0; i <= n_stars; i++) {
707
Gdk.Pixbuf to_copy = null;
712
to_copy = plus_sign_pix;
713
} else if (i < rating) {
714
to_copy = starred_pix;
716
to_copy = not_starred_pix;
719
assert (to_copy != null);
721
int dest_x = i * (item_width + (i > 0 ? star_spacing : 0)), dest_y = 0;
722
to_copy.copy_area (0, 0, item_width, item_height, star_canvas, dest_x, dest_y);
725
canvas.fill (pixel_color_rgba);
726
star_canvas.composite (canvas, 0, 0, canvas.width, canvas.height,
727
0, 0, 1, 1, Gdk.InterpType.BILINEAR, 255);
729
warning ("NULL rating canvas");
733
// No more work to do until the next property change
734
property_changed = false;
736
enable_property_notify ();
739
private inline void disable_property_notify () {
740
notify.disconnect (on_property_changed);
743
private inline void enable_property_notify () {
744
notify.connect (on_property_changed);
748
* Returns a new rating value between 0 and n_stars, based on the cursor position
749
* relative to the left side of the widget (x = 0).
755
* | x_offset | | spacing | | spacing | | spacing | | spacing ... | remaining space...
756
* <-------------> X --------- X --------- X --------- X --------- ... X ------------->
757
* ... 0 stars | 1 star | 2 stars | 3 stars | 4 stars ...| n_stars stars...
759
* The first row in the graphic above represents the values involved:
760
* - x_offset : the value added in front of the first star.
761
* - spacing : space inserted between stars (star_spacing).
762
* - n_stars : total number of stars. It also represents the maximum rating.
764
* As you can see, you can modify the placement of the invisible value separators ("|")
765
* by changing the value of x_offset. For instance, if you wanted the next star to be activated
766
* when the cursor is at least halfway towards it, just modify x_offset. It should be similar
767
* for other cases as well. 'rating_offset' uses exactly that mechanism to apply its value.
769
internal int get_new_rating (double x) {
772
x_offset -= (int) rating_offset;
774
int cursor_x_pos = (int) x;
777
for (int i = 0; i < n_stars; i++) {
778
if (cursor_x_pos > x_offset + i * (item_width + star_spacing))
785
private void on_style_changed () {
786
// Invalidate old cached pixbufs
787
starred_pixbufs.clear ();
788
not_starred_pixbufs.clear ();
789
plus_sign_pixbufs.clear ();
792
private void on_property_changed () {
793
property_changed = true;
795
if (!delayed_render_mode)