1/*
2 *  Copyright (C) 2013 Igalia S.L
3 *
4 *  This library is free software; you can redistribute it and/or
5 *  modify it under the terms of the GNU Library General Public
6 *  License as published by the Free Software Foundation; either
7 *  version 2 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 *  Library General Public License for more details.
13 *
14 *  You should have received a copy of the GNU Library General Public License
15 *  along with this library; see the file COPYING.LIB.  If not, write to
16 *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 *  Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21
22#if ENABLE(VIDEO) && USE(GSTREAMER) && USE(NATIVE_FULLSCREEN_VIDEO)
23
24#include "FullscreenVideoControllerGtk.h"
25
26#include "FullscreenVideoControllerGStreamer.h"
27#include "GRefPtrGtk.h"
28#include "GStreamerGWorld.h"
29#include "GtkVersioning.h"
30#include "MediaPlayer.h"
31#include "MediaPlayerPrivateGStreamerBase.h"
32
33#include <gdk/gdk.h>
34#include <gdk/gdkkeysyms.h>
35#include <glib/gi18n-lib.h>
36#include <gtk/gtk.h>
37#include <wtf/text/CString.h>
38
39#define HUD_AUTO_HIDE_INTERVAL 3000 // 3 seconds
40#define PROGRESS_BAR_UPDATE_INTERVAL 150 // 150ms
41
42// Use symbolic icons only if we build with GTK+-3 support. They could
43// be enabled for the GTK+2 build but we'd need to bump the required
44// version to at least 2.22.
45#if GTK_MAJOR_VERSION > 2
46#define ICON_NAME_SUFFIX "-symbolic"
47#else
48#define ICON_NAME_SUFFIX
49#endif
50
51#define PLAY_ICON_NAME "media-playback-start" ICON_NAME_SUFFIX
52#define PAUSE_ICON_NAME "media-playback-pause" ICON_NAME_SUFFIX
53#define EXIT_FULLSCREEN_ICON_NAME "view-restore" ICON_NAME_SUFFIX
54
55namespace WebCore {
56
57static gboolean hideHudCallback(FullscreenVideoControllerGtk* controller)
58{
59    controller->hideHud();
60    return FALSE;
61}
62
63static gboolean onFullscreenGtkMotionNotifyEvent(GtkWidget* widget, GdkEventMotion* event,  FullscreenVideoControllerGtk* controller)
64{
65    controller->showHud(true);
66    return TRUE;
67}
68
69static void onFullscreenGtkActiveNotification(GtkWidget* widget, GParamSpec* property, FullscreenVideoControllerGtk* controller)
70{
71    if (!gtk_window_is_active(GTK_WINDOW(widget)))
72        controller->hideHud();
73}
74
75static gboolean onFullscreenGtkConfigureEvent(GtkWidget* widget, GdkEventConfigure* event, FullscreenVideoControllerGtk* controller)
76{
77    controller->gtkConfigure(event);
78    return TRUE;
79}
80
81static void onFullscreenGtkDestroy(GtkWidget* widget, FullscreenVideoControllerGtk* controller)
82{
83    controller->exitFullscreen();
84}
85
86static void togglePlayPauseActivated(GtkAction* action, FullscreenVideoControllerGtk* controller)
87{
88    controller->togglePlay();
89}
90
91static void exitFullscreenActivated(GtkAction* action, FullscreenVideoControllerGtk* controller)
92{
93    controller->exitOnUserRequest();
94}
95
96static gboolean progressBarUpdateCallback(FullscreenVideoControllerGtk* controller)
97{
98    return controller->updateHudProgressBar();
99}
100
101static gboolean timeScaleButtonPressed(GtkWidget* widget, GdkEventButton* event, FullscreenVideoControllerGtk* controller)
102{
103    if (event->type != GDK_BUTTON_PRESS)
104        return FALSE;
105
106    controller->beginSeek();
107    return FALSE;
108}
109
110static gboolean timeScaleButtonReleased(GtkWidget* widget, GdkEventButton* event, FullscreenVideoControllerGtk* controller)
111{
112    controller->endSeek();
113    return FALSE;
114}
115
116static void timeScaleValueChanged(GtkWidget* widget, FullscreenVideoControllerGtk* controller)
117{
118    controller->doSeek();
119}
120
121static void volumeValueChanged(GtkScaleButton *button, gdouble value, FullscreenVideoControllerGtk* controller)
122{
123    controller->setVolume(static_cast<float>(value));
124}
125
126
127FullscreenVideoControllerGtk::FullscreenVideoControllerGtk(MediaPlayerPrivateGStreamerBase* player)
128    : FullscreenVideoControllerGStreamer(player)
129    , m_hudTimeoutId(0)
130    , m_progressBarUpdateId(0)
131    , m_seekLock(false)
132    , m_window(0)
133    , m_hudWindow(0)
134    , m_volumeButton(0)
135    , m_keyPressSignalId(0)
136    , m_destroySignalId(0)
137    , m_isActiveSignalId(0)
138    , m_motionNotifySignalId(0)
139    , m_configureEventSignalId(0)
140    , m_hudMotionNotifySignalId(0)
141    , m_timeScaleButtonPressedSignalId(0)
142    , m_timeScaleButtonReleasedSignalId(0)
143    , m_playActionActivateSignalId(0)
144    , m_exitFullcreenActionActivateSignalId(0)
145{
146}
147
148void FullscreenVideoControllerGtk::gtkConfigure(GdkEventConfigure* event)
149{
150    updateHudPosition();
151}
152
153void FullscreenVideoControllerGtk::showHud(bool autoHide)
154{
155    if (!m_hudWindow)
156        return;
157
158    if (m_hudTimeoutId) {
159        g_source_remove(m_hudTimeoutId);
160        m_hudTimeoutId = 0;
161    }
162
163    // Show the cursor.
164    GdkWindow* window = gtk_widget_get_window(m_window);
165    gdk_window_set_cursor(window, 0);
166
167    // Update the progress bar immediately before showing the window.
168    updateHudProgressBar();
169    gtk_widget_show_all(m_hudWindow);
170    updateHudPosition();
171
172    // Start periodic updates of the progress bar.
173    if (!m_progressBarUpdateId)
174        m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast<GSourceFunc>(progressBarUpdateCallback), this);
175
176    // Hide the hud in few seconds, if requested.
177    if (autoHide)
178        m_hudTimeoutId = g_timeout_add(HUD_AUTO_HIDE_INTERVAL, reinterpret_cast<GSourceFunc>(hideHudCallback), this);
179}
180
181void FullscreenVideoControllerGtk::hideHud()
182{
183    if (m_hudTimeoutId) {
184        g_source_remove(m_hudTimeoutId);
185        m_hudTimeoutId = 0;
186    }
187
188    if (!m_hudWindow)
189        return;
190
191    // Keep the hud visible if a seek is in progress or if the volume
192    // popup is visible.
193    GtkWidget* volumePopup = gtk_scale_button_get_popup(GTK_SCALE_BUTTON(m_volumeButton));
194    if (m_seekLock || gtk_widget_get_visible(volumePopup)) {
195        showHud(true);
196        return;
197    }
198
199    GdkWindow* window = gtk_widget_get_window(m_window);
200    GRefPtr<GdkCursor> cursor = adoptGRef(gdk_cursor_new(GDK_BLANK_CURSOR));
201    gdk_window_set_cursor(window, cursor.get());
202
203    gtk_widget_hide(m_hudWindow);
204
205    if (m_progressBarUpdateId) {
206        g_source_remove(m_progressBarUpdateId);
207        m_progressBarUpdateId = 0;
208    }
209}
210
211static gboolean onFullscreenGtkKeyPressEvent(GtkWidget* widget, GdkEventKey* event, FullscreenVideoControllerGtk* controller)
212{
213    switch (event->keyval) {
214    case GDK_Escape:
215        controller->exitOnUserRequest();
216        break;
217    case GDK_space:
218    case GDK_Return:
219        controller->togglePlay();
220        break;
221    case GDK_Up:
222        controller->increaseVolume();
223        break;
224    case GDK_Down:
225        controller->decreaseVolume();
226        break;
227    default:
228        break;
229    }
230
231    return TRUE;
232}
233
234void FullscreenVideoControllerGtk::initializeWindow()
235{
236    m_window = reinterpret_cast<GtkWidget*>(m_gstreamerGWorld->platformVideoWindow()->window());
237
238    if (!m_hudWindow)
239        createHud();
240
241    // Ensure black background.
242#ifdef GTK_API_VERSION_2
243    GdkColor color = { 1, 0, 0, 0 };
244    gtk_widget_modify_bg(m_window, GTK_STATE_NORMAL, &color);
245#else
246    GdkRGBA color = { 0, 0, 0, 1};
247    gtk_widget_override_background_color(m_window, GTK_STATE_FLAG_NORMAL, &color);
248#endif
249    gtk_widget_set_double_buffered(m_window, FALSE);
250
251    m_keyPressSignalId = g_signal_connect(m_window, "key-press-event", G_CALLBACK(onFullscreenGtkKeyPressEvent), this);
252    m_destroySignalId = g_signal_connect(m_window, "destroy", G_CALLBACK(onFullscreenGtkDestroy), this);
253    m_isActiveSignalId = g_signal_connect(m_window, "notify::is-active", G_CALLBACK(onFullscreenGtkActiveNotification), this);
254
255    gtk_widget_show_all(m_window);
256
257    GdkWindow* window = gtk_widget_get_window(m_window);
258    GRefPtr<GdkCursor> cursor = adoptGRef(gdk_cursor_new(GDK_BLANK_CURSOR));
259    gdk_window_set_cursor(window, cursor.get());
260
261    m_motionNotifySignalId = g_signal_connect(m_window, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this);
262    m_configureEventSignalId = g_signal_connect(m_window, "configure-event", G_CALLBACK(onFullscreenGtkConfigureEvent), this);
263
264    gtk_window_fullscreen(GTK_WINDOW(m_window));
265    showHud(true);
266}
267
268void FullscreenVideoControllerGtk::updateHudPosition()
269{
270    if (!m_hudWindow)
271        return;
272
273    // Get the screen rectangle.
274    GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(m_window));
275    GdkWindow* window = gtk_widget_get_window(m_window);
276    GdkRectangle fullscreenRectangle;
277    gdk_screen_get_monitor_geometry(screen, gdk_screen_get_monitor_at_window(screen, window), &fullscreenRectangle);
278
279    // Get the popup window size.
280    int hudWidth, hudHeight;
281    gtk_window_get_size(GTK_WINDOW(m_hudWindow), &hudWidth, &hudHeight);
282
283    // Resize the hud to the full width of the screen.
284    gtk_window_resize(GTK_WINDOW(m_hudWindow), fullscreenRectangle.width, hudHeight);
285
286    // Move the hud to the bottom of the screen.
287    gtk_window_move(GTK_WINDOW(m_hudWindow), fullscreenRectangle.x, fullscreenRectangle.height + fullscreenRectangle.y - hudHeight);
288}
289
290void FullscreenVideoControllerGtk::destroyWindow()
291{
292    if (!m_hudWindow)
293        return;
294
295    g_signal_handler_disconnect(m_window, m_keyPressSignalId);
296    g_signal_handler_disconnect(m_window, m_destroySignalId);
297    g_signal_handler_disconnect(m_window, m_isActiveSignalId);
298    g_signal_handler_disconnect(m_window, m_motionNotifySignalId);
299    g_signal_handler_disconnect(m_window, m_configureEventSignalId);
300    g_signal_handler_disconnect(m_hudWindow, m_hudMotionNotifySignalId);
301    g_signal_handler_disconnect(m_timeHScale, m_timeScaleButtonPressedSignalId);
302    g_signal_handler_disconnect(m_timeHScale, m_timeScaleButtonReleasedSignalId);
303    g_signal_handler_disconnect(m_timeHScale, m_hscaleUpdateId);
304    g_signal_handler_disconnect(m_volumeButton, m_volumeUpdateId);
305    g_signal_handler_disconnect(m_playPauseAction, m_playActionActivateSignalId);
306    g_signal_handler_disconnect(m_exitFullscreenAction, m_exitFullcreenActionActivateSignalId);
307
308    if (m_hudTimeoutId) {
309        g_source_remove(m_hudTimeoutId);
310        m_hudTimeoutId = 0;
311    }
312
313    if (m_progressBarUpdateId) {
314        g_source_remove(m_progressBarUpdateId);
315        m_progressBarUpdateId = 0;
316    }
317
318    gtk_widget_hide(m_window);
319
320    if (m_hudWindow)
321        gtk_widget_destroy(m_hudWindow);
322    m_hudWindow = 0;
323}
324
325void FullscreenVideoControllerGtk::playStateChanged()
326{
327    if (m_client->mediaPlayerIsPaused())
328        g_object_set(m_playPauseAction, "tooltip", _("Play"), "icon-name", PLAY_ICON_NAME, NULL);
329    else
330        g_object_set(m_playPauseAction, "tooltip", _("Pause"), "icon-name", PAUSE_ICON_NAME, NULL);
331    showHud(!m_client->mediaPlayerIsPaused());
332}
333
334void FullscreenVideoControllerGtk::volumeChanged()
335{
336    if (!m_volumeButton)
337        return;
338
339    g_signal_handler_block(m_volumeButton, m_volumeUpdateId);
340    gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), m_player->volume());
341    g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId);
342}
343
344void FullscreenVideoControllerGtk::muteChanged()
345{
346    if (!m_volumeButton)
347        return;
348
349    g_signal_handler_block(m_volumeButton, m_volumeUpdateId);
350    gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), m_player->muted() ? 0 : m_player->volume());
351    g_signal_handler_unblock(m_volumeButton, m_volumeUpdateId);
352}
353
354void FullscreenVideoControllerGtk::beginSeek()
355{
356    m_seekLock = true;
357}
358
359void FullscreenVideoControllerGtk::doSeek()
360{
361    if (!m_seekLock)
362        return;
363
364    m_player->seek(gtk_range_get_value(GTK_RANGE(m_timeHScale))*m_player->duration() / 100);
365}
366
367void FullscreenVideoControllerGtk::endSeek()
368{
369    m_seekLock = false;
370}
371
372gboolean FullscreenVideoControllerGtk::updateHudProgressBar()
373{
374    float mediaDuration(m_player->duration());
375    float mediaPosition(m_player->currentTime());
376
377    if (!m_seekLock) {
378        gdouble value = 0.0;
379
380        if (mediaPosition && mediaDuration)
381            value = (mediaPosition * 100.0) / mediaDuration;
382
383        GtkAdjustment* adjustment = gtk_range_get_adjustment(GTK_RANGE(m_timeHScale));
384        gtk_adjustment_set_value(adjustment, value);
385    }
386
387    gtk_range_set_fill_level(GTK_RANGE(m_timeHScale), (m_player->maxTimeLoaded() / mediaDuration)* 100);
388
389    gchar* label = g_strdup_printf("%s / %s", timeToString(mediaPosition).utf8().data(), timeToString(mediaDuration).utf8().data());
390    gtk_label_set_text(GTK_LABEL(m_timeLabel), label);
391    g_free(label);
392    return TRUE;
393}
394
395void FullscreenVideoControllerGtk::createHud()
396{
397    m_hudWindow = gtk_window_new(GTK_WINDOW_POPUP);
398    gtk_window_set_gravity(GTK_WINDOW(m_hudWindow), GDK_GRAVITY_SOUTH_WEST);
399    gtk_window_set_type_hint(GTK_WINDOW(m_hudWindow), GDK_WINDOW_TYPE_HINT_NORMAL);
400
401    m_hudMotionNotifySignalId = g_signal_connect(m_hudWindow, "motion-notify-event", G_CALLBACK(onFullscreenGtkMotionNotifyEvent), this);
402
403#ifdef GTK_API_VERSION_2
404    GtkWidget* hbox = gtk_hbox_new(FALSE, 4);
405#else
406    GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
407#endif
408    gtk_container_add(GTK_CONTAINER(m_hudWindow), hbox);
409
410    m_playPauseAction = gtk_action_new("play", _("Play / Pause"), _("Play or pause the media"), PAUSE_ICON_NAME);
411    m_playActionActivateSignalId = g_signal_connect(m_playPauseAction, "activate", G_CALLBACK(togglePlayPauseActivated), this);
412
413    GtkWidget* item = gtk_action_create_tool_item(m_playPauseAction);
414    gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0);
415
416    GtkWidget* label = gtk_label_new(_("Time:"));
417    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, TRUE, 0);
418
419    GtkAdjustment* adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 100.0, 0.1, 1.0, 1.0));
420#ifdef GTK_API_VERSION_2
421    m_timeHScale = gtk_hscale_new(adjustment);
422#else
423    m_timeHScale = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL, adjustment);
424#endif
425    gtk_scale_set_draw_value(GTK_SCALE(m_timeHScale), FALSE);
426    gtk_range_set_show_fill_level(GTK_RANGE(m_timeHScale), TRUE);
427    m_timeScaleButtonPressedSignalId = g_signal_connect(m_timeHScale, "button-press-event", G_CALLBACK(timeScaleButtonPressed), this);
428    m_timeScaleButtonReleasedSignalId = g_signal_connect(m_timeHScale, "button-release-event", G_CALLBACK(timeScaleButtonReleased), this);
429    m_hscaleUpdateId = g_signal_connect(m_timeHScale, "value-changed", G_CALLBACK(timeScaleValueChanged), this);
430
431    gtk_box_pack_start(GTK_BOX(hbox), m_timeHScale, TRUE, TRUE, 0);
432
433    m_timeLabel = gtk_label_new("");
434    gtk_box_pack_start(GTK_BOX(hbox), m_timeLabel, FALSE, TRUE, 0);
435
436    // Volume button.
437    m_volumeButton = gtk_volume_button_new();
438    gtk_box_pack_start(GTK_BOX(hbox), m_volumeButton, FALSE, TRUE, 0);
439    gtk_scale_button_set_value(GTK_SCALE_BUTTON(m_volumeButton), m_player->volume());
440    m_volumeUpdateId = g_signal_connect(m_volumeButton, "value-changed", G_CALLBACK(volumeValueChanged), this);
441
442    m_exitFullscreenAction = gtk_action_new("exit", _("Exit Fullscreen"), _("Exit from fullscreen mode"), EXIT_FULLSCREEN_ICON_NAME);
443    m_exitFullcreenActionActivateSignalId = g_signal_connect(m_exitFullscreenAction, "activate", G_CALLBACK(exitFullscreenActivated), this);
444    g_object_set(m_exitFullscreenAction, "icon-name", EXIT_FULLSCREEN_ICON_NAME, NULL);
445    item = gtk_action_create_tool_item(m_exitFullscreenAction);
446    gtk_box_pack_start(GTK_BOX(hbox), item, FALSE, TRUE, 0);
447
448    m_progressBarUpdateId = g_timeout_add(PROGRESS_BAR_UPDATE_INTERVAL, reinterpret_cast<GSourceFunc>(progressBarUpdateCallback), this);
449
450    playStateChanged();
451}
452
453} // namespace WebCore
454#endif
455