diff --git a/README.md b/README.md
index b84ed129..b006da0a 100644
--- a/README.md
+++ b/README.md
@@ -28,10 +28,11 @@ Using hardware acceleration is highly recommended. As stated in `GStreamer` wiki
In the case of OpenGL based elements, the buffers have the GstVideoGLTextureUploadMeta meta, which
efficiently copies the content of the VA-API surface into a GL texture.
```
-Clapper uses `OpenGL` based sinks, so when `VA-API` is available, both CPU and RAM usage is much lower.
+Clapper uses `OpenGL` based sinks, so when `VA-API` is available, both CPU and RAM usage is much lower. Especially if you have `gst-plugins-bad` 1.18+ with new `vah264dec` decoder which shares a single GL context with Clapper and uses DRM connection. If you have an AMD/Intel GPU, I highly recommend this new decoder.
-To use `VA-API` make sure you have `gstreamer1-vaapi` installed. Verify with:
+To use `VA-API` with H.264 videos, make sure you have `gst-plugins-bad` 1.18+. For other codecs additionally install `gstreamer1-vaapi`. Verify with:
```shell
+gst-inspect-1.0 vah264dec
gst-inspect-1.0 vaapi
```
On some older GPUs you might need to export `GST_VAAPI_ALL_DRIVERS=1` environment variable.
@@ -40,11 +41,13 @@ Other acceleration methods (supported by `GStreamer`) should also work, but I ha
## Requirements
-Clapper uses `GStreamer` bindings from `GI` repository, so if your distro ships them as separate package, they must be installed first.
+Clapper uses GTK4 along with `GStreamer` bindings from `GI` repository, so if your distro ships them as separate package, they must be installed first.
Additionally Clapper requires these `GStreamer` elements:
-* [gtkglsink](https://gstreamer.freedesktop.org/documentation/gtk/gtkglsink.html)
+* [gtk4glsink](https://gstreamer.freedesktop.org/documentation/gtk/gtkglsink.html)
* [glsinkbin](https://gstreamer.freedesktop.org/documentation/opengl/glsinkbin.html)
+**Attention:** `gtk4glsink` is my own port of current GStreamer `gtkglsink` to GTK4. The element is not part of GStreamer yet (pending review). Fedora package is available in my OBS repository. It will be installed along with Clapper if you add my repo to `dnf` package manager. Otherwise you might want to build it yourself from [source code](https://gitlab.freedesktop.org/Rafostar/gst-plugins-good/-/tree/GTK4) of my gstreamer GTK4 branch.
+
Other required plugins (codecs) depend on video format.
Recommended additional packages you should install manually via package manager:
@@ -53,69 +56,90 @@ Recommended additional packages you should install manually via package manager:
Please note that packages naming varies by distro.
-## Installation
-Run in terminal:
-```shell
-meson builddir --prefix=/usr/local
-sudo meson install -C builddir
-```
-
-Additional GStreamer elements installation:
-
- Fedora
-
-Enable RPM Fusion and run:
-```shell
-sudo dnf install \
- gstreamer1-plugins-base \
- gstreamer1-plugins-good-gtk \
- gstreamer1-libav \
- gstreamer1-vaapi
-```
-
-
-
- openSUSE
-
-```shell
-sudo zypper install \
- gstreamer-plugins-base \
- gstreamer-plugins-good \
- gstreamer-plugins-libav \
- gstreamer-plugins-vaapi
-```
-
-
-
- Arch Linux
-
-```shell
-sudo pacman -S \
- gst-plugins-base \
- gst-plugin-gtk \
- gst-libav \
- gstreamer-vaapi
-```
-
-
## Packages
The [pkgs folder](https://github.com/Rafostar/clapper/tree/master/pkgs) in this repository contains build scripts for various package formats. You can use them to build package yourself or download one of pre-built packages:
- Fedora, openSUSE & SLE (rpm)
+ Debian, Fedora, openSUSE & Ubuntu
-Pre-built packages are available here:
-[software.opensuse.org//download.html?project=home:sp1rit&package=clapper](https://software.opensuse.org//download.html?project=home%3Asp1rit&package=clapper) ([See status](https://build.opensuse.org/package/show/home:sp1rit/clapper))
+Pre-built packages are available in [my repo](https://software.opensuse.org//download.html?project=home%3ARafostar&package=clapper) ([see status](https://build.opensuse.org/package/show/home:Rafostar/clapper))
-Arch Linux
+Arch Linux
-You can get clapper from the AUR: [clapper-git](https://aur.archlinux.org/packages/clapper-git), or
+You can get Clapper from the AUR: [clapper-git](https://aur.archlinux.org/packages/clapper-git), or
```shell
cd pkgs/arch
makepkg -si
```
+## Installation from source code
+Run in terminal:
+```shell
+meson builddir --prefix=/usr/local
+sudo meson install -C builddir
+```
+
+GStreamer elements installation:
+
+ Debian/Ubuntu
+
+```shell
+sudo apt install \
+ gstreamer1.0-plugins-base \
+ gstreamer1.0-plugins-good \
+ gstreamer1.0-plugins-bad \
+ gstreamer1.0-gl \
+ gstreamer1.0-gtk4 \
+ gstreamer1.0-libav \
+ gstreamer-vaapi
+```
+
+
+
+ Fedora
+
+Enable RPM Fusion and run:
+```shell
+sudo dnf install \
+ gstreamer1-plugins-base \
+ gstreamer1-plugins-good \
+ gstreamer1-plugins-good-gtk4 \
+ gstreamer1-plugins-bad-free \
+ gstreamer1-plugins-bad-free-extras \
+ gstreamer1-libav \
+ gstreamer1-vaapi
+```
+
+
+
+ openSUSE
+
+```shell
+sudo zypper install \
+ gstreamer-plugins-base \
+ gstreamer-plugins-good \
+ gstreamer-plugins-good-gtk4 \
+ gstreamer-plugins-bad \
+ gstreamer-plugins-libav \
+ gstreamer-plugins-vaapi
+```
+
+
+
+ Arch Linux
+
+```shell
+sudo pacman -S \
+ gst-plugins-base \
+ gst-plugins-good \
+ gst-plugin-gtk4 \
+ gst-plugins-bad-libs \
+ gst-libav \
+ gstreamer-vaapi
+```
+
+
## Special Thanks
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.
diff --git a/clapper_src/app.js b/clapper_src/app.js
index 98b1397e..53a69a8c 100644
--- a/clapper_src/app.js
+++ b/clapper_src/app.js
@@ -1,5 +1,6 @@
const { Gdk, GLib, GObject, Gtk, GstPlayer } = imports.gi;
const Debug = imports.clapper_src.debug;
+const { HeaderBar } = imports.clapper_src.headerbar;
const { Interface } = imports.clapper_src.interface;
const { Player } = imports.clapper_src.player;
const { Window } = imports.clapper_src.window;
@@ -20,8 +21,6 @@ var App = GObject.registerClass({
{
_init(opts)
{
- GLib.set_prgname(APP_NAME);
-
super._init({
application_id: pkg.name
});
@@ -39,7 +38,10 @@ var App = GObject.registerClass({
this.window = null;
this.interface = null;
this.player = null;
- this.dragStartReady = false;
+ this.dragAllowed = false;
+
+ this.posX = 0;
+ this.posY = 0;
}
vfunc_startup()
@@ -51,35 +53,43 @@ var App = GObject.registerClass({
'realize', this._onWindowRealize.bind(this)
);
this.window.connect(
- 'key-press-event', this._onWindowKeyPressEvent.bind(this)
+ 'fullscreen-changed', this._onWindowFullscreenChanged.bind(this)
);
this.window.connect(
- 'fullscreen-changed', this._onWindowFullscreenChanged.bind(this)
+ 'close-request', this._onWindowCloseRequest.bind(this)
);
this.interface = new Interface();
- let headerBar = new Gtk.HeaderBar({
- title: APP_NAME,
- show_close_button: true,
- });
- headerBar.pack_end(this.interface.controls.openMenuButton);
- headerBar.pack_end(this.interface.controls.fullscreenButton);
+
+ let headerStart = [];
+ let headerEnd = [
+ this.interface.controls.openMenuButton,
+ this.interface.controls.fullscreenButton
+ ];
+ let headerBar = new HeaderBar(this.window, headerStart, headerEnd);
this.interface.addHeaderBar(headerBar, APP_NAME);
+
this.interface.controls.fullscreenButton.connect(
- 'clicked', () => this._onInterfaceToggleFullscreenClicked(true)
+ 'clicked', () => this.activeWindow.fullscreen()
);
this.interface.controls.unfullscreenButton.connect(
- 'clicked', () => this._onInterfaceToggleFullscreenClicked(false)
+ 'clicked', () => this.activeWindow.unfullscreen()
);
this.window.set_titlebar(this.interface.headerBar);
- this.window.add(this.interface);
+ this.window.set_child(this.interface);
}
vfunc_activate()
{
super.vfunc_activate();
- this.window.show_all();
+
+ this.window.present();
+ Gtk.StyleContext.add_provider_for_display(
+ Gdk.Display.get_default(),
+ this.cssProvider,
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
+ );
}
run(arr)
@@ -94,8 +104,8 @@ var App = GObject.registerClass({
this.hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
this.hideCursorTimeout = null;
- if(this.isCursorInPlayer)
- this.playerWindow.set_cursor(this.blankCursor);
+ if(this.player.motionController.is_pointer)
+ this.player.widget.set_cursor(this.blankCursor);
return GLib.SOURCE_REMOVE;
});
@@ -107,7 +117,7 @@ var App = GObject.registerClass({
this.hideControlsTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => {
this.hideControlsTimeout = null;
- if(this.window.isFullscreen && this.isCursorInPlayer) {
+ if(this.window.isFullscreen && this.player.motionController.is_pointer) {
this.clearTimeout('updateTime');
this.interface.revealControls(false);
}
@@ -146,80 +156,63 @@ var App = GObject.registerClass({
{
this.window.disconnect(this.windowRealizeSignal);
- Gtk.StyleContext.add_provider_for_screen(
- Gdk.Screen.get_default(),
- this.cssProvider,
- Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
- );
-
this.player = new Player();
- this.player.widget.add_events(
- Gdk.EventMask.SCROLL_MASK
- | Gdk.EventMask.ENTER_NOTIFY_MASK
- | Gdk.EventMask.LEAVE_NOTIFY_MASK
- );
- this.interface.addPlayer(this.player);
- this.player.connect('warning', this._onPlayerWarning.bind(this));
- this.player.connect('error', this._onPlayerError.bind(this));
+ if(!this.player.widget)
+ return this.quit();
+
+ this.player.widget.width_request = 960;
+ this.player.widget.height_request = 540;
+
+ this.interface.addPlayer(this.player);
this.player.connect('state-changed', this._onPlayerStateChanged.bind(this));
- this.player.connectWidget(
- 'button-press-event', this._onPlayerButtonPressEvent.bind(this)
+ this.player.clickGesture.connect(
+ 'pressed', this._onPlayerPressed.bind(this)
);
- this.player.connectWidget(
- 'enter-notify-event', this._onPlayerEnterNotifyEvent.bind(this)
+ this.player.keyController.connect(
+ 'key-pressed', this._onPlayerKeyPressed.bind(this)
);
- this.player.connectWidget(
- 'leave-notify-event', this._onPlayerLeaveNotifyEvent.bind(this)
+ this.player.motionController.connect(
+ 'enter', this._onPlayerEnter.bind(this)
);
- this.player.connectWidget(
- 'motion-notify-event', this._onPlayerMotionNotifyEvent.bind(this)
+ this.player.motionController.connect(
+ 'leave', this._onPlayerLeave.bind(this)
+ );
+ this.player.motionController.connect(
+ 'motion', this._onPlayerMotion.bind(this)
+ );
+ this.player.dragGesture.connect(
+ 'drag-update', this._onPlayerDragUpdate.bind(this)
);
/* Widget signals that are disconnected after first run */
this._playerRealizeSignal = this.player.widget.connect(
'realize', this._onPlayerRealize.bind(this)
);
- this._playerDrawSignal = this.player.widget.connect(
- 'draw', this._onPlayerDraw.bind(this)
+ this._playerMapSignal = this.player.widget.connect(
+ 'map', this._onPlayerMap.bind(this)
);
}
_onWindowFullscreenChanged(window, isFullscreen)
{
- // when changing fullscreen pango layout of popup is lost
- // and we need to re-add marks to the new layout
- this.interface.controls.setVolumeMarks(false);
-
if(isFullscreen) {
this.setUpdateTimeInterval();
this.setHideControlsTimeout();
- this.interface.controls.unfullscreenButton.set_sensitive(true);
- this.interface.controls.unfullscreenButton.show();
- this.interface.showControls(true);
}
else {
this.clearTimeout('updateTime');
- this.interface.controls.unfullscreenButton.set_sensitive(false);
- this.interface.controls.unfullscreenButton.hide();
- this.interface.showControls(false);
}
- this.interface.setControlsOnVideo(isFullscreen);
- this.interface.controls.setVolumeMarks(true);
- this.interface.controls.setFullscreenMode(isFullscreen);
+ this.interface.setFullscreenMode(isFullscreen);
}
- _onWindowKeyPressEvent(self, event)
+ _onPlayerKeyPressed(self, keyval, keycode, state)
{
- let [res, key] = event.get_keyval();
- if(!res) return;
-
- //let keyName = Gdk.keyval_name(key);
let bool = false;
- switch(key) {
+ switch(keyval) {
case Gdk.KEY_space:
case Gdk.KEY_Return:
this.player.toggle_play();
@@ -244,42 +237,27 @@ var App = GObject.registerClass({
break;
case Gdk.KEY_q:
case Gdk.KEY_Q:
- this.window.destroy();
+ this._onWindowCloseRequest();
break;
default:
break;
}
}
- _onInterfaceToggleFullscreenClicked(isFsRequested)
- {
- if(this.window.isFullscreen === isFsRequested)
- return;
-
- this.window.toggleFullscreen();
- }
-
_onPlayerRealize()
{
this.player.widget.disconnect(this._playerRealizeSignal);
this.player.renderer.expose();
- let display = this.player.widget.get_display();
+ this.defaultCursor = Gdk.Cursor.new_from_name('default', null);
+ this.blankCursor = Gdk.Cursor.new_from_name('none', null);
- this.defaultCursor = Gdk.Cursor.new_from_name(
- display, 'default'
- );
- this.blankCursor = Gdk.Cursor.new_for_display(
- display, Gdk.CursorType.BLANK_CURSOR
- );
-
- this.playerWindow = this.player.widget.get_window();
this.setHideCursorTimeout();
}
- _onPlayerDraw(self, data)
+ _onPlayerMap(self, data)
{
- this.player.widget.disconnect(this._playerDrawSignal);
+ this.player.widget.disconnect(this._playerMapSignal);
this.emit('ready', true);
if(this.playlist.length)
@@ -291,6 +269,7 @@ var App = GObject.registerClass({
if(state === GstPlayer.PlayerState.BUFFERING)
return;
+ let isInhibited = false;
let flags = Gtk.ApplicationInhibitFlags.SUSPEND
| Gtk.ApplicationInhibitFlags.IDLE;
@@ -303,79 +282,66 @@ var App = GObject.registerClass({
flags,
'video is playing'
);
+ if(!this.inhibitCookie)
+ debug(new Error('could not inhibit session!'));
+
+ isInhibited = (this.inhibitCookie > 0);
}
else {
- if(!this.inhibitCookie)
+ //if(!this.inhibitCookie)
return;
+ /* Uninhibit seems to be broken as of GTK 3.99.2
this.uninhibit(this.inhibitCookie);
this.inhibitCookie = null;
+ */
}
- debug('set prevent suspend to: ' + this.is_inhibited(flags));
+ debug(`set prevent suspend to: ${isInhibited}`);
}
- _onPlayerButtonPressEvent(self, event)
+ _onPlayerPressed(gesture, nPress, x, y)
{
- let [res, button] = event.get_button();
- if(!res) return;
-
- this.dragStartReady = false;
+ let button = gesture.get_current_button();
+ let isDouble = (nPress % 2 == 0);
+ this.dragAllowed = !isDouble;
switch(button) {
case Gdk.BUTTON_PRIMARY:
- this._handlePrimaryButtonPress(event, button);
+ if(isDouble)
+ this.window.toggleFullscreen();
break;
case Gdk.BUTTON_SECONDARY:
- if(event.get_event_type() === Gdk.EventType.BUTTON_PRESS)
- this.player.toggle_play();
+ this.player.toggle_play();
break;
default:
break;
}
}
- _handlePrimaryButtonPress(event, button)
+ _onPlayerEnter(controller, x, y)
{
- let eventType = event.get_event_type();
-
- switch(eventType) {
- case Gdk.EventType.BUTTON_PRESS:
- let [res, x, y] = event.get_root_coords();
- if(!res)
- break;
- this.dragStartX = x;
- this.dragStartY = y;
- this.dragStartReady = true;
- break;
- case Gdk.EventType.DOUBLE_BUTTON_PRESS:
- this.window.toggleFullscreen();
- break;
- default:
- break;
- }
- }
-
- _onPlayerEnterNotifyEvent(self, event)
- {
- this.isCursorInPlayer = true;
-
this.setHideCursorTimeout();
if(this.window.isFullscreen)
this.setHideControlsTimeout();
}
- _onPlayerLeaveNotifyEvent(self, event)
+ _onPlayerLeave(controller)
{
- this.isCursorInPlayer = false;
-
this.clearTimeout('hideCursor');
this.clearTimeout('hideControls');
}
- _onPlayerMotionNotifyEvent(self, event)
+ _onPlayerMotion(self, posX, posY)
{
- this.playerWindow.set_cursor(this.defaultCursor);
+ /* GTK4 sometimes generates motions with same coords */
+ if(this.posX === posX && this.posY === posY)
+ return;
+
+ this.posX = posX;
+ this.posY = posY;
+
+ this.player.widget.set_cursor(this.defaultCursor);
this.setHideCursorTimeout();
if(this.window.isFullscreen) {
@@ -388,36 +354,40 @@ var App = GObject.registerClass({
else if(this.hideControlsTimeout) {
this.clearTimeout('hideControls');
}
+ }
- if(!this.dragStartReady || this.window.isFullscreen)
+ _onPlayerDragUpdate(gesture, offsetX, offsetY)
+ {
+ if(!this.dragAllowed || this.activeWindow.isFullscreen)
return;
- let [res, x, y] = event.get_root_coords();
- if(!res) return;
+ let { gtk_double_click_distance } = this.player.widget.get_settings();
- let startDrag = this.player.widget.drag_check_threshold(
- this.dragStartX, this.dragStartY, x, y
- );
- if(!startDrag) return;
+ if (
+ Math.abs(offsetX) > gtk_double_click_distance
+ || Math.abs(offsetY) > gtk_double_click_distance
+ ) {
+ let [isActive, startX, startY] = gesture.get_start_point();
+ if(!isActive) return;
- this.dragStartReady = false;
- let timestamp = event.get_time();
+ this.activeWindow.get_surface().begin_move(
+ gesture.get_device(),
+ gesture.get_current_button(),
+ startX,
+ startY,
+ gesture.get_current_event_time()
+ );
- this.window.begin_move_drag(
- Gdk.BUTTON_PRIMARY,
- this.dragStartX,
- this.dragStartY,
- timestamp
- );
+ gesture.reset();
+ }
}
- _onPlayerWarning(self, error)
+ _onWindowCloseRequest()
{
- debug(error.message, 'LEVEL_WARNING');
- }
+ this.window.destroy();
+ this.player.widget.emit('destroy');
+ this.interface.emit('destroy');
- _onPlayerError(self, error)
- {
- debug(error);
+ this.quit();
}
});
diff --git a/clapper_src/buttons.js b/clapper_src/buttons.js
index a4597396..0e16a9b5 100644
--- a/clapper_src/buttons.js
+++ b/clapper_src/buttons.js
@@ -1,41 +1,24 @@
const { GObject, Gtk } = imports.gi;
-var BoxedIconButton = GObject.registerClass(
-class BoxedIconButton extends Gtk.Button
+var CustomButton = GObject.registerClass(
+class ClapperCustomButton extends Gtk.Button
{
- _init(icon, size, isFullscreen)
+ _init(opts)
{
- super._init({
+ opts = opts || {};
+
+ let defaults = {
margin_top: 4,
margin_bottom: 4,
- can_focus: false,
- can_default: false,
- });
+ margin_start: 1,
+ margin_end: 1,
+ };
+ Object.assign(opts, defaults);
- this.isFullscreen = isFullscreen || false;
+ super._init(opts);
- size = size || Gtk.IconSize.SMALL_TOOLBAR;
- let image = Gtk.Image.new_from_icon_name(icon, size);
-
- if(image)
- this.set_image(image);
-
- this.image.defaultSize = size;
- this.image.fullscreenSize = (size === Gtk.IconSize.SMALL_TOOLBAR)
- ? Gtk.IconSize.LARGE_TOOLBAR
- : Gtk.IconSize.DND;
-
- this.get_style_context().add_class('flat');
-
- this.box = new Gtk.Box();
- this.box.pack_start(this, false, false, 0);
-
- super.show();
- }
-
- get visible()
- {
- return this.box.visible;
+ this.isFullscreen = false;
+ this.add_css_class('flat');
}
setFullscreenMode(isFullscreen)
@@ -43,62 +26,126 @@ class BoxedIconButton extends Gtk.Button
if(this.isFullscreen === isFullscreen)
return;
- this.image.icon_size = (isFullscreen)
- ? this.image.fullscreenSize
- : this.image.defaultSize;
-
+ this.margin_top = (isFullscreen) ? 6 : 4;
this.isFullscreen = isFullscreen;
}
-
- show_all()
- {
- this.box.show_all();
- }
-
- show()
- {
- this.box.show();
- }
-
- hide()
- {
- this.box.hide();
- }
});
-var BoxedPopoverButton = GObject.registerClass(
-class BoxedPopoverButton extends BoxedIconButton
+var IconButton = GObject.registerClass(
+class ClapperIconButton extends CustomButton
{
- _init(icon, size, isFullscreen)
+ _init(icon)
{
- super._init(icon, size, isFullscreen);
-
- this.popover = new Gtk.Popover({
- relative_to: this.box
+ super._init({
+ icon_name: icon,
});
- this.popoverBox = new Gtk.Box({
- orientation: Gtk.Orientation.VERTICAL
- });
- this.popover.add(this.popoverBox);
- this.popoverBox.show();
+ }
+});
- if(this.isFullscreen)
- this.popover.get_style_context().add_class('osd');
+var IconToggleButton = GObject.registerClass(
+class ClapperIconToggleButton extends IconButton
+{
+ _init(primaryIcon, secondaryIcon)
+ {
+ super._init(primaryIcon);
+
+ this.primaryIcon = primaryIcon;
+ this.secondaryIcon = secondaryIcon;
}
- setFullscreenMode(isEnabled)
+ setPrimaryIcon()
{
- if(this.isFullscreen === isEnabled)
+ this.icon_name = this.primaryIcon;
+ }
+
+ setSecondaryIcon()
+ {
+ this.icon_name = this.secondaryIcon;
+ }
+});
+
+var LabelButton = GObject.registerClass(
+class ClapperLabelButton extends CustomButton
+{
+ _init(text)
+ {
+ super._init({
+ margin_start: 0,
+ margin_end: 0,
+ });
+
+ this.customLabel = new Gtk.Label({
+ label: text,
+ single_line_mode: true,
+ });
+
+ this.customLabel.add_css_class('labelbutton');
+ this.set_child(this.customLabel);
+ }
+
+ set_label(text)
+ {
+ this.customLabel.set_text(text);
+ }
+});
+
+var PopoverButton = GObject.registerClass(
+class ClapperPopoverButton extends IconButton
+{
+ _init(icon)
+ {
+ super._init(icon);
+
+ this.popover = new Gtk.Popover({
+ position: Gtk.PositionType.TOP,
+ });
+ this.popoverBox = new Gtk.Box({
+ orientation: Gtk.Orientation.VERTICAL,
+ });
+
+ this.popover.set_parent(this);
+ this.popover.set_child(this.popoverBox);
+ this.popover.set_offset(0, -this.margin_top);
+
+ if(this.isFullscreen)
+ this.popover.add_css_class('osd');
+
+ this.changeStateSignal = this.popover.connect('closed', () =>
+ this.unset_state_flags(Gtk.StateFlags.CHECKED)
+ );
+ this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ setFullscreenMode(isFullscreen)
+ {
+ if(this.isFullscreen === isFullscreen)
return;
- let action = (isEnabled) ? 'add_class' : 'remove_class';
- this.popover.get_style_context()[action]('osd');
+ super.setFullscreenMode(isFullscreen);
- super.setFullscreenMode(isEnabled);
+ this.popover.set_offset(0, -this.margin_top);
+
+ let cssClass = 'osd';
+ if(isFullscreen == this.popover.has_css_class(cssClass))
+ return;
+
+ let action = (isFullscreen) ? 'add' : 'remove';
+ this.popover[action + '_css_class'](cssClass);
}
vfunc_clicked()
{
+ this.set_state_flags(Gtk.StateFlags.CHECKED, false);
this.popover.popup();
}
+
+ _onDestroy()
+ {
+ this.disconnect(this.destroySignal);
+
+ this.popover.disconnect(this.changeStateSignal);
+ this.popover.unparent();
+ this.popoverBox.emit('destroy');
+ this.popover.emit('destroy');
+ }
});
diff --git a/clapper_src/controls.js b/clapper_src/controls.js
index 2c60e6e0..96608eb2 100644
--- a/clapper_src/controls.js
+++ b/clapper_src/controls.js
@@ -31,8 +31,8 @@ var Controls = GObject.registerClass({
valign: Gtk.Align.END,
});
- this.fullscreenMode = false;
this.durationFormated = '00:00:00';
+ this.elapsedInitial = '00:00:00/00:00:00';
this.buttonsArr = [];
this._addTogglePlayButton();
@@ -52,142 +52,110 @@ var Controls = GObject.registerClass({
this._addVolumeButton();
this.unfullscreenButton = this.addButton(
'view-restore-symbolic',
- Gtk.IconSize.SMALL_TOOLBAR,
- true
);
+ this.unfullscreenButton.set_visible(false);
this.fullscreenButton = Gtk.Button.new_from_icon_name(
'view-fullscreen-symbolic',
- Gtk.IconSize.SMALL_TOOLBAR
);
- this.setDefaultWidgetBehaviour(this.fullscreenButton);
this.openMenuButton = Gtk.Button.new_from_icon_name(
'open-menu-symbolic',
- Gtk.IconSize.SMALL_TOOLBAR
);
- this.setDefaultWidgetBehaviour(this.openMenuButton);
- this.forall(this.setDefaultWidgetBehaviour);
- this.realizeSignal = this.connect(
- 'realize', this._onControlsRealize.bind(this)
- );
- }
+ this.add_css_class('playercontrols');
- pack_start(widget, expand, fill, padding)
- {
- if(
- widget.box
- && widget.box.constructor
- && widget.box.constructor === Gtk.Box
- )
- widget = widget.box;
-
- super.pack_start(widget, expand, fill, padding);
+ this.realizeSignal = this.connect('realize', this._onRealize.bind(this));
+ this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
}
setFullscreenMode(isFullscreen)
{
- if(isFullscreen === this.fullscreenMode)
- return;
-
for(let button of this.buttonsArr)
button.setFullscreenMode(isFullscreen);
- this.fullscreenMode = isFullscreen;
+ this.unfullscreenButton.set_visible(isFullscreen);
}
- addButton(iconName, size, noPack)
+ addButton(buttonIcon)
{
- let button = new Buttons.BoxedIconButton(
- iconName, size, this.fullscreenMode
- );
+ let button = (buttonIcon instanceof Gtk.Button)
+ ? buttonIcon
+ : new Buttons.IconButton(buttonIcon);
- if(!noPack)
- this.pack_start(button, false, false, 0);
-
- this.buttonsArr.push(button);
- return button;
- }
-
- addPopoverButton(iconName, size)
- {
- let button = new Buttons.BoxedPopoverButton(
- iconName, size, this.fullscreenMode
- );
- this.pack_start(button, false, false, 0);
+ this.append(button);
this.buttonsArr.push(button);
return button;
}
- addRadioButtons(box, array, activeId)
+ addLabelButton(text)
+ {
+ text = text || '';
+ let button = new Buttons.LabelButton(text);
+
+ return this.addButton(button);
+ }
+
+ addPopoverButton(iconName)
+ {
+ let button = new Buttons.PopoverButton(iconName);
+
+ return this.addButton(button);
+ }
+
+ addCheckButtons(box, array, activeId)
{
let group = null;
- let children = box.get_children();
- let lastEl = (children.length > array.length)
- ? children.length
- : array.length;
+ let child = box.get_first_child();
+ let i = 0;
- for(let i = 0; i < lastEl; i++) {
+ while(child || i < array.length) {
if(i >= array.length) {
- children[i].hide();
- debug(`hiding unused ${children[i].type} radioButton nr: ${i}`);
+ child.hide();
+ debug(`hiding unused ${child.type} checkButton nr: ${i}`);
+ i++;
+ child = child.get_next_sibling();
continue;
}
let el = array[i];
- let radioButton;
+ let checkButton;
- if(i < children.length) {
- radioButton = children[i];
- debug(`reusing ${el.type} radioButton nr: ${i}`);
+ if(child) {
+ checkButton = child;
+ debug(`reusing ${el.type} checkButton nr: ${i}`);
}
else {
- debug(`creating new ${el.type} radioButton nr: ${i}`);
- radioButton = new Gtk.RadioButton({
+ debug(`creating new ${el.type} checkButton nr: ${i}`);
+ checkButton = new Gtk.CheckButton({
group: group,
});
- radioButton.connect(
+ checkButton.connect(
'toggled',
- this._onRadioButtonToggled.bind(this, radioButton)
+ this._onCheckButtonToggled.bind(this, checkButton)
);
- this.setDefaultWidgetBehaviour(radioButton);
- box.add(radioButton);
+ box.append(checkButton);
}
- radioButton.label = el.label;
- debug(`radioButton label: ${radioButton.label}`);
- radioButton.type = el.type;
- debug(`radioButton type: ${radioButton.type}`);
- radioButton.activeId = el.activeId;
- debug(`radioButton id: ${radioButton.activeId}`);
+ checkButton.label = el.label;
+ debug(`checkButton label: ${checkButton.label}`);
+ checkButton.type = el.type;
+ debug(`checkButton type: ${checkButton.type}`);
+ checkButton.activeId = el.activeId;
+ debug(`checkButton id: ${checkButton.activeId}`);
- if(radioButton.activeId === activeId) {
- radioButton.set_active(true);
- debug(`activated ${el.type} radioButton nr: ${i}`);
+ if(checkButton.activeId === activeId) {
+ checkButton.set_active(true);
+ debug(`activated ${el.type} checkButton nr: ${i}`);
}
if(!group)
- group = radioButton;
+ group = checkButton;
- radioButton.show();
+ i++;
+ if(child)
+ child = child.get_next_sibling();
}
}
- setDefaultWidgetBehaviour(widget)
- {
- widget.can_focus = false;
- widget.can_default = false;
- }
-
- setVolumeMarks(isAdded)
- {
- if(!isAdded)
- return this.volumeScale.clear_marks();
-
- this.volumeScale.add_mark(0, Gtk.PositionType.LEFT, '0%');
- this.volumeScale.add_mark(1, Gtk.PositionType.LEFT, '100%');
- this.volumeScale.add_mark(2, Gtk.PositionType.LEFT, '200%');
- }
-
handleScaleIncrement(type, isUp)
{
let value = this[`${type}Scale`].get_value();
@@ -206,49 +174,45 @@ var Controls = GObject.registerClass({
_addTogglePlayButton()
{
- this.togglePlayButton = this.addButton(
+ this.togglePlayButton = new Buttons.IconToggleButton(
'media-playback-start-symbolic',
- Gtk.IconSize.LARGE_TOOLBAR
+ 'media-playback-pause-symbolic'
);
- this.togglePlayButton.setPlayImage = () =>
- {
- this.togglePlayButton.image.set_from_icon_name(
- 'media-playback-start-symbolic',
- this.togglePlayButton.image.icon_size
- );
- }
- this.togglePlayButton.setPauseImage = () =>
- {
- this.togglePlayButton.image.set_from_icon_name(
- 'media-playback-pause-symbolic',
- this.togglePlayButton.image.icon_size
- );
- }
+ this.togglePlayButton.add_css_class('playbackicon');
+ this.addButton(this.togglePlayButton);
}
_addPositionScale()
{
+ this.elapsedButton = this.addLabelButton(this.elapsedInitial);
this.positionScale = new Gtk.Scale({
orientation: Gtk.Orientation.HORIZONTAL,
value_pos: Gtk.PositionType.LEFT,
- draw_value: true,
+ draw_value: false,
hexpand: true,
+ valign: Gtk.Align.CENTER,
+ can_focus: false,
});
- let style = this.positionScale.get_style_context();
- style.add_class('positionscale');
- this.positionScale.connect(
- 'format-value', this._onPositionScaleFormatValue.bind(this)
+ this.togglePlayButton.bind_property('margin_top',
+ this.positionScale, 'margin_top', GObject.BindingFlags.SYNC_CREATE
);
- this.positionScale.connect(
- 'button-press-event', this._onPositionScaleButtonPressEvent.bind(this)
+ this.togglePlayButton.bind_property('margin_bottom',
+ this.positionScale, 'margin_bottom', GObject.BindingFlags.SYNC_CREATE
);
+
+ this.positionScale.add_css_class('positionscale');
+ this.positionScale.connect('value-changed', this._onPositionScaleValueChanged.bind(this));
+
+ /* GTK4 is missing pressed/released signals for GtkRange/GtkScale.
+ * We cannot add controllers, cause it already has them, so we
+ * workaround this by observing css classes it currently has */
this.positionScale.connect(
- 'button-release-event', this._onPositionScaleButtonReleaseEvent.bind(this)
+ 'notify::css-classes', this._onPositionScaleDragging.bind(this)
);
this.positionAdjustment = this.positionScale.get_adjustment();
- this.pack_start(this.positionScale, true, true, 0);
+ this.append(this.positionScale);
}
_addVolumeButton()
@@ -256,10 +220,14 @@ var Controls = GObject.registerClass({
this.volumeButton = this.addPopoverButton(
'audio-volume-muted-symbolic'
);
- this.volumeButton.add_events(Gdk.EventMask.SCROLL_MASK);
- this.volumeButton.connect(
- 'scroll-event', (self, event) => this._onScrollEvent(event)
+ let scrollController = new Gtk.EventControllerScroll();
+ scrollController.set_flags(
+ Gtk.EventControllerScrollFlags.VERTICAL
+ | Gtk.EventControllerScrollFlags.DISCRETE
);
+ scrollController.connect('scroll', this._onScroll.bind(this));
+ this.volumeButton.add_controller(scrollController);
+
this.volumeScale = new Gtk.Scale({
orientation: Gtk.Orientation.VERTICAL,
inverted: true,
@@ -268,18 +236,18 @@ var Controls = GObject.registerClass({
round_digits: 2,
vexpand: true,
});
- this.volumeScale.get_style_context().add_class('volumescale');
+ this.volumeScale.add_css_class('volumescale');
this.volumeAdjustment = this.volumeScale.get_adjustment();
this.volumeAdjustment.set_upper(2);
this.volumeAdjustment.set_step_increment(0.05);
this.volumeAdjustment.set_page_increment(0.05);
- this.setDefaultWidgetBehaviour(this.volumeScale);
- this.volumeButton.popoverBox.add(this.volumeScale);
- this.volumeButton.popoverBox.show_all();
-
- this.setVolumeMarks(true);
+ for(let i = 0; i <= 2; i++) {
+ let text = (i) ? `${i}00%` : '0%';
+ this.volumeScale.add_mark(i, Gtk.PositionType.LEFT, text);
+ }
+ this.volumeButton.popoverBox.append(this.volumeScale);
}
_getFormatedTime(time)
@@ -293,25 +261,25 @@ var Controls = GObject.registerClass({
return `${hours}:${minutes}:${seconds}`;
}
- _onRadioButtonToggled(self, radioButton)
+ _onCheckButtonToggled(self, checkButton)
{
- if(!radioButton.get_active())
+ if(!checkButton.get_active())
return;
- switch(radioButton.type) {
+ switch(checkButton.type) {
case 'video':
case 'audio':
case 'subtitle':
this.emit(
'track-change-requested',
- radioButton.type,
- radioButton.activeId
+ checkButton.type,
+ checkButton.activeId
);
break;
case 'visualization':
this.emit(
- `${radioButton.type}-change-requested`,
- radioButton.activeId
+ `${checkButton.type}-change-requested`,
+ checkButton.activeId
);
break;
default:
@@ -319,25 +287,26 @@ var Controls = GObject.registerClass({
}
}
- _onPositionScaleFormatValue(self, value)
+ _onPositionScaleValueChanged()
{
- return this._getFormatedTime(value)
+ let elapsed = this._getFormatedTime(this.positionScale.get_value())
+ '/' + this.durationFormated;
+
+ this.elapsedButton.set_label(elapsed);
}
- _onPositionScaleButtonPressEvent()
+ _onPositionScaleDragging(scale)
{
- this.isPositionSeeking = true;
+ let isPositionSeeking = scale.has_css_class('dragging');
+
+ if(this.isPositionSeeking === isPositionSeeking)
+ return;
+
+ this.isPositionSeeking = isPositionSeeking;
this.emit('position-seeking-changed', this.isPositionSeeking);
}
- _onPositionScaleButtonReleaseEvent()
- {
- this.isPositionSeeking = false;
- this.emit('position-seeking-changed', this.isPositionSeeking);
- }
-
- _onControlsRealize()
+ _onRealize()
{
this.disconnect(this.realizeSignal);
@@ -352,27 +321,25 @@ var Controls = GObject.registerClass({
this[`${name}Button`].hide();
}
- _onScrollEvent(event)
+ _onScroll(controller, dx, dy)
{
- let [res, direction] = event.get_scroll_direction();
- if(!res) return;
+ let isVertical = Math.abs(dy) >= Math.abs(dx);
+ let isIncrease = (isVertical) ? dy < 0 : dx < 0;
+ let type = (isVertical) ? 'volume' : 'position';
- let type = 'volume';
+ this.handleScaleIncrement(type, isIncrease);
- switch(direction) {
- case Gdk.ScrollDirection.RIGHT:
- case Gdk.ScrollDirection.LEFT:
- type = 'position';
- case Gdk.ScrollDirection.UP:
- case Gdk.ScrollDirection.DOWN:
- let isUp = (
- direction === Gdk.ScrollDirection.UP
- || direction === Gdk.ScrollDirection.RIGHT
- );
- this.handleScaleIncrement(type, isUp);
- break;
- default:
- break;
- }
+ return true;
+ }
+
+ _onDestroy()
+ {
+ this.disconnect(this.destroySignal);
+
+ this.visualizationsButton.emit('destroy');
+ this.videoTracksButton.emit('destroy');
+ this.audioTracksButton.emit('destroy');
+ this.subtitleTracksButton.emit('destroy');
+ this.volumeButton.emit('destroy');
}
});
diff --git a/clapper_src/headerbar.js b/clapper_src/headerbar.js
new file mode 100644
index 00000000..2fdbffa2
--- /dev/null
+++ b/clapper_src/headerbar.js
@@ -0,0 +1,76 @@
+const { GLib, GObject, Gtk, Pango } = imports.gi;
+
+var HeaderBar = GObject.registerClass(
+class ClapperHeaderBar extends Gtk.HeaderBar
+{
+ _init(window, startButtons, endButtons)
+ {
+ super._init({
+ can_focus: false,
+ });
+
+ this.set_title_widget(this._createWidgetForWindow(window));
+ startButtons.forEach(btn => this.pack_start(btn));
+ endButtons.forEach(btn => this.pack_end(btn));
+ }
+
+ updateHeaderBar(mediaInfo)
+ {
+ let title = mediaInfo.get_title();
+ let subtitle = mediaInfo.get_uri() || null;
+
+ if(subtitle && subtitle.startsWith('file://')) {
+ subtitle = GLib.path_get_basename(
+ GLib.filename_from_uri(subtitle)[0]
+ );
+ }
+
+ if(!title) {
+ title = (!subtitle)
+ ? this.defaultTitle
+ : (subtitle.includes('.'))
+ ? subtitle.split('.').slice(0, -1).join('.')
+ : subtitle;
+
+ subtitle = null;
+ }
+
+ this.titleLabel.label = title;
+ this.subtitleLabel.visible = (subtitle !== null);
+
+ if(subtitle)
+ this.subtitleLabel.label = subtitle;
+ }
+
+ _createWidgetForWindow(window)
+ {
+ let box = new Gtk.Box ({
+ orientation: Gtk.Orientation.VERTICAL,
+ valign: Gtk.Align.CENTER,
+ });
+
+ this.titleLabel = new Gtk.Label({
+ halign: Gtk.Align.CENTER,
+ single_line_mode: true,
+ ellipsize: Pango.EllipsizeMode.END,
+ width_chars: 5,
+ });
+ this.titleLabel.add_css_class('title');
+ this.titleLabel.set_parent(box);
+
+ window.bind_property('title', this.titleLabel, 'label',
+ GObject.BindingFlags.SYNC_CREATE
+ );
+
+ this.subtitleLabel = new Gtk.Label({
+ halign: Gtk.Align.CENTER,
+ single_line_mode: true,
+ ellipsize: Pango.EllipsizeMode.END,
+ });
+ this.subtitleLabel.add_css_class('subtitle');
+ this.subtitleLabel.set_parent(box);
+ this.subtitleLabel.visible = false;
+
+ return box;
+ }
+});
diff --git a/clapper_src/interface.js b/clapper_src/interface.js
index d53e593e..602fbf8f 100644
--- a/clapper_src/interface.js
+++ b/clapper_src/interface.js
@@ -19,7 +19,7 @@ class ClapperInterface extends Gtk.Grid
};
Object.assign(this, defaults, opts);
- this.controlsInVideo = false;
+ this.fullscreenMode = false;
this.lastVolumeValue = null;
this.lastPositionValue = 0;
this.lastRevealerEventTime = 0;
@@ -33,29 +33,32 @@ class ClapperInterface extends Gtk.Grid
this.revealerBottom = new Revealers.RevealerBottom();
this.controls = new Controls();
- this.videoBox.get_style_context().add_class('videobox');
- this.videoBox.pack_start(this.overlay, true, true, 0);
+ this.videoBox.add_css_class('videobox');
+ this.videoBox.append(this.overlay);
this.attach(this.videoBox, 0, 0, 1, 1);
this.attach(this.controls, 0, 1, 1, 1);
+
+ this.destroySignal = this.connect('destroy', this._onDestroy.bind(this));
}
addPlayer(player)
{
this._player = player;
- this._player.widget.expand = true;
+ this._player.widget.vexpand = true;
+ this._player.widget.hexpand = true;
this._player.connect('state-changed', this._onPlayerStateChanged.bind(this));
this._player.connect('volume-changed', this._onPlayerVolumeChanged.bind(this));
this._player.connect('duration-changed', this._onPlayerDurationChanged.bind(this));
this._player.connect('position-updated', this._onPlayerPositionUpdated.bind(this));
- this._player.connectWidget(
- 'scroll-event', (self, event) => this.controls._onScrollEvent(event)
+ this._player.scrollController.connect(
+ 'scroll', (ctl, dx, dy) => this.controls._onScroll(ctl, dx, dy)
);
this.controls.togglePlayButton.connect(
'clicked', this._onControlsTogglePlayClicked.bind(this)
);
- this.controls.positionScale.connect(
+ this.scaleSig = this.controls.positionScale.connect(
'value-changed', this._onControlsPositionChanged.bind(this)
);
this.controls.volumeScale.connect(
@@ -70,9 +73,9 @@ class ClapperInterface extends Gtk.Grid
this.controls.connect(
'visualization-change-requested', this._onVisualizationChangeRequested.bind(this)
);
- this.revealerTop.connect('event-after', (self, event) => this._player.widget.event(event));
+ //this.revealerTop.connect('event-after', (self, event) => this._player.widget.event(event));
- this.overlay.add(this._player.widget);
+ this.overlay.set_child(this._player.widget);
this.overlay.add_overlay(this.revealerTop);
this.overlay.add_overlay(this.revealerBottom);
@@ -98,32 +101,33 @@ class ClapperInterface extends Gtk.Grid
this[`revealer${pos}`].showChild(isShow);
}
- setControlsOnVideo(isOnVideo)
+ setFullscreenMode(isFullscreen)
{
- if(this.controlsInVideo === isOnVideo)
+ if(this.fullscreenMode === isFullscreen)
return;
- if(isOnVideo) {
+ if(isFullscreen) {
this.remove(this.controls);
- this.controls.pack_start(this.controls.unfullscreenButton.box, false, false, 0);
- this.revealerBottom.addWidget(this.controls);
+ this.revealerBottom.append(this.controls);
}
else {
- this.revealerBottom.removeWidget(this.controls);
- this.controls.remove(this.controls.unfullscreenButton.box);
+ this.revealerBottom.remove(this.controls);
this.attach(this.controls, 0, 1, 1, 1);
}
- this.controlsInVideo = isOnVideo;
- debug(`placed controls in overlay: ${isOnVideo}`);
+ this.controls.setFullscreenMode(isFullscreen);
+ this.showControls(isFullscreen);
+
+ this.fullscreenMode = isFullscreen;
+ debug(`interface in fullscreen mode: ${isFullscreen}`);
}
updateMediaTracks()
{
let mediaInfo = this._player.get_media_info();
- // set titlebar media title and path
- this.updateHeaderBar(mediaInfo);
+ /* Set titlebar media title and path */
+ this.updateTitles(mediaInfo);
// we can also check if video is "live" or "seekable" (right now unused)
// it might be a good idea to hide position seek bar and disable seeking
@@ -208,7 +212,7 @@ class ClapperInterface extends Gtk.Grid
}
continue;
}
- this.controls.addRadioButtons(
+ this.controls.addCheckButtons(
this.controls[`${type}TracksButton`].popoverBox,
parsedInfo[`${type}Tracks`],
activeId
@@ -220,32 +224,12 @@ class ClapperInterface extends Gtk.Grid
}
}
- updateHeaderBar(mediaInfo)
+ updateTitles(mediaInfo)
{
- if(!this.headerBar)
- return;
+ if(this.headerBar)
+ this.headerBar.updateHeaderBar(mediaInfo);
- let title = mediaInfo.get_title();
- let subtitle = mediaInfo.get_uri() || null;
-
- if(subtitle.startsWith('file://')) {
- subtitle = GLib.filename_from_uri(subtitle)[0];
- subtitle = GLib.path_get_basename(subtitle);
- }
-
- if(!title) {
- title = (!subtitle)
- ? this.defaultTitle
- : (subtitle.includes('.'))
- ? subtitle.split('.').slice(0, -1).join('.')
- : subtitle;
-
- subtitle = null;
- }
-
- this.headerBar.set_title(title);
- this.headerBar.set_subtitle(subtitle);
- this.revealerTop.setMediaTitle(title);
+ this.revealerTop.setMediaTitle(this.headerBar.titleLabel.label);
}
updateTime()
@@ -281,7 +265,7 @@ class ClapperInterface extends Gtk.Grid
});
});
- this.controls.addRadioButtons(
+ this.controls.addCheckButtons(
this.controls.visualizationsButton.popoverBox,
parsedVisArr,
null
@@ -300,8 +284,8 @@ class ClapperInterface extends Gtk.Grid
_onTrackChangeRequested(self, type, activeId)
{
- // reenabling audio is slow (as expected),
- // so it is better to toggle mute instead
+ /* Reenabling audio is slow (as expected),
+ * so it is better to toggle mute instead */
if(type === 'audio') {
if(activeId < 0)
return this._player.set_mute(true);
@@ -313,8 +297,8 @@ class ClapperInterface extends Gtk.Grid
}
if(activeId < 0) {
- // disabling video leaves last frame frozen,
- // so we hide it by making it transparent
+ /* Disabling video leaves last frame frozen,
+ * so we hide it by making it transparent */
if(type === 'video')
this._player.widget.set_opacity(0);
@@ -365,10 +349,10 @@ class ClapperInterface extends Gtk.Grid
case GstPlayer.PlayerState.STOPPED:
this.needsTracksUpdate = true;
case GstPlayer.PlayerState.PAUSED:
- this.controls.togglePlayButton.setPlayImage();
+ this.controls.togglePlayButton.setPrimaryIcon();
break;
case GstPlayer.PlayerState.PLAYING:
- this.controls.togglePlayButton.setPauseImage();
+ this.controls.togglePlayButton.setSecondaryIcon();
if(this.needsTracksUpdate) {
this.needsTracksUpdate = false;
this.updateMediaTracks();
@@ -449,7 +433,7 @@ class ClapperInterface extends Gtk.Grid
this.lastPositionValue = positionSeconds;
this._player.seek_seconds(positionSeconds);
- if(this.controls.fullscreenMode)
+ if(this.fullscreenMode)
this.updateTime();
}
@@ -468,14 +452,10 @@ class ClapperInterface extends Gtk.Grid
: 'overamplified';
let iconName = `audio-volume-${icon}-symbolic`;
-
- if(this.controls.volumeButton.image.icon_name !== iconName)
+ if(this.controls.volumeButton.icon_name !== iconName)
{
debug(`set volume icon: ${icon}`);
- this.controls.volumeButton.image.set_from_icon_name(
- iconName,
- this.controls.volumeButton.image.icon_size
- );
+ this.controls.volumeButton.set_icon_name(iconName);
}
if(volume === this.lastVolumeValue)
@@ -484,4 +464,10 @@ class ClapperInterface extends Gtk.Grid
this.lastVolumeValue = volume;
this._player.set_volume(volume);
}
+
+ _onDestroy()
+ {
+ this.disconnect(this.destroySignal);
+ this.controls.emit('destroy');
+ }
});
diff --git a/clapper_src/player.js b/clapper_src/player.js
index 6592b052..fc59430d 100644
--- a/clapper_src/player.js
+++ b/clapper_src/player.js
@@ -1,4 +1,4 @@
-const { Gio, GLib, GObject, Gst, GstPlayer } = imports.gi;
+const { Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.clapper_src.debug;
@@ -21,7 +21,16 @@ class ClapperPlayer extends GstPlayer.Player
if(!Gst.is_initialized())
Gst.init(null);
- let gtkglsink = Gst.ElementFactory.make('gtkglsink', null);
+ let plugin = 'gtk4glsink';
+ let gtkglsink = Gst.ElementFactory.make(plugin, null);
+
+ if(!gtkglsink) {
+ return debug(new Error(
+ `Could not load "${plugin}".`
+ + ' Do you have gstreamer-plugins-good-gtk4 installed?'
+ ));
+ }
+
let glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtkglsink;
@@ -35,8 +44,7 @@ class ClapperPlayer extends GstPlayer.Player
video_renderer: renderer
});
- // assign elements to player for later access
- // and make sure that GJS will not free them early
+ /* Assign elements to player for later access */
this.gtkglsink = gtkglsink;
this.glsinkbin = glsinkbin;
this.dispatcher = dispatcher;
@@ -60,6 +68,7 @@ class ClapperPlayer extends GstPlayer.Player
this.set_config(config);
this.set_mute(false);
+ this.set_plugin_rank('vah264dec', 300);
this.loop = GLib.MainLoop.new(null, false);
this.run_loop = opts.run_loop || false;
@@ -71,9 +80,28 @@ class ClapperPlayer extends GstPlayer.Player
this._trackId = 0;
this.playlist_ext = opts.playlist_ext || 'claps';
+ this.keyController = new Gtk.EventControllerKey();
+ this.motionController = new Gtk.EventControllerMotion();
+ this.scrollController = new Gtk.EventControllerScroll();
+ this.clickGesture = new Gtk.GestureClick();
+ this.dragGesture = new Gtk.GestureDrag();
+
+ this.scrollController.set_flags(
+ Gtk.EventControllerScrollFlags.BOTH_AXES
+ );
+ this.clickGesture.set_button(0);
+
+ this.widget.add_controller(this.keyController);
+ this.widget.add_controller(this.motionController);
+ this.widget.add_controller(this.scrollController);
+ this.widget.add_controller(this.clickGesture);
+ this.widget.add_controller(this.dragGesture);
+
this.connect('state-changed', this._onStateChanged.bind(this));
this.connect('uri-loaded', this._onUriLoaded.bind(this));
this.connect('end-of-stream', this._onStreamEnded.bind(this));
+ this.connect('warning', this._onPlayerWarning.bind(this));
+ this.connect('error', this._onPlayerError.bind(this));
this.connectWidget('destroy', this._onWidgetDestroy.bind(this));
}
@@ -241,6 +269,16 @@ class ClapperPlayer extends GstPlayer.Player
this.loop.run();
}
+ _onPlayerWarning(self, error)
+ {
+ debug(error.message, 'LEVEL_WARNING');
+ }
+
+ _onPlayerError(self, error)
+ {
+ debug(error);
+ }
+
_onWidgetDestroy()
{
while(this._widgetSignals.length)
diff --git a/clapper_src/revealers.js b/clapper_src/revealers.js
index 020e2d57..6af461a6 100644
--- a/clapper_src/revealers.js
+++ b/clapper_src/revealers.js
@@ -10,6 +10,13 @@ class ClapperCustomRevealer extends Gtk.Revealer
{
_init(opts)
{
+ opts = opts || {};
+
+ let defaults = {
+ visible: false,
+ };
+ Object.assign(opts, defaults);
+
super._init(opts);
this.revealerName = '';
@@ -19,10 +26,7 @@ class ClapperCustomRevealer extends Gtk.Revealer
{
if(isReveal) {
this._clearTimeout();
- if(!this.visible)
- this.show();
-
- this._setShowTimeout();
+ this.set_visible(isReveal);
}
else
this._setHideTimeout();
@@ -30,68 +34,39 @@ class ClapperCustomRevealer extends Gtk.Revealer
this._timedReveal(isReveal, REVEAL_TIME);
}
- show()
- {
- if(this.visible)
- return;
-
- // Decreased size = lower CPU usage
- this._setTopAlign('START');
-
- super.show();
- debug(`showing ${this.revealerName} revealer in drawing area`);
- }
-
- hide()
- {
- if(!this.visible)
- return;
-
- super.hide();
- debug(`removed ${this.revealerName} revealer from drawing area`);
- }
-
showChild(isReveal)
{
this._clearTimeout();
-
- if(isReveal)
- this.show();
- else if(!isReveal)
- this.hide();
-
+ this.set_visible(isReveal);
this._timedReveal(isReveal, 0);
}
+ set_visible(isVisible)
+ {
+ if(this.visible === isVisible)
+ return false;
+
+ super.set_visible(isVisible);
+ debug(`${this.revealerName} revealer visible: ${isVisible}`);
+
+ return true;
+ }
+
_timedReveal(isReveal, time)
{
this.set_transition_duration(time);
this.set_reveal_child(isReveal);
}
- // Drawing revealers on top of video frames
- // increases CPU usage, so we hide them
+ /* Drawing revealers on top of video frames
+ * increases CPU usage, so we hide them */
_setHideTimeout()
{
this._clearTimeout();
- this._setTopAlign('FILL');
this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => {
this._revealerTimeout = null;
- this.hide();
-
- return GLib.SOURCE_REMOVE;
- });
- }
-
- _setShowTimeout()
- {
- this._clearTimeout();
- this._setTopAlign('FILL');
-
- this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => {
- this._revealerTimeout = null;
- this._setTopAlign('START');
+ this.set_visible(false);
return GLib.SOURCE_REMOVE;
});
@@ -105,17 +80,6 @@ class ClapperCustomRevealer extends Gtk.Revealer
GLib.source_remove(this._revealerTimeout);
this._revealerTimeout = null;
}
-
- _setTopAlign(align)
- {
- if(
- this.revealerName !== 'top'
- || this.valign === Gtk.Align[align]
- )
- return;
-
- this.valign = Gtk.Align[align];
- }
});
var RevealerTop = GObject.registerClass(
@@ -130,7 +94,7 @@ class ClapperRevealerTop extends CustomRevealer
});
this.revealerName = 'top';
-
+/*
this.set_events(
Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
@@ -141,7 +105,7 @@ class ClapperRevealerTop extends CustomRevealer
| Gdk.EventMask.ENTER_NOTIFY_MASK
| Gdk.EventMask.LEAVE_NOTIFY_MASK
);
-
+*/
let initTime = GLib.DateTime.new_now_local().format('%X');
this.timeFormat = (initTime.length > 8)
? '%I:%M %p'
@@ -150,13 +114,13 @@ class ClapperRevealerTop extends CustomRevealer
this.revealerGrid = new Gtk.Grid({
column_spacing: 8
});
- let gridContext = this.revealerGrid.get_style_context();
- gridContext.add_class('osd');
- gridContext.add_class('reavealertop');
+ this.revealerGrid.add_css_class('osd');
+ this.revealerGrid.add_css_class('reavealertop');
this.mediaTitle = new Gtk.Label({
ellipsize: Pango.EllipsizeMode.END,
- expand: true,
+ vexpand: true,
+ hexpand: true,
margin_top: 14,
margin_start: 12,
xalign: 0,
@@ -169,17 +133,16 @@ class ClapperRevealerTop extends CustomRevealer
yalign: 0,
};
this.currentTime = new Gtk.Label(timeLabelOpts);
- this.currentTime.get_style_context().add_class('osdtime');
+ this.currentTime.add_css_class('osdtime');
this.endTime = new Gtk.Label(timeLabelOpts);
- this.endTime.get_style_context().add_class('osdendtime');
+ this.endTime.add_css_class('osdendtime');
this.revealerGrid.attach(this.mediaTitle, 0, 0, 1, 1);
this.revealerGrid.attach(this.currentTime, 1, 0, 1, 1);
this.revealerGrid.attach(this.endTime, 1, 0, 1, 1);
- this.add(this.revealerGrid);
- this.revealerGrid.show_all();
+ this.set_child(this.revealerGrid);
}
setMediaTitle(title)
@@ -195,8 +158,8 @@ class ClapperRevealerTop extends CustomRevealer
this.currentTime.set_label(now);
this.endTime.set_label(end);
- // Make sure that next timeout is always run after clock changes,
- // by delaying it for additional few milliseconds
+ /* Make sure that next timeout is always run after clock changes,
+ * by delaying it for additional few milliseconds */
let nextUpdate = 60002 - parseInt(currTime.get_seconds() * 1000);
debug(`updated current time: ${now}`);
@@ -217,19 +180,48 @@ class ClapperRevealerBottom extends CustomRevealer
this.revealerName = 'bottom';
this.revealerBox = new Gtk.Box();
- this.revealerBox.get_style_context().add_class('osd');
+ this.revealerBox.add_css_class('osd');
- this.add(this.revealerBox);
- this.revealerBox.show_all();
+ this.set_child(this.revealerBox);
}
- addWidget(widget)
+ append(widget)
{
- this.revealerBox.pack_start(widget, false, true, 0);
+ this.revealerBox.append(widget);
}
- removeWidget(widget)
+ remove(widget)
{
this.revealerBox.remove(widget);
}
+
+ set_visible(isVisible)
+ {
+ let isChange = super.set_visible(isVisible);
+ if(!isChange) return;
+
+ let parent = this.get_parent();
+ let playerWidget = parent.get_first_child();
+ if(!playerWidget) return;
+
+ if(isVisible) {
+ let box = this.get_first_child();
+ if(!box) return;
+
+ let controls = box.get_first_child();
+ if(!controls) return;
+
+ let togglePlayButton = controls.get_first_child();
+ if(togglePlayButton) {
+ togglePlayButton.grab_focus();
+ debug('focus moved to toggle play button');
+ }
+ playerWidget.set_can_focus(false);
+ }
+ else {
+ playerWidget.set_can_focus(true);
+ playerWidget.grab_focus();
+ debug('focus moved to player widget');
+ }
+ }
});
diff --git a/clapper_src/window.js b/clapper_src/window.js
index 1625f48d..a9228202 100644
--- a/clapper_src/window.js
+++ b/clapper_src/window.js
@@ -13,13 +13,11 @@ var Window = GObject.registerClass({
super._init({
application: application,
title: title || 'Clapper',
- border_width: 0,
resizable: true,
- window_position: Gtk.WindowPosition.CENTER,
- width_request: 960,
- height_request: 642
+ destroy_with_parent: true,
});
this.isFullscreen = false;
+ this.mapSignal = this.connect('map', this._onMap.bind(this));
}
toggleFullscreen()
@@ -28,13 +26,10 @@ var Window = GObject.registerClass({
this[`${un}fullscreen`]();
}
- vfunc_window_state_event(event)
+ _onStateNotify(toplevel)
{
- super.vfunc_window_state_event(event);
-
let isFullscreen = Boolean(
- event.new_window_state
- & Gdk.WindowState.FULLSCREEN
+ toplevel.state & Gdk.ToplevelState.FULLSCREEN
);
if(this.isFullscreen === isFullscreen)
@@ -43,4 +38,12 @@ var Window = GObject.registerClass({
this.isFullscreen = isFullscreen;
this.emit('fullscreen-changed', this.isFullscreen);
}
+
+ _onMap()
+ {
+ this.disconnect(this.mapSignal);
+
+ let surface = this.get_surface();
+ surface.connect('notify::state', this._onStateNotify.bind(this));
+ }
});
diff --git a/css/styles.css b/css/styles.css
index b0332d2d..267c24de 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -7,13 +7,15 @@ scale marks {
font-size: 22px;
font-weight: 500;
}
+.osd .playercontrols {
+ -gtk-icon-size: 24px;
+}
.osd button {
- margin: 2px;
min-width: 36px;
min-height: 36px;
}
.osd scale trough highlight {
- min-width: 6px;
+ min-width: 0px;
min-height: 6px;
}
.osd radio {
@@ -22,12 +24,23 @@ scale marks {
min-width: 18px;
min-height: 18px;
}
+.playbackicon {
+ -gtk-icon-size: 20px;
+}
+.osd .playbackicon {
+ -gtk-icon-size: 28px;
+}
+.labelbutton {
+ font-family: 'Cantarell', 'Noto Sans', sans-serif;
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+}
.videobox {
background: black;
}
.reavealertop {
- min-height: 100px;
- box-shadow: inset 0px 200px 10px -124px rgba(0,0,0,0.4);
+ min-height: 90px;
+ box-shadow: inset 0px 200px 10px -124px rgba(0,0,0,0.3);
font-family: 'Cantarell', 'Noto Sans', sans-serif;
font-size: 30px;
font-weight: 500;
@@ -43,25 +56,13 @@ scale marks {
font-size: 24px;
font-weight: 600;
}
-
-/* Position Scale */
-.positionscale value {
- font-weight: 600;
- color: currentColor;
-}
-.positionscale trough highlight {
- min-height: 4px;
-}
-.osd .positionscale value {
+.osd .labelbutton {
font-size: 24px;
}
-.positionscale contents {
- margin-left: 4px;
- margin-right: 2px;
-}
-.osd .positionscale contents {
- margin-left: 8px;
- margin-right: 2px;
+
+/* Position Scale */
+.positionscale trough highlight {
+ min-height: 4px;
}
.osd .positionscale trough slider {
color: transparent;
@@ -75,12 +76,10 @@ scale marks {
/* Volume Scale */
.volumescale {
- margin-left: 4px;
min-height: 180px;
}
.osd .volumescale {
margin: 6px;
- margin-left: 10px;
min-height: 280px;
}
.volumescale marks label {
@@ -91,3 +90,6 @@ scale marks {
.osd .volumescale marks label {
margin-bottom: -8px;
}
+.osd .volumescale trough highlight {
+ min-width: 6px;
+}
diff --git a/gjs-1.0/clapper.js.in b/gjs-1.0/clapper.js.in
index 91dabb16..e5ba93ca 100644
--- a/gjs-1.0/clapper.js.in
+++ b/gjs-1.0/clapper.js.in
@@ -1,5 +1,5 @@
-imports.gi.versions.Gdk = '3.0';
-imports.gi.versions.Gtk = '3.0';
+imports.gi.versions.Gdk = '4.0';
+imports.gi.versions.Gtk = '4.0';
imports.searchPath.unshift('@importspath@');
const ClapperSrc = imports.clapper_src;
diff --git a/main.js b/main.js
index ce9ffdc1..efa88d3b 100644
--- a/main.js
+++ b/main.js
@@ -1,5 +1,5 @@
-imports.gi.versions.Gdk = '3.0';
-imports.gi.versions.Gtk = '3.0';
+imports.gi.versions.Gdk = '4.0';
+imports.gi.versions.Gtk = '4.0';
const { Gst } = imports.gi;
const { App } = imports.clapper_src.app;
diff --git a/pkgs/arch/PKGBUILD b/pkgs/arch/PKGBUILD
index cfe5f321..c70ad9f2 100644
--- a/pkgs/arch/PKGBUILD
+++ b/pkgs/arch/PKGBUILD
@@ -27,13 +27,13 @@ arch=(any)
url="https://github.com/Rafostar/clapper"
license=("GPL-3.0")
depends=(
- "gtk3>=3.19.4"
+ "gtk4>=3.99.2"
"hicolor-icon-theme"
"gjs"
"gst-plugins-base-libs"
"gst-plugins-good"
- "gst-plugins-bad-libs>=1.16.0"
- "gst-plugin-gtk"
+ "gst-plugins-bad-libs>=1.18.0"
+ "gst-plugin-gtk4"
)
makedepends=(
"meson>=0.50"
@@ -44,7 +44,7 @@ optdepends=(
"gst-libav: Popular video decoders"
"gstreamer-vaapi: Intel/AMD video acceleration"
)
-source=("${pkgname%-git}::git+https://github.com/Rafostar/${pkgname%-git}.git#branch=master")
+source=("${pkgname%-git}::git+https://github.com/Rafostar/${pkgname%-git}.git")
provides=("${pkgname%-git}")
replaces=("${pkgname%-git}")
conflicts=("${pkgname%-git}")
diff --git a/pkgs/deb/debian/control b/pkgs/deb/debian/control
index ddc16735..a8e5ceeb 100644
--- a/pkgs/deb/debian/control
+++ b/pkgs/deb/debian/control
@@ -14,14 +14,14 @@ Build-Depends: debhelper (>= 10),
Package: clapper
Architecture: all
Depends: gjs (>= 1.50),
- gir1.2-gtk-3.0 (>= 3.19),
+ gir1.2-gtk-4.0 (>= 3.99.2),
hicolor-icon-theme,
libgstreamer1.0-0,
gstreamer1.0-plugins-base,
gstreamer1.0-plugins-good,
- gstreamer1.0-plugins-bad (>= 1.16),
+ gstreamer1.0-plugins-bad (>= 1.18),
gstreamer1.0-gl,
- gstreamer1.0-gtk3
+ gstreamer1.0-gtk4
Recommends: gstreamer1.0-libav,
gstreamer1.0-pulseaudio
Suggests: gstreamer-plugins-ugly,
diff --git a/pkgs/rpm/clapper.spec b/pkgs/rpm/clapper.spec
index c002bee0..4ce0e835 100644
--- a/pkgs/rpm/clapper.spec
+++ b/pkgs/rpm/clapper.spec
@@ -19,8 +19,8 @@
%global appname com.github.rafostar.Clapper
-%global gst_version 1.16.0
-%global gtk3_version 3.19.4
+%global gst_version 1.18.0
+%global gtk4_version 3.99.2
Name: clapper
Version: 0.0.0
@@ -39,7 +39,7 @@ BuildRequires: desktop-file-utils
BuildRequires: hicolor-icon-theme
Requires: gjs
-Requires: gtk3 >= %{gtk3_version}
+Requires: gtk4 >= %{gtk4_version}
Requires: hicolor-icon-theme
%if 0%{?suse_version}
@@ -51,7 +51,7 @@ BuildRequires: update-desktop-files
Requires: gstreamer
Requires: gstreamer-plugins-base
Requires: gstreamer-plugins-good
-Requires: gstreamer-plugins-good-gtk
+Requires: gstreamer-plugins-good-gtk4
Requires: gstreamer-plugins-bad
Requires: libgstplayer-1_0-0 >= %{gst_version}
@@ -62,14 +62,12 @@ Recommends: gstreamer-plugins-libav
Suggests: gstreamer-plugins-ugly
# Intel/AMD video acceleration
Suggests: gstreamer-plugins-vaapi
-%endif
-
-%if 0%{?fedora} || 0%{?rhel_version} || 0%{?centos_version}
+%else
BuildRequires: glibc-all-langpacks
Requires: gstreamer1
Requires: gstreamer1-plugins-base
Requires: gstreamer1-plugins-good
-Requires: gstreamer1-plugins-good-gtk
+Requires: gstreamer1-plugins-good-gtk4
# Contains GstPlayer lib
Requires: gstreamer1-plugins-bad-free >= %{gst_version}
@@ -113,7 +111,11 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_datadir}/mime/packages/%{appname}.xml
%changelog
+* Wed Oct 14 2020 Rafostar - 0.0.0-3
+- Update to GTK4
+
* Sat Sep 19 22:02:00 CEST 2020 sp1rit - 0.0.0-2
- Added suse_update_desktop_file macro for SuSE packages
+
* Fri Sep 18 2020 Rafostar - 0.0.0-1
- Initial package