1 /*
2     DeaDBeeF -- the music player
3     Copyright (C) 2009-2015 Alexey Yakovenko and other contributors
4 
5     This software is provided 'as-is', without any express or implied
6     warranty.  In no event will the authors be held liable for any damages
7     arising from the use of this software.
8 
9     Permission is granted to anyone to use this software for any purpose,
10     including commercial applications, and to alter it and redistribute it
11     freely, subject to the following restrictions:
12 
13     1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17 
18     2. Altered source versions must be plainly marked as such, and must not be
19      misrepresented as being the original software.
20 
21     3. This notice may not be removed or altered from any source distribution.
22 */
23 
24 #include <glib.h>
25 #include <glib-object.h>
26 #include <gtk/gtk.h>
27 #include <gdk/gdk.h>
28 #include <drawing.h>
29 #include <gtkui.h>
30 #include <math.h>
31 #include "support.h"
32 #include "ddbseekbar.h"
33 
34 #define DDB_TYPE_SEEKBAR (ddb_seekbar_get_type ())
35 #define DDB_SEEKBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), DDB_TYPE_SEEKBAR, DdbSeekbar))
36 #define DDB_SEEKBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), DDB_TYPE_SEEKBAR, DdbSeekbarClass))
37 #define DDB_IS_SEEKBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), DDB_TYPE_SEEKBAR))
38 #define DDB_IS_SEEKBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), DDB_TYPE_SEEKBAR))
39 #define DDB_SEEKBAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), DDB_TYPE_SEEKBAR, DdbSeekbarClass))
40 
41 static gpointer ddb_seekbar_parent_class = NULL;
42 
43 GType ddb_seekbar_get_type (void);
44 enum  {
45 	DDB_SEEKBAR_DUMMY_PROPERTY
46 };
47 #if GTK_CHECK_VERSION(3,0,0)
48 static void ddb_seekbar_get_preferred_width (GtkWidget* base, gint *minimal_width, gint *natural_width);
49 static void ddb_seekbar_get_preferred_height (GtkWidget* base, gint *minimal_height, gint *natural_height);
50 #else
51 static gboolean ddb_seekbar_real_expose_event (GtkWidget* base, GdkEventExpose* event);
52 #endif
53 static void ddb_seekbar_real_size_request (GtkWidget* base, GtkRequisition* requisition);
54 static gboolean ddb_seekbar_real_draw (GtkWidget* base, cairo_t *cr);
55 static gboolean ddb_seekbar_real_button_press_event (GtkWidget* base, GdkEventButton* event);
56 static gboolean ddb_seekbar_real_button_release_event (GtkWidget* base, GdkEventButton* event);
57 static gboolean ddb_seekbar_real_motion_notify_event (GtkWidget* base, GdkEventMotion* event);
58 static gboolean ddb_seekbar_real_configure_event (GtkWidget* base, GdkEventConfigure* event);
59 DdbSeekbar* ddb_seekbar_construct (GType object_type);
60 static GObject * ddb_seekbar_constructor (GType type, guint n_construct_properties, GObjectConstructParam * construct_properties);
61 
62 
63 #if GTK_CHECK_VERSION(3,0,0)
ddb_seekbar_get_preferred_width(GtkWidget * widget,gint * minimal_width,gint * natural_width)64 static void ddb_seekbar_get_preferred_width (GtkWidget* widget, gint *minimal_width, gint *natural_width) {
65     GtkRequisition requisition;
66 
67     ddb_seekbar_real_size_request (widget, &requisition);
68 
69     *minimal_width = *natural_width = requisition.width;
70 }
71 
ddb_seekbar_get_preferred_height(GtkWidget * widget,gint * minimal_height,gint * natural_height)72 static void ddb_seekbar_get_preferred_height (GtkWidget* widget, gint *minimal_height, gint *natural_height) {
73     GtkRequisition requisition;
74 
75     ddb_seekbar_real_size_request (widget, &requisition);
76 
77     *minimal_height = *natural_height = requisition.height;
78 }
79 #endif
80 
ddb_seekbar_real_size_request(GtkWidget * base,GtkRequisition * requisition)81 static void ddb_seekbar_real_size_request (GtkWidget* base, GtkRequisition* requisition) {
82 	DdbSeekbar * self;
83 	GtkRequisition _vala_requisition = {0};
84 	self = (DdbSeekbar*) base;
85 	if (requisition) {
86 		*requisition = _vala_requisition;
87 	}
88 }
89 
ddb_seekbar_real_draw(GtkWidget * base,cairo_t * cr)90 static gboolean ddb_seekbar_real_draw (GtkWidget* base, cairo_t *cr) {
91 	seekbar_draw (base, cr);
92 	return FALSE;
93 }
94 
95 #if !GTK_CHECK_VERSION(3,0,0)
ddb_seekbar_real_expose_event(GtkWidget * base,GdkEventExpose * event)96 static gboolean ddb_seekbar_real_expose_event (GtkWidget* base, GdkEventExpose* event) {
97     cairo_t *cr = gdk_cairo_create (gtk_widget_get_window (base));
98     ddb_seekbar_real_draw (base, cr);
99     cairo_destroy (cr);
100 	return FALSE;
101 }
102 #endif
103 
ddb_seekbar_real_button_press_event(GtkWidget * base,GdkEventButton * event)104 static gboolean ddb_seekbar_real_button_press_event (GtkWidget* base, GdkEventButton* event) {
105 	DdbSeekbar * self;
106 	gboolean result = FALSE;
107 	GdkEventButton _tmp0_;
108 	gboolean _tmp1_ = FALSE;
109 	self = (DdbSeekbar*) base;
110 	g_return_val_if_fail (event != NULL, FALSE);
111 	_tmp0_ = *event;
112 	_tmp1_ = on_seekbar_button_press_event ((GtkWidget*) self, &_tmp0_);
113 	result = _tmp1_;
114 	return result;
115 }
116 
117 
ddb_seekbar_real_button_release_event(GtkWidget * base,GdkEventButton * event)118 static gboolean ddb_seekbar_real_button_release_event (GtkWidget* base, GdkEventButton* event) {
119 	DdbSeekbar * self;
120 	gboolean result = FALSE;
121 	GdkEventButton _tmp0_;
122 	gboolean _tmp1_ = FALSE;
123 	self = (DdbSeekbar*) base;
124 	g_return_val_if_fail (event != NULL, FALSE);
125 	_tmp0_ = *event;
126 	_tmp1_ = on_seekbar_button_release_event ((GtkWidget*) self, &_tmp0_);
127 	result = _tmp1_;
128 	return result;
129 }
130 
131 
ddb_seekbar_real_motion_notify_event(GtkWidget * base,GdkEventMotion * event)132 static gboolean ddb_seekbar_real_motion_notify_event (GtkWidget* base, GdkEventMotion* event) {
133 	DdbSeekbar * self;
134 	gboolean result = FALSE;
135 	GdkEventMotion _tmp0_;
136 	gboolean _tmp1_ = FALSE;
137 	self = (DdbSeekbar*) base;
138 	g_return_val_if_fail (event != NULL, FALSE);
139 	_tmp0_ = *event;
140 	_tmp1_ = on_seekbar_motion_notify_event ((GtkWidget*) self, &_tmp0_);
141 	result = _tmp1_;
142 	return result;
143 }
144 
145 
ddb_seekbar_real_configure_event(GtkWidget * base,GdkEventConfigure * event)146 static gboolean ddb_seekbar_real_configure_event (GtkWidget* base, GdkEventConfigure* event) {
147 	DdbSeekbar * self;
148 	gboolean result = FALSE;
149 	self = (DdbSeekbar*) base;
150 	g_return_val_if_fail (event != NULL, FALSE);
151 	gtkui_init_theme_colors ();
152 	result = FALSE;
153 	return result;
154 }
155 
156 
ddb_seekbar_construct(GType object_type)157 DdbSeekbar* ddb_seekbar_construct (GType object_type) {
158 	DdbSeekbar * self;
159 	self = g_object_newv (object_type, 0, NULL);
160 	return self;
161 }
162 
163 
ddb_seekbar_new(void)164 GtkWidget* ddb_seekbar_new (void) {
165 	return GTK_WIDGET (ddb_seekbar_construct (DDB_TYPE_SEEKBAR));
166 }
167 
168 
ddb_seekbar_constructor(GType type,guint n_construct_properties,GObjectConstructParam * construct_properties)169 static GObject * ddb_seekbar_constructor (GType type, guint n_construct_properties, GObjectConstructParam * construct_properties) {
170 	GObject * obj;
171 	GObjectClass * parent_class;
172 	DdbSeekbar * self;
173 	parent_class = G_OBJECT_CLASS (ddb_seekbar_parent_class);
174 	obj = parent_class->constructor (type, n_construct_properties, construct_properties);
175 	self = DDB_SEEKBAR (obj);
176 	return obj;
177 }
178 
179 
ddb_seekbar_class_init(DdbSeekbarClass * klass)180 static void ddb_seekbar_class_init (DdbSeekbarClass * klass) {
181 	ddb_seekbar_parent_class = g_type_class_peek_parent (klass);
182 #if GTK_CHECK_VERSION(3,0,0)
183 	GTK_WIDGET_CLASS (klass)->get_preferred_width = ddb_seekbar_get_preferred_width;
184 	GTK_WIDGET_CLASS (klass)->get_preferred_height = ddb_seekbar_get_preferred_height;
185 	GTK_WIDGET_CLASS (klass)->draw = ddb_seekbar_real_draw;
186 #else
187 	GTK_WIDGET_CLASS (klass)->size_request = ddb_seekbar_real_size_request;
188 	GTK_WIDGET_CLASS (klass)->expose_event = ddb_seekbar_real_expose_event;
189 #endif
190 	GTK_WIDGET_CLASS (klass)->button_press_event = ddb_seekbar_real_button_press_event;
191 	GTK_WIDGET_CLASS (klass)->button_release_event = ddb_seekbar_real_button_release_event;
192 	GTK_WIDGET_CLASS (klass)->motion_notify_event = ddb_seekbar_real_motion_notify_event;
193 	GTK_WIDGET_CLASS (klass)->configure_event = ddb_seekbar_real_configure_event;
194 	G_OBJECT_CLASS (klass)->constructor = ddb_seekbar_constructor;
195 }
196 
197 
ddb_seekbar_instance_init(DdbSeekbar * self)198 static void ddb_seekbar_instance_init (DdbSeekbar * self) {
199 	gtk_widget_set_has_window ((GtkWidget*) self, FALSE);
200 	gtk_widget_set_has_tooltip ((GtkWidget*) self, TRUE);
201 	self->seekbar_moving = 0;
202     self->seekbar_move_x = 0;
203 }
204 
205 
ddb_seekbar_get_type(void)206 GType ddb_seekbar_get_type (void) {
207 	static volatile gsize ddb_seekbar_type_id__volatile = 0;
208 	if (g_once_init_enter (&ddb_seekbar_type_id__volatile)) {
209 		static const GTypeInfo g_define_type_info = { sizeof (DdbSeekbarClass), (GBaseInitFunc) NULL, (GBaseFinalizeFunc) NULL, (GClassInitFunc) ddb_seekbar_class_init, (GClassFinalizeFunc) NULL, NULL, sizeof (DdbSeekbar), 0, (GInstanceInitFunc) ddb_seekbar_instance_init, NULL };
210 		GType ddb_seekbar_type_id;
211 		ddb_seekbar_type_id = g_type_register_static (GTK_TYPE_WIDGET, "DdbSeekbar", &g_define_type_info, 0);
212 		g_once_init_leave (&ddb_seekbar_type_id__volatile, ddb_seekbar_type_id);
213 	}
214 	return ddb_seekbar_type_id__volatile;
215 }
216 
217 enum
218 {
219 	CORNER_NONE        = 0,
220 	CORNER_TOPLEFT     = 1,
221 	CORNER_TOPRIGHT    = 2,
222 	CORNER_BOTTOMLEFT  = 4,
223 	CORNER_BOTTOMRIGHT = 8,
224 	CORNER_ALL         = 15
225 };
226 
227 static void
clearlooks_rounded_rectangle(cairo_t * cr,double x,double y,double w,double h,double radius,uint8_t corners)228 clearlooks_rounded_rectangle (cairo_t * cr,
229 			      double x, double y, double w, double h,
230 			      double radius, uint8_t corners)
231 {
232     if (radius < 0.01 || (corners == CORNER_NONE)) {
233         cairo_rectangle (cr, x, y, w, h);
234         return;
235     }
236 
237     if (corners & CORNER_TOPLEFT)
238         cairo_move_to (cr, x + radius, y);
239     else
240         cairo_move_to (cr, x, y);
241 
242     if (corners & CORNER_TOPRIGHT)
243         cairo_arc (cr, x + w - radius, y + radius, radius, M_PI * 1.5, M_PI * 2);
244     else
245         cairo_line_to (cr, x + w, y);
246 
247     if (corners & CORNER_BOTTOMRIGHT)
248         cairo_arc (cr, x + w - radius, y + h - radius, radius, 0, M_PI * 0.5);
249     else
250         cairo_line_to (cr, x + w, y + h);
251 
252     if (corners & CORNER_BOTTOMLEFT)
253         cairo_arc (cr, x + radius, y + h - radius, radius, M_PI * 0.5, M_PI);
254     else
255         cairo_line_to (cr, x, y + h);
256 
257     if (corners & CORNER_TOPLEFT)
258         cairo_arc (cr, x + radius, y + radius, radius, M_PI, M_PI * 1.5);
259     else
260         cairo_line_to (cr, x, y);
261 
262 }
263 
264 void
seekbar_draw(GtkWidget * widget,cairo_t * cr)265 seekbar_draw (GtkWidget *widget, cairo_t *cr) {
266     if (!widget) {
267         return;
268     }
269 
270     DdbSeekbar *self = DDB_SEEKBAR (widget);
271 
272 #if GTK_CHECK_VERSION(3,0,0)
273     GtkAllocation allocation;
274     gtk_widget_get_allocation (widget, &allocation);
275     cairo_translate (cr, -allocation.x, -allocation.y);
276 #endif
277 
278     GdkColor clr_selection, clr_back;
279     gtkui_get_bar_foreground_color (&clr_selection);
280     gtkui_get_bar_background_color (&clr_back);
281 
282     GtkAllocation a;
283     gtk_widget_get_allocation (widget, &a);
284 
285     int ax = a.x;
286     int ay = a.y;
287     int aw = a.width;
288     int ah = a.height;
289 
290     DB_playItem_t *trk = deadbeef->streamer_get_playing_track ();
291     if (!trk || deadbeef->pl_get_item_duration (trk) < 0) {
292         if (trk) {
293             deadbeef->pl_item_unref (trk);
294         }
295         // empty seekbar, just a frame
296         clearlooks_rounded_rectangle (cr, 2+ax, a.height/2-4+ay, aw-4, 8, 4, 0xff);
297         cairo_set_source_rgb (cr, clr_selection.red/65535.f, clr_selection.green/65535.f, clr_selection.blue/65535.f );
298         cairo_set_line_width (cr, 2);
299         cairo_stroke (cr);
300         return;
301     }
302     float pos = 0;
303     if (self->seekbar_moving) {
304         int x = self->seekbar_move_x;
305         if (x < 0) {
306             x = 0;
307         }
308         if (x > a.width-1) {
309             x = a.width-1;
310         }
311         pos = x;
312     }
313     else {
314         if (deadbeef->pl_get_item_duration (trk) > 0) {
315             pos = deadbeef->streamer_get_playpos () / deadbeef->pl_get_item_duration (trk);
316             pos *= a.width;
317         }
318     }
319     // left
320     if (pos > 0) {
321         cairo_set_source_rgb (cr, clr_selection.red/65535.f, clr_selection.green/65535.f, clr_selection.blue/65535.f );
322         cairo_rectangle (cr, ax, ah/2-4+ay, pos, 8);
323         cairo_clip (cr);
324         clearlooks_rounded_rectangle (cr, 0+ax, ah/2-4 + ay, aw, 8, 4, 0xff);
325         cairo_fill (cr);
326         cairo_reset_clip (cr);
327     }
328 
329     // right
330     cairo_set_source_rgb (cr, clr_back.red/65535.f, clr_back.green/65535.f, clr_back.blue/65535.f );
331     cairo_rectangle (cr, pos+ax, ah/2-4+ay, aw-pos, 8);
332     cairo_clip (cr);
333     clearlooks_rounded_rectangle (cr, 0+ax, ah/2-4+ay, aw, 8, 4, 0xff);
334     cairo_fill (cr);
335     cairo_reset_clip (cr);
336 
337     if (!gtkui_disable_seekbar_overlay && (self->seekbar_moving || self->seekbar_moved > 0.0) && trk) {
338         float time = 0;
339         float dur = deadbeef->pl_get_item_duration (trk);
340 
341         if (self->seekbar_moved > 0) {
342             time = deadbeef->streamer_get_playpos ();
343         }
344         else {
345             time = self->seekbar_move_x * dur / (a.width);
346         }
347 
348         if (time < 0) {
349             time = 0;
350         }
351         if (time > dur) {
352             time = dur;
353         }
354         char s[1000];
355         int hr = time/3600;
356         int mn = (time-hr*3600)/60;
357         int sc = time-hr*3600-mn*60;
358         snprintf (s, sizeof (s), "%02d:%02d:%02d", hr, mn, sc);
359 
360         cairo_set_source_rgba (cr, clr_selection.red/65535.f, clr_selection.green/65535.f, clr_selection.blue/65535.f, self->seektime_alpha);
361         cairo_save (cr);
362         cairo_set_font_size (cr, 20);
363 
364         cairo_text_extents_t ex;
365         cairo_text_extents (cr, s, &ex);
366         if (self->textpos == -1) {
367             self->textpos = ax + aw/2 - ex.width/2;
368             self->textwidth = ex.width + 20;
369         }
370 
371         clearlooks_rounded_rectangle (cr, ax + aw/2 - self->textwidth/2, ay+4, self->textwidth, ah-8, 3, 0xff);
372         cairo_fill (cr);
373 
374         cairo_move_to (cr, self->textpos, ay+ah/2+ex.height/2);
375         GdkColor clr;
376         gtkui_get_listview_selected_text_color (&clr);
377         cairo_set_source_rgba (cr, clr.red/65535.f, clr.green/65535.f, clr.blue/65535.f, self->seektime_alpha);
378         cairo_show_text (cr, s);
379         cairo_restore (cr);
380 
381         int fps = deadbeef->conf_get_int ("gtkui.refresh_rate", 10);
382         if (fps < 1) {
383             fps = 1;
384         }
385         else if (fps > 30) {
386             fps = 30;
387         }
388         if (self->seekbar_moved >= 0.0) {
389             self->seekbar_moved -= 1.0/fps;
390         }
391         else {
392             self->seekbar_moved = 0.0;
393         }
394     }
395 
396     if (trk) {
397         deadbeef->pl_item_unref (trk);
398     }
399 }
400 
401 gboolean
on_seekbar_motion_notify_event(GtkWidget * widget,GdkEventMotion * event)402 on_seekbar_motion_notify_event         (GtkWidget       *widget,
403                                         GdkEventMotion  *event)
404 {
405     DdbSeekbar *self = DDB_SEEKBAR (widget);
406     if (self->seekbar_moving) {
407         GtkAllocation a;
408         gtk_widget_get_allocation (widget, &a);
409         self->seekbar_move_x = event->x - a.x;
410         gtk_widget_queue_draw (widget);
411     }
412     return FALSE;
413 }
414 
415 gboolean
on_seekbar_button_press_event(GtkWidget * widget,GdkEventButton * event)416 on_seekbar_button_press_event          (GtkWidget       *widget,
417                                         GdkEventButton  *event)
418 {
419     DdbSeekbar *self = DDB_SEEKBAR (widget);
420     if (deadbeef->get_output ()->state () == OUTPUT_STATE_STOPPED) {
421         return FALSE;
422     }
423     self->seekbar_moving = 1;
424     self->seekbar_moved = 0;
425     self->textpos = -1;
426     self->textwidth = -1;
427     self->seektime_alpha = 0.8;
428     GtkAllocation a;
429     gtk_widget_get_allocation (widget, &a);
430     self->seekbar_move_x = event->x - a.x;
431     gtk_widget_queue_draw (widget);
432     return FALSE;
433 }
434 
435 
436 gboolean
on_seekbar_button_release_event(GtkWidget * widget,GdkEventButton * event)437 on_seekbar_button_release_event        (GtkWidget       *widget,
438                                         GdkEventButton  *event)
439 {
440     DdbSeekbar *self = DDB_SEEKBAR (widget);
441     self->seekbar_moving = 0;
442     self->seekbar_moved = 1.0;
443     DB_playItem_t *trk = deadbeef->streamer_get_playing_track ();
444     if (trk) {
445         if (deadbeef->pl_get_item_duration (trk) >= 0) {
446             GtkAllocation a;
447             gtk_widget_get_allocation (widget, &a);
448             float time = (event->x - a.x) * deadbeef->pl_get_item_duration (trk) / (a.width);
449             if (time < 0) {
450                 time = 0;
451             }
452             deadbeef->sendmessage (DB_EV_SEEK, 0, time * 1000, 0);
453         }
454         deadbeef->pl_item_unref (trk);
455     }
456     gtk_widget_queue_draw (widget);
457     return FALSE;
458 }
459 
460 static gboolean
on_evbox_button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)461 on_evbox_button_press_event          (GtkWidget       *widget,
462                                         GdkEventButton  *event,
463                                         gpointer         user_data)
464 {
465     return gtk_widget_event (GTK_WIDGET (user_data), (GdkEvent *)event);
466 }
467 
468 static gboolean
on_evbox_button_release_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)469 on_evbox_button_release_event        (GtkWidget       *widget,
470                                         GdkEventButton  *event,
471                                         gpointer         user_data)
472 {
473     return gtk_widget_event (GTK_WIDGET (user_data), (GdkEvent *)event);
474 }
475 
476 static gboolean
on_evbox_motion_notify_event(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)477 on_evbox_motion_notify_event         (GtkWidget       *widget,
478                                         GdkEventMotion  *event,
479                                         gpointer         user_data)
480 {
481     return gtk_widget_event (GTK_WIDGET (user_data), (GdkEvent *)event);
482 }
483 
484 static gboolean
on_evbox_scroll_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)485 on_evbox_scroll_event                (GtkWidget       *widget,
486                                         GdkEvent        *event,
487                                         gpointer         user_data) {
488     return gtk_widget_event (GTK_WIDGET (user_data), (GdkEvent *)event);
489 }
490 
491 void
ddb_seekbar_init_signals(DdbSeekbar * sb,GtkWidget * evbox)492 ddb_seekbar_init_signals (DdbSeekbar *sb, GtkWidget *evbox) {
493   g_signal_connect ((gpointer) evbox, "button_press_event",
494                     G_CALLBACK (on_evbox_button_press_event),
495                     sb);
496   g_signal_connect ((gpointer) evbox, "button_release_event",
497                     G_CALLBACK (on_evbox_button_release_event),
498                     sb);
499   g_signal_connect ((gpointer) evbox, "scroll_event",
500                     G_CALLBACK (on_evbox_scroll_event),
501                     sb);
502   g_signal_connect ((gpointer) evbox, "motion_notify_event",
503                     G_CALLBACK (on_evbox_motion_notify_event),
504                     sb);
505 }
506 
507