From 60234419c299137fd93d3464a207d32c19eeff3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 11:54:50 +0200 Subject: [PATCH 01/10] data: Add DBus service file --- data/com.github.rafostar.Clapper.service.in | 3 +++ data/meson.build | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 data/com.github.rafostar.Clapper.service.in diff --git a/data/com.github.rafostar.Clapper.service.in b/data/com.github.rafostar.Clapper.service.in new file mode 100644 index 00000000..d8057116 --- /dev/null +++ b/data/com.github.rafostar.Clapper.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@app_id@ +Exec=@bindir@/@app_id@ --gapplication-service diff --git a/data/meson.build b/data/meson.build index 44adfd86..e436efe5 100644 --- a/data/meson.build +++ b/data/meson.build @@ -32,3 +32,15 @@ gnome.compile_resources('com.github.rafostar.Clapper.data', install: true, install_dir: pkgdatadir, ) + +dbus_conf = configuration_data() +dbus_conf.set('app_id', meson.project_name()) +dbus_conf.set('bindir', bindir) + +configure_file( + input: 'com.github.rafostar.Clapper.service.in', + output: 'com.github.rafostar.Clapper.service', + configuration: dbus_conf, + install: true, + install_dir: join_paths(datadir, 'dbus-1', 'services'), +) From 306505dc4df7df0a0bd57babc38b31317bd50e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 11:55:15 +0200 Subject: [PATCH 02/10] data: Add missing desktop file options Mark as DBusActivatable and enable StartupNotify --- data/com.github.rafostar.Clapper.desktop | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/com.github.rafostar.Clapper.desktop b/data/com.github.rafostar.Clapper.desktop index 4dc7e654..d780a30f 100644 --- a/data/com.github.rafostar.Clapper.desktop +++ b/data/com.github.rafostar.Clapper.desktop @@ -6,6 +6,8 @@ Categories=GTK;GNOME;AudioVideo;Player;Video;TV; MimeType=application/claps;application/mpeg4-iod;application/mpeg4-muxcodetable;application/mxf;application/ogg;application/ram;application/sdp;application/streamingmedia;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;application/x-extension-m4a;application/x-extension-mp4;application/x-flac;application/x-flash-video;application/x-matroska;application/x-ogg;application/x-streamingmedia;audio/3gpp;audio/3gpp2;audio/aac;audio/ac3;audio/amr;audio/amr-wb;audio/basic;audio/dv;audio/eac3;audio/flac;audio/m4a;audio/midi;audio/mp1;audio/mp2;audio/mp3;audio/mp4;audio/mpeg;audio/mpegurl;audio/mpg;audio/ogg;audio/opus;audio/scpls;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.rn-realaudio;audio/wav;audio/webm;audio/x-aac;audio/x-aiff;audio/x-ape;audio/x-flac;audio/x-gsm;audio/x-it;audio/x-m4a;audio/x-matroska;audio/x-mod;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-mpg;audio/x-ms-asf;audio/x-ms-wma;audio/x-musepack;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-realaudio;audio/x-pn-wav;audio/x-real-audio;audio/x-realaudio;audio/x-s3m;audio/x-scpls;audio/x-shorten;audio/x-speex;audio/x-tta;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-wav;audio/x-wavpack;audio/x-xm;video/3gp;video/3gpp;video/3gpp2;video/divx;video/dv;video/fli;video/flv;video/mp2t;video/mp4;video/mp4v-es;video/mpeg;video/mpeg-system;video/msvideo;video/ogg;video/quicktime;video/vnd.mpegurl;video/vnd.rn-realvideo;video/webm;video/x-avi;video/x-flc;video/x-fli;video/x-flv;video/x-m4v;video/x-matroska;video/x-mpeg;video/x-mpeg-system;video/x-mpeg2;video/x-ms-asf;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-msvideo;video/x-nsv;video/x-ogm+ogg;video/x-theora;video/x-theora+ogg;x-content/audio-cdda;x-content/audio-player;x-content/video-dvd;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtmp;x-scheme-handler/rtp;x-scheme-handler/rtsp; Exec=com.github.rafostar.Clapper %U Icon=com.github.rafostar.Clapper +DBusActivatable=true +StartupNotify=true Terminal=false Type=Application # Translators: Search terms to find this application. Do NOT translate the semicolons! From fce8ec59b87878ceb6aecf8d17971e98e42c0d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 13:46:26 +0200 Subject: [PATCH 03/10] app: Merge code into single subclass We no longer ship the web app that was reusing parts of this code. Combine it into single file without additional subclass. Also put window creation inside first activate code instead of startup in order to fix running app as a gapplication service. --- src/app.js | 102 +++++++++++++++++++++++++++++++++++++++++-------- src/appBase.js | 101 ------------------------------------------------ 2 files changed, 87 insertions(+), 116 deletions(-) delete mode 100644 src/appBase.js diff --git a/src/app.js b/src/app.js index e888cc83..0e6d47da 100644 --- a/src/app.js +++ b/src/app.js @@ -1,27 +1,93 @@ -const { Gio, GObject, Gdk, Gtk } = imports.gi; -const { AppBase } = imports.src.appBase; +const { Gio, GLib, GObject, Gtk } = imports.gi; const { Widget } = imports.src.widget; const Debug = imports.src.debug; +const FileOps = imports.src.fileOps; +const Misc = imports.src.misc; +const Actions = imports.src.actions; const { debug } = Debug; +const { settings } = Misc; var App = GObject.registerClass({ GTypeName: 'ClapperApp', }, -class ClapperApp extends AppBase +class ClapperApp extends Gtk.Application { _init() { - super._init(); + super._init({ + application_id: Misc.appId, + flags: Gio.ApplicationFlags.HANDLES_OPEN, + }); - this.flags |= Gio.ApplicationFlags.HANDLES_OPEN; + this.doneFirstActivate = false; + this.isFileAppend = false; + this.mapSignal = null; } - vfunc_startup() + vfunc_open(files, hint) { - super.vfunc_startup(); + super.vfunc_open(files, hint); + + this._openFilesAsync(files).then(() => this.activate()).catch(debug); + } + + vfunc_activate() + { + super.vfunc_activate(); + + if(!this.doneFirstActivate) + this._onFirstActivate(); + + this.active_window.present_with_time( + Math.floor(GLib.get_monotonic_time() / 1000) + ); + } + + async _openFilesAsync(files) + { + const urisArr = []; + + for(let file of files) { + const uri = file.get_uri(); + if(!uri.startsWith('file:')) { + urisArr.push(uri); + continue; + } + + /* If file is not a dir its URI will be returned in an array */ + const uris = await FileOps.getDirFilesUrisPromise(file).catch(debug); + if(uris && uris.length) + urisArr.push(...uris); + } + + const [playlist, subs] = Misc.parsePlaylistFiles(urisArr); + const { player } = this.active_window.get_child(); + const action = (this.isFileAppend) ? 'append' : 'set'; + + if(playlist && playlist.length) + player[`${action}_playlist`](playlist); + if(subs) + player.set_subtitles(subs); + + /* Restore default behavior */ + this.isFileAppend = false; + } + + _onFirstActivate() + { + const window = new Gtk.ApplicationWindow({ + application: this, + title: Misc.appName, + }); + + window.add_css_class('adwrounded'); + + if(!settings.get_boolean('render-shadows')) + window.add_css_class('gpufriendly'); + + window.add_css_class('gpufriendlyfs'); - const window = this.active_window; const clapperWidget = new Widget(); const dummyHeaderbar = new Gtk.Box({ can_focus: false, @@ -33,14 +99,20 @@ class ClapperApp extends AppBase window.set_child(clapperWidget); window.set_titlebar(dummyHeaderbar); + for(let name in Actions.actions) { + const simpleAction = new Gio.SimpleAction({ name }); + simpleAction.connect('activate', (action) => + Actions.handleAction(action, window) + ); + this.add_action(simpleAction); + + const accels = Actions.actions[name]; + if(accels) + this.set_accels_for_action(`app.${name}`, accels); + } + this.mapSignal = window.connect('map', this._onWindowMap.bind(this)); - } - - vfunc_open(files, hint) - { - super.vfunc_open(files, hint); - - this._openFilesAsync(files).then(() => this.activate()).catch(debug); + this.doneFirstActivate = true; } _onWindowMap(window) diff --git a/src/appBase.js b/src/appBase.js deleted file mode 100644 index be72d61d..00000000 --- a/src/appBase.js +++ /dev/null @@ -1,101 +0,0 @@ -const { Gio, GLib, GObject, Gtk } = imports.gi; -const Debug = imports.src.debug; -const FileOps = imports.src.fileOps; -const Misc = imports.src.misc; -const Actions = imports.src.actions; - -const { debug } = Debug; -const { settings } = Misc; - -var AppBase = GObject.registerClass({ - GTypeName: 'ClapperAppBase', -}, -class ClapperAppBase extends Gtk.Application -{ - _init() - { - super._init({ - application_id: Misc.appId, - }); - - this.doneFirstActivate = false; - this.isFileAppend = false; - } - - vfunc_startup() - { - super.vfunc_startup(); - - const window = new Gtk.ApplicationWindow({ - application: this, - title: Misc.appName, - }); - - /* FIXME: AFAIK there is no way to detect theme rounded corners. - * Having 2/4 corners rounded in floating mode is not good. */ - window.add_css_class('adwrounded'); - - if(!settings.get_boolean('render-shadows')) - window.add_css_class('gpufriendly'); - - window.add_css_class('gpufriendlyfs'); - } - - vfunc_activate() - { - super.vfunc_activate(); - - if(!this.doneFirstActivate) - this._onFirstActivate(); - - this.active_window.present_with_time( - Math.floor(GLib.get_monotonic_time() / 1000) - ); - } - - async _openFilesAsync(files) - { - const urisArr = []; - - for(let file of files) { - const uri = file.get_uri(); - if(!uri.startsWith('file:')) { - urisArr.push(uri); - continue; - } - - /* If file is not a dir its URI will be returned in an array */ - const uris = await FileOps.getDirFilesUrisPromise(file).catch(debug); - if(uris && uris.length) - urisArr.push(...uris); - } - - const [playlist, subs] = Misc.parsePlaylistFiles(urisArr); - const { player } = this.active_window.get_child(); - const action = (this.isFileAppend) ? 'append' : 'set'; - - if(playlist && playlist.length) - player[`${action}_playlist`](playlist); - if(subs) - player.set_subtitles(subs); - - /* Restore default behavior */ - this.isFileAppend = false; - } - - _onFirstActivate() - { - for(let name in Actions.actions) { - const simpleAction = new Gio.SimpleAction({ name }); - simpleAction.connect('activate', (action) => - Actions.handleAction(action, this.active_window) - ); - this.add_action(simpleAction); - - const accels = Actions.actions[name]; - if(accels) - this.set_accels_for_action(`app.${name}`, accels); - } - this.doneFirstActivate = true; - } -}); From 041deb559cafa1a00667689a89787b19b254bbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 13:50:27 +0200 Subject: [PATCH 04/10] headerbar: Merge code into single subclass We no longer ship the web app that was reusing parts of this code. Combine it into single file without additional subclass. --- src/headerbar.js | 274 +++++++++++++++++++++++++++++++++++++++- src/headerbarBase.js | 288 ------------------------------------------- 2 files changed, 271 insertions(+), 291 deletions(-) delete mode 100644 src/headerbarBase.js diff --git a/src/headerbar.js b/src/headerbar.js index d41a519f..b1551ccf 100644 --- a/src/headerbar.js +++ b/src/headerbar.js @@ -1,11 +1,233 @@ -const { GObject } = imports.gi; -const { HeaderBarBase } = imports.src.headerbarBase; +const { GObject, Gtk } = imports.gi; +const Debug = imports.src.debug; +const Misc = imports.src.misc; + +const { debug } = Debug; var HeaderBar = GObject.registerClass({ GTypeName: 'ClapperHeaderBar', }, -class ClapperHeaderBar extends HeaderBarBase +class ClapperHeaderBar extends Gtk.Box { + _init() + { + super._init({ + can_focus: false, + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 6, + margin_top: 6, + margin_start: 6, + margin_end: 6, + }); + this.add_css_class('osdheaderbar'); + + this.isMaximized = false; + this.isMenuOnLeft = true; + + const uiBuilder = Misc.getBuilderForName('clapper.ui'); + + this.menuWidget = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + valign: Gtk.Align.CENTER, + spacing: 6, + }); + + this.menuButton = new Gtk.MenuButton({ + icon_name: 'open-menu-symbolic', + valign: Gtk.Align.CENTER, + can_focus: false, + }); + const menuToggleButton = this.menuButton.get_first_child(); + menuToggleButton.add_css_class('osd'); + const mainMenuModel = uiBuilder.get_object('mainMenu'); + const mainMenuPopover = new HeaderBarPopover(mainMenuModel); + this.menuButton.set_popover(mainMenuPopover); + this.menuButton.add_css_class('circular'); + this.menuWidget.append(this.menuButton); + + this.extraButtonsBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + valign: Gtk.Align.CENTER, + }); + this.extraButtonsBox.add_css_class('linked'); + + const floatButton = new Gtk.Button({ + icon_name: 'pip-in-symbolic', + can_focus: false, + }); + floatButton.add_css_class('osd'); + floatButton.add_css_class('circular'); + floatButton.add_css_class('linkedleft'); + floatButton.connect('clicked', + this._onFloatButtonClicked.bind(this) + ); + this.extraButtonsBox.append(floatButton); + + const separator = new Gtk.Separator({ + orientation: Gtk.Orientation.VERTICAL, + }); + separator.add_css_class('linkseparator'); + this.extraButtonsBox.append(separator); + + const fullscreenButton = new Gtk.Button({ + icon_name: 'view-fullscreen-symbolic', + can_focus: false, + }); + fullscreenButton.add_css_class('osd'); + fullscreenButton.add_css_class('circular'); + fullscreenButton.add_css_class('linkedright'); + fullscreenButton.connect('clicked', + this._onFullscreenButtonClicked.bind(this) + ); + this.extraButtonsBox.append(fullscreenButton); + this.menuWidget.append(this.extraButtonsBox); + + this.spacerWidget = new Gtk.Box({ + hexpand: true, + }); + + this.minimizeWidget = this._getWindowButton('minimize'); + this.maximizeWidget = this._getWindowButton('maximize'); + this.closeWidget = this._getWindowButton('close'); + + const gtkSettings = Gtk.Settings.get_default(); + this._onLayoutUpdate(gtkSettings); + + gtkSettings.connect( + 'notify::gtk-decoration-layout', + this._onLayoutUpdate.bind(this) + ); + } + + setMenuOnLeft(isOnLeft) + { + if(this.isMenuOnLeft === isOnLeft) + return; + + if(isOnLeft) { + this.menuWidget.reorder_child_after( + this.extraButtonsBox, this.menuButton + ); + } + else { + this.menuWidget.reorder_child_after( + this.menuButton, this.extraButtonsBox + ); + } + + this.isMenuOnLeft = isOnLeft; + } + + setMaximized(isMaximized) + { + if(this.isMaximized === isMaximized) + return; + + this.maximizeWidget.icon_name = (isMaximized) + ? 'window-restore-symbolic' + : 'window-maximize-symbolic'; + + this.isMaximized = isMaximized; + } + + _onLayoutUpdate(gtkSettings) + { + const gtkLayout = gtkSettings.gtk_decoration_layout; + + this._replaceButtons(gtkLayout); + } + + _replaceButtons(gtkLayout) + { + const modLayout = gtkLayout.replace(':', ',spacer,'); + const layoutArr = modLayout.split(','); + + let lastWidget = null; + + let showMinimize = false; + let showMaximize = false; + let showClose = false; + + let menuAdded = false; + let spacerAdded = false; + + debug(`headerbar layout: ${modLayout}`); + + for(let name of layoutArr) { + /* Menu might be named "appmenu" */ + if(!menuAdded && (!name || name === 'appmenu' || name === 'icon')) + name = 'menu'; + + const widget = this[`${name}Widget`]; + if(!widget) continue; + + if(!widget.parent) + this.append(widget); + else + this.reorder_child_after(widget, lastWidget); + + switch(name) { + case 'spacer': + spacerAdded = true; + break; + case 'minimize': + showMinimize = true; + break; + case 'maximize': + showMaximize = true; + break; + case 'close': + showClose = true; + break; + case 'menu': + this.setMenuOnLeft(!spacerAdded); + menuAdded = true; + break; + default: + break; + } + + lastWidget = widget; + } + + this.minimizeWidget.visible = showMinimize; + this.maximizeWidget.visible = showMaximize; + this.closeWidget.visible = showClose; + } + + _getWindowButton(name) + { + const button = new Gtk.Button({ + icon_name: `window-${name}-symbolic`, + valign: Gtk.Align.CENTER, + can_focus: false, + }); + button.add_css_class('osd'); + button.add_css_class('circular'); + + if(name === 'maximize') + name = 'toggle-maximized'; + + button.connect('clicked', + this._onWindowButtonActivate.bind(this, name) + ); + + return button; + } + + _updateFloatIcon(isFloating) + { + const floatButton = this.extraButtonsBox.get_first_child(); + if(!floatButton) return; + + const iconName = (isFloating) + ? 'pip-out-symbolic' + : 'pip-in-symbolic'; + + if(floatButton.icon_name !== iconName) + floatButton.icon_name = iconName; + } + _onWindowButtonActivate(action) { this.activate_action(`window.${action}`, null); @@ -29,3 +251,49 @@ class ClapperHeaderBar extends HeaderBarBase this.root.fullscreen(); } }); + +var HeaderBarPopover = GObject.registerClass({ + GTypeName: 'ClapperHeaderBarPopover', +}, +class ClapperHeaderBarPopover extends Gtk.PopoverMenu +{ + _init(model) + { + super._init({ + menu_model: model, + }); + + this.connect('map', this._onMap.bind(this)); + this.connect('closed', this._onClosed.bind(this)); + } + + _onMap() + { + const { child } = this.root; + + if( + !child + || !child.player + || !child.player.widget + ) + return; + + child.revealControls(); + child.isPopoverOpen = true; + } + + _onClosed() + { + const { child } = this.root; + + if( + !child + || !child.player + || !child.player.widget + ) + return; + + child.revealControls(); + child.isPopoverOpen = false; + } +}); diff --git a/src/headerbarBase.js b/src/headerbarBase.js deleted file mode 100644 index 46950070..00000000 --- a/src/headerbarBase.js +++ /dev/null @@ -1,288 +0,0 @@ -const { GObject, Gtk } = imports.gi; -const Debug = imports.src.debug; -const Misc = imports.src.misc; - -const { debug } = Debug; - -var HeaderBarBase = GObject.registerClass({ - GTypeName: 'ClapperHeaderBarBase', -}, -class ClapperHeaderBarBase extends Gtk.Box -{ - _init() - { - super._init({ - can_focus: false, - orientation: Gtk.Orientation.HORIZONTAL, - spacing: 6, - margin_top: 6, - margin_start: 6, - margin_end: 6, - }); - this.add_css_class('osdheaderbar'); - - this.isMaximized = false; - this.isMenuOnLeft = true; - - const uiBuilder = Misc.getBuilderForName('clapper.ui'); - - this.menuWidget = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - valign: Gtk.Align.CENTER, - spacing: 6, - }); - - this.menuButton = new Gtk.MenuButton({ - icon_name: 'open-menu-symbolic', - valign: Gtk.Align.CENTER, - can_focus: false, - }); - const menuToggleButton = this.menuButton.get_first_child(); - menuToggleButton.add_css_class('osd'); - const mainMenuModel = uiBuilder.get_object('mainMenu'); - const mainMenuPopover = new HeaderBarPopover(mainMenuModel); - this.menuButton.set_popover(mainMenuPopover); - this.menuButton.add_css_class('circular'); - this.menuWidget.append(this.menuButton); - - this.extraButtonsBox = new Gtk.Box({ - orientation: Gtk.Orientation.HORIZONTAL, - valign: Gtk.Align.CENTER, - }); - this.extraButtonsBox.add_css_class('linked'); - - const floatButton = new Gtk.Button({ - icon_name: 'pip-in-symbolic', - can_focus: false, - }); - floatButton.add_css_class('osd'); - floatButton.add_css_class('circular'); - floatButton.add_css_class('linkedleft'); - floatButton.connect('clicked', - this._onFloatButtonClicked.bind(this) - ); - this.extraButtonsBox.append(floatButton); - - const separator = new Gtk.Separator({ - orientation: Gtk.Orientation.VERTICAL, - }); - separator.add_css_class('linkseparator'); - this.extraButtonsBox.append(separator); - - const fullscreenButton = new Gtk.Button({ - icon_name: 'view-fullscreen-symbolic', - can_focus: false, - }); - fullscreenButton.add_css_class('osd'); - fullscreenButton.add_css_class('circular'); - fullscreenButton.add_css_class('linkedright'); - fullscreenButton.connect('clicked', - this._onFullscreenButtonClicked.bind(this) - ); - this.extraButtonsBox.append(fullscreenButton); - this.menuWidget.append(this.extraButtonsBox); - - this.spacerWidget = new Gtk.Box({ - hexpand: true, - }); - - this.minimizeWidget = this._getWindowButton('minimize'); - this.maximizeWidget = this._getWindowButton('maximize'); - this.closeWidget = this._getWindowButton('close'); - - const gtkSettings = Gtk.Settings.get_default(); - this._onLayoutUpdate(gtkSettings); - - gtkSettings.connect( - 'notify::gtk-decoration-layout', - this._onLayoutUpdate.bind(this) - ); - } - - setMenuOnLeft(isOnLeft) - { - if(this.isMenuOnLeft === isOnLeft) - return; - - if(isOnLeft) { - this.menuWidget.reorder_child_after( - this.extraButtonsBox, this.menuButton - ); - } - else { - this.menuWidget.reorder_child_after( - this.menuButton, this.extraButtonsBox - ); - } - - this.isMenuOnLeft = isOnLeft; - } - - setMaximized(isMaximized) - { - if(this.isMaximized === isMaximized) - return; - - this.maximizeWidget.icon_name = (isMaximized) - ? 'window-restore-symbolic' - : 'window-maximize-symbolic'; - - this.isMaximized = isMaximized; - } - - _onLayoutUpdate(gtkSettings) - { - const gtkLayout = gtkSettings.gtk_decoration_layout; - - this._replaceButtons(gtkLayout); - } - - _replaceButtons(gtkLayout) - { - const modLayout = gtkLayout.replace(':', ',spacer,'); - const layoutArr = modLayout.split(','); - - let lastWidget = null; - - let showMinimize = false; - let showMaximize = false; - let showClose = false; - - let menuAdded = false; - let spacerAdded = false; - - debug(`headerbar layout: ${modLayout}`); - - for(let name of layoutArr) { - /* Menu might be named "appmenu" */ - if(!menuAdded && (!name || name === 'appmenu' || name === 'icon')) - name = 'menu'; - - const widget = this[`${name}Widget`]; - if(!widget) continue; - - if(!widget.parent) - this.append(widget); - else - this.reorder_child_after(widget, lastWidget); - - switch(name) { - case 'spacer': - spacerAdded = true; - break; - case 'minimize': - showMinimize = true; - break; - case 'maximize': - showMaximize = true; - break; - case 'close': - showClose = true; - break; - case 'menu': - this.setMenuOnLeft(!spacerAdded); - menuAdded = true; - break; - default: - break; - } - - lastWidget = widget; - } - - this.minimizeWidget.visible = showMinimize; - this.maximizeWidget.visible = showMaximize; - this.closeWidget.visible = showClose; - } - - _getWindowButton(name) - { - const button = new Gtk.Button({ - icon_name: `window-${name}-symbolic`, - valign: Gtk.Align.CENTER, - can_focus: false, - }); - button.add_css_class('osd'); - button.add_css_class('circular'); - - if(name === 'maximize') - name = 'toggle-maximized'; - - button.connect('clicked', - this._onWindowButtonActivate.bind(this, name) - ); - - return button; - } - - _updateFloatIcon(isFloating) - { - const floatButton = this.extraButtonsBox.get_first_child(); - if(!floatButton) return; - - const iconName = (isFloating) - ? 'pip-out-symbolic' - : 'pip-in-symbolic'; - - if(floatButton.icon_name !== iconName) - floatButton.icon_name = iconName; - } - - _onWindowButtonActivate(action) - { - } - - _onFloatButtonClicked(button) - { - } - - _onFullscreenButtonClicked(button) - { - } -}); - -var HeaderBarPopover = GObject.registerClass({ - GTypeName: 'ClapperHeaderBarPopover', -}, -class ClapperHeaderBarPopover extends Gtk.PopoverMenu -{ - _init(model) - { - super._init({ - menu_model: model, - }); - - this.connect('map', this._onMap.bind(this)); - this.connect('closed', this._onClosed.bind(this)); - } - - _onMap() - { - const { child } = this.root; - - if( - !child - || !child.player - || !child.player.widget - ) - return; - - child.revealControls(); - child.isPopoverOpen = true; - } - - _onClosed() - { - const { child } = this.root; - - if( - !child - || !child.player - || !child.player.widget - ) - return; - - child.revealControls(); - child.isPopoverOpen = false; - } -}); From 5f6c0922c097660e2b578a9c0354f4347c8aa622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 15:02:25 +0200 Subject: [PATCH 05/10] plugin: Clear paintable importer in dispose Remove our ref on importer a little earlier, with a lock taken --- lib/gst/plugin/gstclapperpaintable.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/gst/plugin/gstclapperpaintable.c b/lib/gst/plugin/gstclapperpaintable.c index 289a07a4..ebd756a5 100644 --- a/lib/gst/plugin/gstclapperpaintable.c +++ b/lib/gst/plugin/gstclapperpaintable.c @@ -84,6 +84,10 @@ gst_clapper_paintable_dispose (GObject *object) GST_CLAPPER_PAINTABLE_UNLOCK (self); + GST_CLAPPER_PAINTABLE_IMPORTER_LOCK (self); + gst_clear_object (&self->importer); + GST_CLAPPER_PAINTABLE_IMPORTER_UNLOCK (self); + GST_CALL_PARENT (G_OBJECT_CLASS, dispose, (object)); } @@ -95,7 +99,6 @@ gst_clapper_paintable_finalize (GObject *object) GST_TRACE ("Finalize"); g_weak_ref_clear (&self->widget); - gst_clear_object (&self->importer); g_mutex_clear (&self->lock); g_mutex_clear (&self->importer_lock); From 1bd371dabdd3e5d876ff5c16221c5be929562e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Mon, 23 May 2022 15:32:32 +0200 Subject: [PATCH 06/10] app: Always activate before opening files Make sure app window was created and bring it on top when started with file(s) as args --- src/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index 0e6d47da..94b26685 100644 --- a/src/app.js +++ b/src/app.js @@ -29,7 +29,8 @@ class ClapperApp extends Gtk.Application { super.vfunc_open(files, hint); - this._openFilesAsync(files).then(() => this.activate()).catch(debug); + this.activate(); + this._openFilesAsync(files).catch(debug); } vfunc_activate() From f9e84ac99b53d688e375aea3b6773d8be1f1c613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 24 May 2022 12:02:23 +0200 Subject: [PATCH 07/10] plugin: Improve GL context realization code We only want GLES on EGL display, so check for it one by one instead of always trying to realize GLES with everything that is not a macOS --- .../importers/gstclapperglbaseimporter.c | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/gst/plugin/importers/gstclapperglbaseimporter.c b/lib/gst/plugin/importers/gstclapperglbaseimporter.c index 497ed19a..795739dc 100644 --- a/lib/gst/plugin/importers/gstclapperglbaseimporter.c +++ b/lib/gst/plugin/importers/gstclapperglbaseimporter.c @@ -465,7 +465,8 @@ _realize_gdk_context_with_api (GdkGLContext *gdk_context, GdkGLAPI api) static gboolean gst_clapper_gl_base_importer_gdk_context_realize (GstClapperGLBaseImporter *self, GdkGLContext *gdk_context) { - GdkGLAPI preferred_api; + GdkGLAPI preferred_api = GDK_GL_API_GL; + GdkDisplay *gdk_display; const gchar *gl_env; gboolean success; @@ -483,13 +484,24 @@ gst_clapper_gl_base_importer_gdk_context_realize (GstClapperGLBaseImporter *self return _realize_gdk_context_with_api (gdk_context, preferred_api); } + gdk_display = gdk_gl_context_get_display (gdk_context); + GST_DEBUG_OBJECT (self, "Auto selecting GL API for display: %s", + gdk_display_get_name (gdk_display)); + /* Apple decoder uses rectangle texture-target, which GLES does not support. - * For Linux we prefer GLES in order to get HW colorspace conversion. + * For Linux we prefer EGL + GLES in order to get direct HW colorspace conversion. * Windows will try EGL + GLES setup first and auto fallback to WGL. */ -#if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_MACOS - preferred_api = GDK_GL_API_GL; -#else - preferred_api = GDK_GL_API_GLES; +#if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND + if (GDK_IS_WAYLAND_DISPLAY (gdk_display)) + preferred_api = GDK_GL_API_GLES; +#endif +#if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL + if (GDK_IS_X11_DISPLAY (gdk_display) && gdk_x11_display_get_egl_display (gdk_display)) + preferred_api = GDK_GL_API_GLES; +#endif +#if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WIN32_EGL + if (GDK_IS_WIN32_DISPLAY (gdk_display) && gdk_win32_display_get_egl_display (gdk_display)) + preferred_api = GDK_GL_API_GLES; #endif if (!(success = _realize_gdk_context_with_api (gdk_context, preferred_api))) { From 51619cbd2ac26624a50080b2a71aa30369d9fcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 24 May 2022 12:54:08 +0200 Subject: [PATCH 08/10] plugin: Temporarily try to avoid direct DMABuf import on desktop This tries to avoid "scrambled" image on Linux with Intel GPUs that are mostly used together with x86 CPUs at the expense of using slightly slower non-direct DMABuf import, by not going with GLES by default on EGL. The "GST_GL_API" env can still be used to force one or another context type. We need for GStreamer to be aware of currently used DRM modifier first. On the other hand forcing GL everywhere would break most embedded systems that only work with GLES, so do it for x86 processors only... and pray for it to work. See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1236 --- lib/gst/plugin/importers/gstclapperglbaseimporter.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/gst/plugin/importers/gstclapperglbaseimporter.c b/lib/gst/plugin/importers/gstclapperglbaseimporter.c index 795739dc..207fe0ce 100644 --- a/lib/gst/plugin/importers/gstclapperglbaseimporter.c +++ b/lib/gst/plugin/importers/gstclapperglbaseimporter.c @@ -504,6 +504,16 @@ gst_clapper_gl_base_importer_gdk_context_realize (GstClapperGLBaseImporter *self preferred_api = GDK_GL_API_GLES; #endif + /* FIXME: Remove once GStreamer can handle DRM modifiers. This tries to avoid + * "scrambled" image on Linux with Intel GPUs that are mostly used together with + * x86 CPUs at the expense of using slightly slower non-direct DMABuf import. + * See: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1236 */ +#if GST_CLAPPER_GL_BASE_IMPORTER_HAVE_WAYLAND || GST_CLAPPER_GL_BASE_IMPORTER_HAVE_X11_EGL +#if !defined(HAVE_GST_PATCHES) && (defined(__i386__) || defined(_M_IX86) || defined(__x86_64__) || defined(_M_X64)) + preferred_api = GDK_GL_API_GL; +#endif +#endif + if (!(success = _realize_gdk_context_with_api (gdk_context, preferred_api))) { GdkGLAPI fallback_api; From 3c248250f31f3d6d4f115444d67d7a34c85f081e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 24 May 2022 16:48:00 +0200 Subject: [PATCH 09/10] plugin: Always make sure we have Gdk display Give up creating GL context in an unlikely situation that there is no default display --- lib/gst/plugin/importers/gstclapperglbaseimporter.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/gst/plugin/importers/gstclapperglbaseimporter.c b/lib/gst/plugin/importers/gstclapperglbaseimporter.c index 207fe0ce..cb558030 100644 --- a/lib/gst/plugin/importers/gstclapperglbaseimporter.c +++ b/lib/gst/plugin/importers/gstclapperglbaseimporter.c @@ -109,6 +109,11 @@ retrieve_gl_context_on_main (GstClapperGLBaseImporter *self) gdk_display = gdk_display_get_default (); + if (G_UNLIKELY (!gdk_display)) { + GST_ERROR_OBJECT (self, "Could not retrieve Gdk display"); + return FALSE; + } + if (!(gdk_context = gdk_display_create_gl_context (gdk_display, &error))) { GST_ERROR_OBJECT (self, "Error creating Gdk GL context: %s", error ? error->message : "No error set by Gdk"); From 54e4644236b52989ba989c655a798f1ea7e61a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Tue, 24 May 2022 18:21:40 +0200 Subject: [PATCH 10/10] app: Call window_present without timestamp GTK4 now handles this internally via xdg-activation. To make it work correctly we should stop using custom timestamp so default handlers will be used. --- src/app.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app.js b/src/app.js index 94b26685..4f9e572a 100644 --- a/src/app.js +++ b/src/app.js @@ -1,4 +1,4 @@ -const { Gio, GLib, GObject, Gtk } = imports.gi; +const { Gio, GObject, Gtk } = imports.gi; const { Widget } = imports.src.widget; const Debug = imports.src.debug; const FileOps = imports.src.fileOps; @@ -40,9 +40,7 @@ class ClapperApp extends Gtk.Application if(!this.doneFirstActivate) this._onFirstActivate(); - this.active_window.present_with_time( - Math.floor(GLib.get_monotonic_time() / 1000) - ); + this.active_window.present(); } async _openFilesAsync(files)