1 | /* |
2 | * Copyright © 2018 Benjamin Otte |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkmediacontrols.h" |
23 | |
24 | #include "gtkadjustment.h" |
25 | #include "gtkbutton.h" |
26 | #include "gtkintl.h" |
27 | #include "gtklabel.h" |
28 | #include "gtkwidgetprivate.h" |
29 | |
30 | /** |
31 | * GtkMediaControls: |
32 | * |
33 | * `GtkMediaControls` is a widget to show controls for a video. |
34 | * |
35 | * ![An example GtkMediaControls](media-controls.png) |
36 | * |
37 | * Usually, `GtkMediaControls` is used as part of [class@Gtk.Video]. |
38 | */ |
39 | |
40 | struct _GtkMediaControls |
41 | { |
42 | GtkWidget parent_instance; |
43 | |
44 | GtkMediaStream *stream; |
45 | |
46 | GtkAdjustment *time_adjustment; |
47 | GtkAdjustment *volume_adjustment; |
48 | GtkWidget *box; |
49 | GtkWidget *play_button; |
50 | GtkWidget *time_box; |
51 | GtkWidget *time_label; |
52 | GtkWidget *seek_scale; |
53 | GtkWidget *duration_label; |
54 | GtkWidget *volume_button; |
55 | }; |
56 | |
57 | enum |
58 | { |
59 | PROP_0, |
60 | PROP_MEDIA_STREAM, |
61 | |
62 | N_PROPS |
63 | }; |
64 | |
65 | G_DEFINE_TYPE (GtkMediaControls, gtk_media_controls, GTK_TYPE_WIDGET) |
66 | |
67 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
68 | |
69 | /* FIXME: Remove |
70 | * See https://bugzilla.gnome.org/show_bug.cgi?id=679850 */ |
71 | static char * |
72 | totem_time_to_string (gint64 usecs, |
73 | gboolean remaining, |
74 | gboolean force_hour) |
75 | { |
76 | int sec, min, hour, _time; |
77 | |
78 | _time = (int) (usecs / G_USEC_PER_SEC); |
79 | /* When calculating the remaining time, |
80 | * we want to make sure that: |
81 | * current time + time remaining = total run time */ |
82 | if (remaining) |
83 | _time++; |
84 | |
85 | sec = _time % 60; |
86 | _time = _time - sec; |
87 | min = (_time % (60*60)) / 60; |
88 | _time = _time - (min * 60); |
89 | hour = _time / (60*60); |
90 | |
91 | if (hour > 0 || force_hour) { |
92 | if (!remaining) { |
93 | /* hour:minutes:seconds */ |
94 | /* Translators: This is a time format, like "9:05:02" for 9 |
95 | * hours, 5 minutes, and 2 seconds. You may change ":" to |
96 | * the separator that your locale uses or use "%Id" instead |
97 | * of "%d" if your locale uses localized digits. |
98 | */ |
99 | return g_strdup_printf (C_("long time format" , "%d:%02d:%02d" ), hour, min, sec); |
100 | } else { |
101 | /* -hour:minutes:seconds */ |
102 | /* Translators: This is a time format, like "-9:05:02" for 9 |
103 | * hours, 5 minutes, and 2 seconds playback remaining. You may |
104 | * change ":" to the separator that your locale uses or use |
105 | * "%Id" instead of "%d" if your locale uses localized digits. |
106 | */ |
107 | return g_strdup_printf (C_("long time format" , "-%d:%02d:%02d" ), hour, min, sec); |
108 | } |
109 | } |
110 | |
111 | if (remaining) { |
112 | /* -minutes:seconds */ |
113 | /* Translators: This is a time format, like "-5:02" for 5 |
114 | * minutes and 2 seconds playback remaining. You may change |
115 | * ":" to the separator that your locale uses or use "%Id" |
116 | * instead of "%d" if your locale uses localized digits. |
117 | */ |
118 | return g_strdup_printf (C_("short time format" , "-%d:%02d" ), min, sec); |
119 | } |
120 | |
121 | /* minutes:seconds */ |
122 | /* Translators: This is a time format, like "5:02" for 5 |
123 | * minutes and 2 seconds. You may change ":" to the |
124 | * separator that your locale uses or use "%Id" instead of |
125 | * "%d" if your locale uses localized digits. |
126 | */ |
127 | return g_strdup_printf (C_("short time format" , "%d:%02d" ), min, sec); |
128 | } |
129 | |
130 | static void |
131 | time_adjustment_changed (GtkAdjustment *adjustment, |
132 | GtkMediaControls *controls) |
133 | { |
134 | if (controls->stream == NULL) |
135 | return; |
136 | |
137 | /* We just updated the adjustment and it's correct now */ |
138 | if (gtk_adjustment_get_value (adjustment) == (double) gtk_media_stream_get_timestamp (self: controls->stream) / G_USEC_PER_SEC) |
139 | return; |
140 | |
141 | gtk_media_stream_seek (self: controls->stream, |
142 | timestamp: gtk_adjustment_get_value (adjustment) * G_USEC_PER_SEC + 0.5); |
143 | } |
144 | |
145 | static void |
146 | volume_adjustment_changed (GtkAdjustment *adjustment, |
147 | GtkMediaControls *controls) |
148 | { |
149 | if (controls->stream == NULL) |
150 | return; |
151 | |
152 | /* We just updated the adjustment and it's correct now */ |
153 | if (gtk_adjustment_get_value (adjustment) == gtk_media_stream_get_volume (self: controls->stream)) |
154 | return; |
155 | |
156 | gtk_media_stream_set_muted (self: controls->stream, muted: gtk_adjustment_get_value (adjustment) == 0.0); |
157 | gtk_media_stream_set_volume (self: controls->stream, volume: gtk_adjustment_get_value (adjustment)); |
158 | } |
159 | |
160 | static void |
161 | play_button_clicked (GtkWidget *button, |
162 | GtkMediaControls *controls) |
163 | { |
164 | if (controls->stream == NULL) |
165 | return; |
166 | |
167 | gtk_media_stream_set_playing (self: controls->stream, |
168 | playing: !gtk_media_stream_get_playing (self: controls->stream)); |
169 | } |
170 | |
171 | static void |
172 | gtk_media_controls_measure (GtkWidget *widget, |
173 | GtkOrientation orientation, |
174 | int for_size, |
175 | int *minimum, |
176 | int *natural, |
177 | int *minimum_baseline, |
178 | int *natural_baseline) |
179 | { |
180 | GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: widget); |
181 | |
182 | gtk_widget_measure (widget: controls->box, |
183 | orientation, |
184 | for_size, |
185 | minimum, natural, |
186 | minimum_baseline, natural_baseline); |
187 | } |
188 | |
189 | static void |
190 | gtk_media_controls_size_allocate (GtkWidget *widget, |
191 | int width, |
192 | int height, |
193 | int baseline) |
194 | { |
195 | GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: widget); |
196 | |
197 | gtk_widget_size_allocate (widget: controls->box, |
198 | allocation: &(GtkAllocation) { |
199 | 0, 0, |
200 | width, height |
201 | }, baseline); |
202 | } |
203 | |
204 | static void |
205 | gtk_media_controls_dispose (GObject *object) |
206 | { |
207 | GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: object); |
208 | |
209 | gtk_media_controls_set_media_stream (controls, NULL); |
210 | |
211 | g_clear_pointer (&controls->box, gtk_widget_unparent); |
212 | |
213 | G_OBJECT_CLASS (gtk_media_controls_parent_class)->dispose (object); |
214 | } |
215 | |
216 | static void |
217 | gtk_media_controls_get_property (GObject *object, |
218 | guint property_id, |
219 | GValue *value, |
220 | GParamSpec *pspec) |
221 | { |
222 | GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: object); |
223 | |
224 | switch (property_id) |
225 | { |
226 | case PROP_MEDIA_STREAM: |
227 | g_value_set_object (value, v_object: controls->stream); |
228 | break; |
229 | |
230 | default: |
231 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
232 | break; |
233 | } |
234 | } |
235 | |
236 | static void |
237 | gtk_media_controls_set_property (GObject *object, |
238 | guint property_id, |
239 | const GValue *value, |
240 | GParamSpec *pspec) |
241 | { |
242 | GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: object); |
243 | |
244 | switch (property_id) |
245 | { |
246 | case PROP_MEDIA_STREAM: |
247 | gtk_media_controls_set_media_stream (controls, stream: g_value_get_object (value)); |
248 | break; |
249 | |
250 | default: |
251 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
252 | break; |
253 | } |
254 | } |
255 | |
256 | static void |
257 | gtk_media_controls_class_init (GtkMediaControlsClass *klass) |
258 | { |
259 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
260 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
261 | |
262 | widget_class->measure = gtk_media_controls_measure; |
263 | widget_class->size_allocate = gtk_media_controls_size_allocate; |
264 | |
265 | gobject_class->dispose = gtk_media_controls_dispose; |
266 | gobject_class->get_property = gtk_media_controls_get_property; |
267 | gobject_class->set_property = gtk_media_controls_set_property; |
268 | |
269 | /** |
270 | * GtkMediaControls:media-stream: (attributes org.gtk.Property.get=gtk_media_controls_get_media_stream org.gtk.Property.set=gtk_media_controls_set_media_stream) |
271 | * |
272 | * The media-stream managed by this object or %NULL if none. |
273 | */ |
274 | properties[PROP_MEDIA_STREAM] = |
275 | g_param_spec_object (name: "media-stream" , |
276 | P_("Media Stream" ), |
277 | P_("The media stream managed" ), |
278 | GTK_TYPE_MEDIA_STREAM, |
279 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
280 | |
281 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
282 | |
283 | gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/org/gtk/libgtk/ui/gtkmediacontrols.ui" ); |
284 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_adjustment); |
285 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, volume_adjustment); |
286 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, box); |
287 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, play_button); |
288 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_box); |
289 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_label); |
290 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, seek_scale); |
291 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, duration_label); |
292 | gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, volume_button); |
293 | |
294 | gtk_widget_class_bind_template_callback (widget_class, play_button_clicked); |
295 | gtk_widget_class_bind_template_callback (widget_class, time_adjustment_changed); |
296 | gtk_widget_class_bind_template_callback (widget_class, volume_adjustment_changed); |
297 | |
298 | gtk_widget_class_set_css_name (widget_class, I_("controls" )); |
299 | } |
300 | |
301 | static void |
302 | gtk_media_controls_init (GtkMediaControls *controls) |
303 | { |
304 | gtk_widget_init_template (GTK_WIDGET (controls)); |
305 | } |
306 | |
307 | /** |
308 | * gtk_media_controls_new: |
309 | * @stream: (nullable) (transfer none): a `GtkMediaStream` to manage |
310 | * |
311 | * Creates a new `GtkMediaControls` managing the @stream passed to it. |
312 | * |
313 | * Returns: a new `GtkMediaControls` |
314 | */ |
315 | GtkWidget * |
316 | gtk_media_controls_new (GtkMediaStream *stream) |
317 | { |
318 | return g_object_new (GTK_TYPE_MEDIA_CONTROLS, |
319 | first_property_name: "media-stream" , stream, |
320 | NULL); |
321 | } |
322 | |
323 | /** |
324 | * gtk_media_controls_get_media_stream: (attributes org.gtk.Method.get_property=media-stream) |
325 | * @controls: a `GtkMediaControls` |
326 | * |
327 | * Gets the media stream managed by @controls or %NULL if none. |
328 | * |
329 | * Returns: (nullable) (transfer none): The media stream managed by @controls |
330 | */ |
331 | GtkMediaStream * |
332 | gtk_media_controls_get_media_stream (GtkMediaControls *controls) |
333 | { |
334 | g_return_val_if_fail (GTK_IS_MEDIA_CONTROLS (controls), NULL); |
335 | |
336 | return controls->stream; |
337 | } |
338 | |
339 | static void |
340 | update_timestamp (GtkMediaControls *controls) |
341 | { |
342 | gint64 timestamp, duration; |
343 | char *time_string; |
344 | |
345 | if (controls->stream) |
346 | { |
347 | timestamp = gtk_media_stream_get_timestamp (self: controls->stream); |
348 | duration = gtk_media_stream_get_duration (self: controls->stream); |
349 | } |
350 | else |
351 | { |
352 | timestamp = 0; |
353 | duration = 0; |
354 | } |
355 | |
356 | time_string = totem_time_to_string (usecs: timestamp, FALSE, FALSE); |
357 | gtk_label_set_text (GTK_LABEL (controls->time_label), str: time_string); |
358 | g_free (mem: time_string); |
359 | |
360 | if (duration > 0) |
361 | { |
362 | time_string = totem_time_to_string (usecs: duration > timestamp ? duration - timestamp : 0, TRUE, FALSE); |
363 | gtk_label_set_text (GTK_LABEL (controls->duration_label), str: time_string); |
364 | g_free (mem: time_string); |
365 | |
366 | gtk_adjustment_set_value (adjustment: controls->time_adjustment, value: (double) timestamp / G_USEC_PER_SEC); |
367 | } |
368 | } |
369 | |
370 | static void |
371 | update_duration (GtkMediaControls *controls) |
372 | { |
373 | gint64 timestamp, duration; |
374 | char *time_string; |
375 | |
376 | if (controls->stream) |
377 | { |
378 | timestamp = gtk_media_stream_get_timestamp (self: controls->stream); |
379 | duration = gtk_media_stream_get_duration (self: controls->stream); |
380 | } |
381 | else |
382 | { |
383 | timestamp = 0; |
384 | duration = 0; |
385 | } |
386 | |
387 | time_string = totem_time_to_string (usecs: duration > timestamp ? duration - timestamp : 0, TRUE, FALSE); |
388 | gtk_label_set_text (GTK_LABEL (controls->duration_label), str: time_string); |
389 | gtk_widget_set_visible (widget: controls->duration_label, visible: duration > 0); |
390 | g_free (mem: time_string); |
391 | |
392 | gtk_adjustment_set_upper (adjustment: controls->time_adjustment, |
393 | upper: gtk_adjustment_get_page_size (adjustment: controls->time_adjustment) |
394 | + (double) duration / G_USEC_PER_SEC); |
395 | gtk_adjustment_set_value (adjustment: controls->time_adjustment, value: (double) timestamp / G_USEC_PER_SEC); |
396 | } |
397 | |
398 | static void |
399 | update_playing (GtkMediaControls *controls) |
400 | { |
401 | gboolean playing; |
402 | const char *icon_name; |
403 | |
404 | if (controls->stream) |
405 | playing = gtk_media_stream_get_playing (self: controls->stream); |
406 | else |
407 | playing = FALSE; |
408 | |
409 | if (playing) |
410 | icon_name = "media-playback-pause-symbolic" ; |
411 | else |
412 | icon_name = "media-playback-start-symbolic" ; |
413 | |
414 | gtk_button_set_icon_name (GTK_BUTTON (controls->play_button), icon_name); |
415 | } |
416 | |
417 | static void |
418 | update_seekable (GtkMediaControls *controls) |
419 | { |
420 | gboolean seekable; |
421 | |
422 | if (controls->stream) |
423 | seekable = gtk_media_stream_is_seekable (self: controls->stream); |
424 | else |
425 | seekable = FALSE; |
426 | |
427 | gtk_widget_set_sensitive (widget: controls->seek_scale, sensitive: seekable); |
428 | } |
429 | |
430 | static void |
431 | update_volume (GtkMediaControls *controls) |
432 | { |
433 | double volume; |
434 | |
435 | if (controls->stream == NULL) |
436 | volume = 1.0; |
437 | else if (gtk_media_stream_get_muted (self: controls->stream)) |
438 | volume = 0.0; |
439 | else |
440 | volume = gtk_media_stream_get_volume (self: controls->stream); |
441 | |
442 | gtk_adjustment_set_value (adjustment: controls->volume_adjustment, value: volume); |
443 | |
444 | gtk_widget_set_sensitive (widget: controls->volume_button, |
445 | sensitive: controls->stream == NULL || |
446 | gtk_media_stream_has_audio (self: controls->stream)); |
447 | } |
448 | |
449 | static void |
450 | update_all (GtkMediaControls *controls) |
451 | { |
452 | update_timestamp (controls); |
453 | update_duration (controls); |
454 | update_playing (controls); |
455 | update_seekable (controls); |
456 | update_volume (controls); |
457 | } |
458 | |
459 | static void |
460 | gtk_media_controls_notify_cb (GtkMediaStream *stream, |
461 | GParamSpec *pspec, |
462 | GtkMediaControls *controls) |
463 | { |
464 | if (g_str_equal (v1: pspec->name, v2: "timestamp" )) |
465 | update_timestamp (controls); |
466 | else if (g_str_equal (v1: pspec->name, v2: "duration" )) |
467 | update_duration (controls); |
468 | else if (g_str_equal (v1: pspec->name, v2: "playing" )) |
469 | update_playing (controls); |
470 | else if (g_str_equal (v1: pspec->name, v2: "seekable" )) |
471 | update_seekable (controls); |
472 | else if (g_str_equal (v1: pspec->name, v2: "muted" )) |
473 | update_volume (controls); |
474 | else if (g_str_equal (v1: pspec->name, v2: "volume" )) |
475 | update_volume (controls); |
476 | else if (g_str_equal (v1: pspec->name, v2: "has-audio" )) |
477 | update_volume (controls); |
478 | } |
479 | |
480 | /** |
481 | * gtk_media_controls_set_media_stream: (attributes org.gtk.Method.set_property=media-stream) |
482 | * @controls: a `GtkMediaControls` widget |
483 | * @stream: (nullable): a `GtkMediaStream` |
484 | * |
485 | * Sets the stream that is controlled by @controls. |
486 | */ |
487 | void |
488 | gtk_media_controls_set_media_stream (GtkMediaControls *controls, |
489 | GtkMediaStream *stream) |
490 | { |
491 | g_return_if_fail (GTK_IS_MEDIA_CONTROLS (controls)); |
492 | g_return_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream)); |
493 | |
494 | if (controls->stream == stream) |
495 | return; |
496 | |
497 | if (controls->stream) |
498 | { |
499 | g_signal_handlers_disconnect_by_func (controls->stream, |
500 | gtk_media_controls_notify_cb, |
501 | controls); |
502 | g_object_unref (object: controls->stream); |
503 | controls->stream = NULL; |
504 | } |
505 | |
506 | if (stream) |
507 | { |
508 | controls->stream = g_object_ref (stream); |
509 | g_signal_connect (controls->stream, |
510 | "notify" , |
511 | G_CALLBACK (gtk_media_controls_notify_cb), |
512 | controls); |
513 | } |
514 | |
515 | update_all (controls); |
516 | gtk_widget_set_sensitive (widget: controls->box, sensitive: stream != NULL); |
517 | |
518 | g_object_notify_by_pspec (G_OBJECT (controls), pspec: properties[PROP_MEDIA_STREAM]); |
519 | } |
520 | |