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