diff --git a/README.md b/README.md index a755c5a5..5e0d6dba 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,26 @@ # Clapper -A GNOME media player build using [GJS](https://gitlab.gnome.org/GNOME/gjs) with [GTK4](https://www.gtk.org) toolkit. The media player is using [GStreamer](https://gstreamer.freedesktop.org/) as a media backend and renders everything via [OpenGL](https://www.opengl.org). +A GNOME media player build using [GJS](https://gitlab.gnome.org/GNOME/gjs) with [GTK4](https://www.gtk.org) toolkit. +The media player is using [GStreamer](https://gstreamer.freedesktop.org/) as a media backend and renders everything via [OpenGL](https://www.opengl.org).

-
+
Windowed Mode

-
+
Fullscreen Mode

-
+
Floating Mode

-### WORK IN PROGRESS -This is still early WIP. Many features are not implemented yet and quite a few are still unstable. - ### Features: * [Hardware acceleration](https://github.com/Rafostar/clapper/wiki/Hardware-acceleration) * [Floating mode](https://github.com/Rafostar/clapper/wiki/Floating-mode) -* [Adaptive UI](https://raw.githubusercontent.com/Rafostar/clapper/master/media/screencast-mobile-ui.webm) +* [Adaptive UI](https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-mobile.png) * [Playlist from file](https://github.com/Rafostar/clapper/wiki/Playlists) * Chapters on progress bar diff --git a/css/styles.css b/css/styles.css index 82ca00b4..4e380533 100644 --- a/css/styles.css +++ b/css/styles.css @@ -5,41 +5,93 @@ scale marks { radio { margin-left: -2px; } -.osd popover box { - text-shadow: none; - font-size: 21px; - font-weight: 500; -} -.osd button { - min-width: 32px; - min-height: 32px; - -gtk-icon-shadow: none; -} -.osd radio { - margin-left: 0px; - margin-right: 4px; - border: 2px solid; - min-width: 17px; - min-height: 17px; -} /* Adwaita is missing osd ListBox */ .osd list { background: none; } +.osd list row { + -gtk-icon-shadow: none; +} .gtk402 trough highlight { border-color: transparent; } .gtk402 .osd trough highlight { border-color: inherit; } +.osdheaderbar { + background: transparent; +} +.osdheaderbar button { + border: transparent; +} +.linkseparator { + background: alpha(@borders, 0.75); + min-width: 1px; +} +.linkedleft image { + margin-left: 2px; +} +.linkedright image { + margin-right: 2px; +} +/* Non-osd style for popover menu */ +.menupopover label { + color: @theme_text_color; +} +.menupopover arrow { + background: @theme_base_color; + border-color: @insensitive_base_color; +} +.menupopover contents { + background: @theme_base_color; + border-color: @insensitive_base_color; +} +.adwrounded.csd { + border-radius: 8px; +} +.adwrounded.fullscreen, +.adwrounded.maximized, +.adwrounded.tiled, +.adwrounded.tiled-top, +.adwrounded.tiled-left, +.adwrounded.tiled-right, +.adwrounded.tiled-bottom { + border-radius: 0px; +} +.roundedcorners { + border-radius: 8px; +} -.osd .playercontrols { +.videowidget { + min-width: 336px; + min-height: 189px; +} + +.tvmode popover box { + text-shadow: none; + font-size: 21px; + font-weight: 500; +} +.tvmode button { + min-width: 32px; + min-height: 32px; + -gtk-icon-shadow: none; +} +.tvmode radio { + margin-left: 0px; + margin-right: 4px; + border: 2px solid; + min-width: 17px; + min-height: 17px; +} + +.tvmode .playercontrols { -gtk-icon-size: 24px; } .playbackicon { -gtk-icon-size: 20px; } -.osd .playbackicon { +.tvmode .playbackicon { -gtk-icon-size: 28px; } .labelbutton { @@ -51,32 +103,33 @@ radio { font-variant-numeric: tabular-nums; font-weight: 600; } -.osd .labelbutton { +.tvmode .labelbutton { margin-top: 0px; font-size: 23px; text-shadow: none; } -.reavealertop { - min-height: 88px; - box-shadow: inset 0px 200px 10px -132px rgba(0,0,0,0.4); + +/* Top Revealer */ +.tvmode .revealertopgrid { font-family: 'Cantarell', sans-serif; +} +.tvmode .tvtitle { font-size: 28px; font-weight: 500; text-shadow: none; - background: transparent; } -.osdtime { +.tvtime { margin-top: -2px; - margin-right: -4px; - min-width: 4px; + margin-bottom: -2px; + min-height: 4px; font-size: 38px; font-weight: 700; font-variant-numeric: tabular-nums; } -.osdendtime { - margin-top: 36px; - margin-right: -4px; - min-width: 4px; +.tvendtime { + margin-top: -4px; + margin-bottom: 2px; + min-height: 6px; font-size: 24px; font-weight: 600; font-variant-numeric: tabular-nums; @@ -93,13 +146,16 @@ radio { margin-top: -2px; margin-bottom: -2px; } -.osd .positionscale { +.tvmode .positionscale { margin-top: -1px; } .positionscale trough highlight { min-height: 4px; } -.osd .positionscale trough slider { +.osd .positionscale trough highlight { + min-height: 6px; +} +.tvmode .positionscale trough slider { color: transparent; background: transparent; border-color: transparent; @@ -111,11 +167,11 @@ radio { .positionscale.fine-tune mark indicator { min-height: 6px; } -.osd .positionscale mark indicator { +.tvmode .positionscale mark indicator { min-height: 7px; min-width: 2px; } -.osd .positionscale.fine-tune mark indicator { +.tvmode .positionscale.fine-tune mark indicator { min-height: 7px; min-width: 2px; } @@ -127,17 +183,17 @@ radio { margin-top: 4px; margin-bottom: -6px; } -.osd .positionscale marks.top { +.tvmode .positionscale marks.top { margin-bottom: 2px; } -.osd .positionscale marks.bottom { +.tvmode .positionscale marks.bottom { margin-top: 2px; } -.osd .positionscale trough highlight { +.tvmode .positionscale trough highlight { border-radius: 3px; min-height: 20px; } -.osd .positionscale.fine-tune trough highlight { +.tvmode .positionscale.fine-tune trough highlight { border-radius: 3px; min-height: 20px; } @@ -149,7 +205,7 @@ radio { margin-right: -6px; min-height: 180px; } -.osd .volumescale { +.tvmode .volumescale { margin: 2px; margin-left: -6px; margin-right: -4px; @@ -160,7 +216,7 @@ radio { margin-top: -4px; margin-bottom: -6px; } -.osd .volumescale trough highlight { +.tvmode .volumescale trough highlight { min-width: 6px; } .overamp trough highlight { @@ -168,57 +224,19 @@ radio { } /* Elapsed Popover */ -.osd list row { - -gtk-icon-shadow: none; -} .elapsedpopoverbox { min-width: 260px; } .elapsedpopoverbox box separator { background: @insensitive_fg_color; } -.osd .elapsedpopoverbox { +.tvmode .elapsedpopoverbox { min-width: 360px; } -.osd .speedscale trough highlight { +.tvmode .speedscale trough highlight { min-height: 6px; } -/* Floating Mode */ -.floatingwindow { - border-radius: 8px; -} -.osd.floatingcontrols .playercontrols { - -gtk-icon-size: 16px; -} -.osd.floatingcontrols .playbackicon { - -gtk-icon-size: 20px; -} -.osd.floatingcontrols button { - border-radius: 10px; - min-width: 24px; - min-height: 24px; -} -.osd.floatingcontrols .positionscale { - margin-top: -2px; -} -.osd.floatingcontrols .positionscale trough highlight { - border-radius: 3px; - min-height: 12px; -} -.osd.floatingcontrols .positionscale.fine-tune trough highlight { - border-radius: 3px; - min-height: 12px; -} -.osd.floatingcontrols .positionscale mark indicator { - min-height: 5px; - min-width: 1px; -} -.osd.floatingcontrols .positionscale.fine-tune mark indicator { - min-height: 5px; - min-width: 1px; -} - .narrowbutton { min-width: 8px; } @@ -238,17 +256,12 @@ radio { .chapterlabel { min-width: 32px; } -.osd .chapterlabel { +.tvmode .chapterlabel { min-width: 40px; text-shadow: none; font-size: 21px; font-weight: 500; } -.osd.floatingcontrols .chapterlabel { - font: inherit; - font-size: 100%; - min-width: 32px; -} /* Preferences */ .prefsnotebook grid { diff --git a/data/com.github.rafostar.Clapper.gschema.xml b/data/com.github.rafostar.Clapper.gschema.xml index 93c3e91a..391a1b03 100644 --- a/data/com.github.rafostar.Clapper.gschema.xml +++ b/data/com.github.rafostar.Clapper.gschema.xml @@ -40,6 +40,10 @@ '[]' Data storing unfinished videos resume info + + false + Auto stick floating window to all workspaces + @@ -104,10 +108,6 @@ '[960, 583]' Stores window size to restore on next launch - - '[480, 270]' - Stores floating window size to restore on next launch - 1 Stores last linear volume value to apply on startup diff --git a/data/com.github.rafostar.Clapper.metainfo.xml b/data/com.github.rafostar.Clapper.metainfo.xml index 401b8998..acf89e53 100644 --- a/data/com.github.rafostar.Clapper.metainfo.xml +++ b/data/com.github.rafostar.Clapper.metainfo.xml @@ -4,21 +4,23 @@ CC0-1.0 GPL-3.0-or-later Clapper - Play videos and music + Simple and modern GNOME media player com.github.rafostar.Clapper com.github.rafostar.Clapper.desktop

Clapper is a GNOME media player build using GJS with GTK4 toolkit. - The media player is using GStreamer GstPlayer API as a media backend - and renders everything via OpenGL. Player works natively on both - Xorg and Wayland. It also supports VA-API on AMD/Intel GPUs. + The media player is using GStreamer as a media backend and renders + everything via OpenGL. Player works natively on both Xorg and Wayland. + It also supports VA-API on AMD/Intel GPUs.

The media player has an adaptive GUI. When viewing videos in "Windowed Mode", Clapper will use mostly unmodified GTK widgets to match your OS look nicely. When player enters "Fullscreen Mode" all GUI elements will become darker, bigger - and semi-transparent for your viewing comfort. It also has a "Floating Mode". + and semi-transparent for your viewing comfort. It also has a "Floating Mode" which + displays video only on top of all other windows for a PiP-like viewing experience. + Mobile friendly transitions are also supported.

Rafał Dzięgiel @@ -32,13 +34,16 @@ - https://raw.githubusercontent.com/Rafostar/clapper/master/media/screenshot-windowed-mode.png + https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-windowed.png - https://raw.githubusercontent.com/Rafostar/clapper/master/media/screenshot-fullscreen-mode.png + https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-fullscreen.png - https://raw.githubusercontent.com/Rafostar/clapper/master/media/screenshot-floating-mode.png + https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-floating.png + + + https://raw.githubusercontent.com/wiki/Rafostar/clapper/media/screenshot-mobile.png diff --git a/media/screencast-mobile-ui.webm b/media/screencast-mobile-ui.webm deleted file mode 100644 index c61f4fc5..00000000 Binary files a/media/screencast-mobile-ui.webm and /dev/null differ diff --git a/media/screenshot-floating-mode.png b/media/screenshot-floating-mode.png deleted file mode 100644 index 91db5419..00000000 Binary files a/media/screenshot-floating-mode.png and /dev/null differ diff --git a/media/screenshot-fullscreen-mode.png b/media/screenshot-fullscreen-mode.png deleted file mode 100644 index d46289b2..00000000 Binary files a/media/screenshot-fullscreen-mode.png and /dev/null differ diff --git a/media/screenshot-windowed-mode.png b/media/screenshot-windowed-mode.png deleted file mode 100644 index 2f2b52a4..00000000 Binary files a/media/screenshot-windowed-mode.png and /dev/null differ diff --git a/pkgs/flatpak/com.github.rafostar.Clapper.json b/pkgs/flatpak/com.github.rafostar.Clapper.json index 7abfa073..ae552021 100644 --- a/pkgs/flatpak/com.github.rafostar.Clapper.json +++ b/pkgs/flatpak/com.github.rafostar.Clapper.json @@ -13,6 +13,7 @@ "--share=network", "--device=all", "--filesystem=xdg-videos", + "--talk-name=org.gnome.Shell", "--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0", "--env=GST_VAAPI_ALL_DRIVERS=1" ], diff --git a/pkgs/flatpak/lib/gtk4.json b/pkgs/flatpak/lib/gtk4.json index 6102d6eb..d21f5878 100644 --- a/pkgs/flatpak/lib/gtk4.json +++ b/pkgs/flatpak/lib/gtk4.json @@ -18,7 +18,8 @@ { "type": "git", "url": "https://gitlab.gnome.org/GNOME/gtk.git", - "commit": "ec94ec0286041c16d6df7b496b0760a0ae0885ba" + "tag": "4.1.0", + "commit": "65c38111f958bac66d578e3f81964ca3857105c1" }, { "type": "patch", diff --git a/src/app.js b/src/app.js index b3357061..b27e66f3 100644 --- a/src/app.js +++ b/src/app.js @@ -1,6 +1,5 @@ -const { Gio, GObject } = imports.gi; +const { Gio, GObject, Gtk } = imports.gi; const { AppBase } = imports.src.appBase; -const { HeaderBar } = imports.src.headerbar; const { Widget } = imports.src.widget; const Debug = imports.src.debug; @@ -23,14 +22,20 @@ class ClapperApp extends AppBase { super.vfunc_startup(); - this.active_window.isClapperApp = true; - this.active_window.add_css_class('nobackground'); + const window = this.active_window; + + window.isClapperApp = true; + window.add_css_class('nobackground'); const clapperWidget = new Widget(); - this.active_window.set_child(clapperWidget); + window.set_child(clapperWidget); - const headerBar = new HeaderBar(this.active_window); - this.active_window.set_titlebar(headerBar); + const dummyHeaderbar = new Gtk.Box({ + can_focus: false, + focusable: false, + visible: false, + }); + window.set_titlebar(dummyHeaderbar); } vfunc_open(files, hint) diff --git a/src/appBase.js b/src/appBase.js index bfd4fc39..4f8981c1 100644 --- a/src/appBase.js +++ b/src/appBase.js @@ -95,6 +95,12 @@ class ClapperAppBase extends Gtk.Application const theme = gtkSettings.gtk_theme_name; debug(`user selected theme: ${theme}`); + if( + theme.startsWith('Adwaita') + && !this.active_window.has_css_class('adwrounded') + ) + this.active_window.add_css_class('adwrounded'); + if(!theme.endsWith('-dark')) return; diff --git a/src/buttons.js b/src/buttons.js index 8a7014f6..bc717a0b 100644 --- a/src/buttons.js +++ b/src/buttons.js @@ -18,11 +18,7 @@ class ClapperCustomButton extends Gtk.Button super._init(opts); - this.floatUnaffected = false; - this.wantedVisible = true; this.isFullscreen = false; - this.isFloating = false; - this.add_css_class('flat'); } @@ -43,60 +39,24 @@ class ClapperCustomButton extends Gtk.Button this.isFullscreen = isFullscreen; } - setFloatingMode(isFloating) - { - if(this.isFloating === isFloating) - return; - - this.isFloating = isFloating; - - if(this.floatUnaffected) - return; - - if(isFloating) - super.set_visible(false); - else - super.set_visible(this.wantedVisible); - } - - set_visible(isVisible) - { - this.wantedVisible = isVisible; - - if(this.isFloating && !this.floatUnaffected) - super.set_visible(false); - else - super.set_visible(isVisible); - } - vfunc_clicked() { if(!this.isFullscreen) return; - const { player } = this.get_ancestor(Gtk.Grid); - player._setHideControlsTimeout(); - } -}); - -var IconButton = GObject.registerClass( -class ClapperIconButton extends CustomButton -{ - _init(icon) - { - super._init({ - icon_name: icon, - }); - this.floatUnaffected = true; + const clapperWidget = this.get_ancestor(Gtk.Grid); + clapperWidget.revealControls(); } }); var IconToggleButton = GObject.registerClass( -class ClapperIconToggleButton extends IconButton +class ClapperIconToggleButton extends CustomButton { _init(primaryIcon, secondaryIcon) { - super._init(primaryIcon); + super._init({ + icon_name: primaryIcon, + }); this.primaryIcon = primaryIcon; this.secondaryIcon = secondaryIcon; @@ -114,11 +74,20 @@ class ClapperIconToggleButton extends IconButton }); var PopoverButtonBase = GObject.registerClass( -class ClapperPopoverButtonBase extends CustomButton +class ClapperPopoverButtonBase extends Gtk.ToggleButton { _init() { - super._init(); + super._init({ + margin_top: 4, + margin_bottom: 4, + margin_start: 2, + margin_end: 2, + can_focus: false, + }); + + this.isFullscreen = false; + this.add_css_class('flat'); this.popover = new Gtk.Popover({ position: Gtk.PositionType.TOP, @@ -142,7 +111,16 @@ class ClapperPopoverButtonBase extends CustomButton if(this.isFullscreen === isFullscreen) return; - super.setFullscreenMode(isFullscreen); + this.margin_top = (isFullscreen) ? 5 : 4; + this.margin_start = (isFullscreen) ? 3 : 2; + this.margin_end = (isFullscreen) ? 3 : 2; + this.can_focus = isFullscreen; + + /* Redraw icon after style class change */ + if(this.icon_name) + this.set_icon_name(this.icon_name); + + this.isFullscreen = isFullscreen; this.popover.set_offset(0, -this.margin_top); @@ -154,20 +132,33 @@ class ClapperPopoverButtonBase extends CustomButton this.popover[action + '_css_class'](cssClass); } - vfunc_clicked() + vfunc_toggled() { - super.vfunc_clicked(); + if(!this.active) + return; + + const clapperWidget = this.get_ancestor(Gtk.Grid); + + if(this.isFullscreen) { + clapperWidget.revealControls(); + clapperWidget.isPopoverOpen = true; + } - this.set_state_flags(Gtk.StateFlags.CHECKED, false); this.popover.popup(); } _onClosed() { - const { player } = this.get_ancestor(Gtk.Grid); - player.widget.grab_focus(); + const clapperWidget = this.get_ancestor(Gtk.Grid); - this.unset_state_flags(Gtk.StateFlags.CHECKED); + clapperWidget.player.widget.grab_focus(); + + /* Set again timeout as popover is now closed */ + if(clapperWidget.isFullscreenMode) + clapperWidget.revealControls(); + + clapperWidget.isPopoverOpen = false; + this.active = false; } _onCloseRequest() diff --git a/src/controls.js b/src/controls.js index 9bdfb82a..43d76ade 100644 --- a/src/controls.js +++ b/src/controls.js @@ -28,8 +28,6 @@ class ClapperControls extends Gtk.Box this.currentPosition = 0; this.currentDuration = 0; this.isPositionDragging = false; - - this.isMobileMonitor = false; this.isMobile = false; this.showHours = false; @@ -49,7 +47,6 @@ class ClapperControls extends Gtk.Box 'go-previous-symbolic', 'go-next-symbolic' ); - revealTracksButton.floatUnaffected = false; revealTracksButton.add_css_class('narrowbutton'); this.buttonsArr.push(revealTracksButton); const tracksRevealer = new Revealers.ButtonsRevealer( @@ -92,12 +89,6 @@ class ClapperControls extends Gtk.Box this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this)); this.unfullscreenButton.set_visible(false); - this.unfloatButton = this.addButton( - 'preferences-desktop-remote-desktop-symbolic' - ); - this.unfloatButton.connect('clicked', this._onUnfloatClicked.bind(this)); - this.unfloatButton.set_visible(false); - const keyController = new Gtk.EventControllerKey(); keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this)); keyController.connect('key-released', this._onControlsKeyReleased.bind(this)); @@ -115,16 +106,8 @@ class ClapperControls extends Gtk.Box for(let button of this.buttonsArr) button.setFullscreenMode(isFullscreen); - this.unfullscreenButton.set_visible(isFullscreen); - this.set_can_focus(isFullscreen); - } - - setFloatingMode(isFloating) - { - this.isMobile = null; - - for(let button of this.buttonsArr) - button.setFloatingMode(isFloating); + this.unfullscreenButton.visible = isFullscreen; + this.can_focus = isFullscreen; } setLiveMode(isLive, isSeekable) @@ -149,7 +132,7 @@ class ClapperControls extends Gtk.Box { const button = (buttonIcon instanceof Gtk.Button) ? buttonIcon - : new Buttons.IconButton(buttonIcon); + : new Buttons.CustomButton({ icon_name: buttonIcon }); if(!revealer) this.append(button); @@ -495,13 +478,13 @@ class ClapperControls extends Gtk.Box this.disconnect(this.realizeSignal); this.realizeSignal = null; - const { player } = this.get_ancestor(Gtk.Grid); + const clapperWidget = this.get_ancestor(Gtk.Grid); const scrollController = new Gtk.EventControllerScroll(); scrollController.set_flags( Gtk.EventControllerScrollFlags.VERTICAL | Gtk.EventControllerScrollFlags.DISCRETE ); - scrollController.connect('scroll', player._onScroll.bind(player)); + scrollController.connect('scroll', clapperWidget._onScroll.bind(clapperWidget)); this.volumeButton.add_controller(scrollController); const initialVolume = (settings.get_string('volume-initial') === 'custom') @@ -530,12 +513,6 @@ class ClapperControls extends Gtk.Box root.unfullscreen(); } - _onUnfloatClicked(button) - { - const clapperWidget = this.get_ancestor(Gtk.Grid); - clapperWidget.setFloatingMode(false); - } - _onCheckButtonToggled(checkButton) { if(!checkButton.get_active()) @@ -564,8 +541,8 @@ class ClapperControls extends Gtk.Box _onPositionScaleScroll(controller, dx, dy) { - const { player } = this.get_ancestor(Gtk.Grid); - player._onScroll(controller, dx || dy, 0); + const clapperWidget = this.get_ancestor(Gtk.Grid); + clapperWidget._onScroll(controller, dx || dy, 0); } _onPositionScaleValueChanged(scale) @@ -616,32 +593,40 @@ class ClapperControls extends Gtk.Box { const isPositionDragging = scale.has_css_class('dragging'); - if((this.isPositionDragging = isPositionDragging)) + if(this.isPositionDragging === isPositionDragging) return; - const isChapterSeek = this.chapterPopover.visible; - - if(!isPositionDragging) - this._setChapterVisible(false); - const clapperWidget = this.get_ancestor(Gtk.Grid); if(!clapperWidget) return; + if(clapperWidget.isFullscreenMode) { + clapperWidget.revealControls(); + + if(isPositionDragging) + clapperWidget._clearTimeout('hideControls'); + } + + if((this.isPositionDragging = isPositionDragging)) + return; + const scaleValue = scale.get_value(); + const isChapterSeek = this.chapterPopover.visible; if(!isChapterSeek) { const positionSeconds = Math.round(scaleValue); clapperWidget.player.seek_seconds(positionSeconds); } - else + else { clapperWidget.player.seek_chapter(scaleValue); + this._setChapterVisible(false); + } } /* Only happens when navigating through controls panel */ _onControlsKeyPressed(controller, keyval, keycode, state) { - const { player } = this.get_ancestor(Gtk.Grid); - player._setHideControlsTimeout(); + const clapperWidget = this.get_ancestor(Gtk.Grid); + clapperWidget._setHideControlsTimeout(); } _onControlsKeyReleased(controller, keyval, keycode, state) diff --git a/src/dbus.js b/src/dbus.js new file mode 100644 index 00000000..b2c6bb1f --- /dev/null +++ b/src/dbus.js @@ -0,0 +1,56 @@ +const { Gio } = imports.gi; +const Debug = imports.src.debug; + +const { debug } = Debug; + +const ShellProxyWrapper = Gio.DBusProxy.makeProxyWrapper(` + + + + + + + + +` +); + +let shellProxy = null; + +new ShellProxyWrapper( + Gio.DBus.session, + 'org.gnome.Shell', + '/org/gnome/Shell', + (proxy, err) => { + if(err) { + debug(err); + + return; + } + shellProxy = proxy; + debug('GNOME Shell DBus proxy is ready'); + }, + null, + Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION + | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS +); + +function shellWindowEval(fn, isEnabled) +{ + if(!shellProxy) + return; + + const un = (isEnabled) ? '' : 'un'; + + debug(`changing ${fn}`); + shellProxy.EvalRemote( + `global.display.focus_window.${un}${fn}()`, + (out) => { + const debugMsg = (out[0]) + ? `window ${fn}: ${isEnabled}` + : new Error(out[1]); + + debug(debugMsg); + } + ); +} diff --git a/src/headerbar.js b/src/headerbar.js index 32651366..1635fc45 100644 --- a/src/headerbar.js +++ b/src/headerbar.js @@ -1,28 +1,33 @@ -const { GObject } = imports.gi; +const { GObject, Gtk } = imports.gi; const { HeaderBarBase } = imports.src.headerbarBase; var HeaderBar = GObject.registerClass( class ClapperHeaderBar extends HeaderBarBase { - _init(window) + _init() { - super._init(window); + super._init(); + this.add_css_class('osd'); + this.add_css_class('osdheaderbar'); + } - const clapperWidget = window.get_child(); - clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible', - GObject.BindingFlags.INVERT_BOOLEAN - ); + _onWindowButtonActivate(action) + { + this.activate_action(action, null); } _onFloatButtonClicked() { - const clapperWidget = this.get_prev_sibling(); - clapperWidget.setFloatingMode(true); + const clapperWidget = this.root.child; + + clapperWidget.controlsRevealer.toggleReveal(); + + /* Reset timer to not disappear during click */ + clapperWidget._setHideControlsTimeout(); } _onFullscreenButtonClicked() { - const window = this.get_parent(); - window.fullscreen(); + this.root.fullscreen(); } }); diff --git a/src/headerbarBase.js b/src/headerbarBase.js index dacdfe8c..959d1c3d 100644 --- a/src/headerbarBase.js +++ b/src/headerbarBase.js @@ -1,90 +1,202 @@ -const { GObject, Gtk, Pango } = imports.gi; +const { GObject, Gtk } = imports.gi; const Misc = imports.src.misc; var HeaderBarBase = GObject.registerClass( -class ClapperHeaderBarBase extends Gtk.HeaderBar +class ClapperHeaderBarBase extends Gtk.Box { - _init(window) + _init() { super._init({ can_focus: false, + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 6, + margin_top: 6, + margin_start: 6, + margin_end: 6, }); + this.isMaximized = false; + this.isMenuOnLeft = true; + const clapperPath = Misc.getClapperPath(); const uiBuilder = Gtk.Builder.new_from_file( `${clapperPath}/ui/clapper.ui` ); - this.add_css_class('noborder'); - this.set_title_widget(this._createWidgetForWindow(window)); + this.menuWidget = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + valign: Gtk.Align.CENTER, + spacing: 6, + }); - const mainMenuButton = new Gtk.MenuButton({ + this.menuButton = new Gtk.MenuButton({ icon_name: 'open-menu-symbolic', + valign: Gtk.Align.CENTER, }); const mainMenuModel = uiBuilder.get_object('mainMenu'); const mainMenuPopover = new HeaderBarPopover(mainMenuModel); - mainMenuButton.set_popover(mainMenuPopover); - this.pack_start(mainMenuButton); + mainMenuPopover.add_css_class('menupopover'); + this.menuButton.set_popover(mainMenuPopover); + this.menuButton.add_css_class('circular'); + this.menuWidget.append(this.menuButton); - const buttonsBox = new Gtk.Box({ + this.extraButtonsBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, + valign: Gtk.Align.CENTER, }); - buttonsBox.add_css_class('linked'); + this.extraButtonsBox.add_css_class('linked'); const floatButton = new Gtk.Button({ - icon_name: 'preferences-desktop-remote-desktop-symbolic', + icon_name: 'go-bottom-symbolic', }); - floatButton.connect('clicked', this._onFloatButtonClicked.bind(this)); - buttonsBox.append(floatButton); + 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', }); - fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this)); + 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); - buttonsBox.append(fullscreenButton); - this.pack_start(buttonsBox); + 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) + ); } - updateHeaderBar(title, subtitle) + setMenuOnLeft(isOnLeft) { - this.titleLabel.label = title; - this.subtitleLabel.visible = (subtitle !== null); + if(this.isMenuOnLeft === isOnLeft) + return; - if(subtitle) - this.subtitleLabel.label = subtitle; + if(isOnLeft) { + this.menuWidget.reorder_child_after( + this.extraButtonsBox, this.menuButton + ); + } + else { + this.menuWidget.reorder_child_after( + this.menuButton, this.extraButtonsBox + ); + } + + this.isMenuOnLeft = isOnLeft; } - _createWidgetForWindow(window) + setMaximized(isMaximized) { - const box = new Gtk.Box({ - orientation: Gtk.Orientation.VERTICAL, + 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 spacerAdded = false; + + for(let name of layoutArr) { + 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); + 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, }); + button.add_css_class('circular'); - 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); + const action = (name === 'maximize') + ? 'window.toggle-maximized' + : 'window.' + name; - window.bind_property('title', this.titleLabel, 'label', - GObject.BindingFlags.SYNC_CREATE + button.connect('clicked', + this._onWindowButtonActivate.bind(this, action) ); - 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 button; + } - return box; + _onWindowButtonActivate(action) + { } _onFloatButtonClicked() @@ -105,12 +217,13 @@ class ClapperHeaderBarPopover extends Gtk.PopoverMenu menu_model: model, }); + this.connect('map', this._onMap.bind(this)); this.connect('closed', this._onClosed.bind(this)); } - _onClosed() + _onMap() { - const { child } = this.get_root(); + const { child } = this.root; if( !child @@ -119,6 +232,24 @@ class ClapperHeaderBarPopover extends Gtk.PopoverMenu ) return; + child.revealControls(); + child.isPopoverOpen = true; + } + + _onClosed() + { + const { child } = this.root; + + if( + !child + || !child.player + || !child.player.widget + ) + return; + + child.revealControls(); + child.isPopoverOpen = false; + child.player.widget.grab_focus(); } }); diff --git a/src/player.js b/src/player.js index 7f8086dd..e7c078d2 100644 --- a/src/player.js +++ b/src/player.js @@ -14,10 +14,7 @@ class ClapperPlayer extends PlayerBase { super._init(); - this.cursorInPlayer = false; this.seek_done = true; - this.dragAllowed = false; - this.isWidgetDragging = false; this.doneStartup = false; this.needsFastSeekRestore = false; @@ -25,55 +22,15 @@ class ClapperPlayer extends PlayerBase this.quitOnStop = false; this.needsTocUpdate = true; - this.posX = 0; - this.posY = 0; this.keyPressCount = 0; this._maxVolume = Misc.getLinearValue(Misc.maxVolume); - this._hideCursorTimeout = null; - this._hideControlsTimeout = null; - this._updateTimeTimeout = null; - - const clickGesture = new Gtk.GestureClick(); - clickGesture.set_button(0); - clickGesture.connect('pressed', this._onWidgetPressed.bind(this)); - this.widget.add_controller(clickGesture); - - const dragGesture = new Gtk.GestureDrag(); - dragGesture.connect('drag-update', this._onWidgetDragUpdate.bind(this)); - this.widget.add_controller(dragGesture); - - const swipeGesture = new Gtk.GestureSwipe({ - touch_only: true, - }); - swipeGesture.connect('swipe', this._onWidgetSwipe.bind(this)); - swipeGesture.connect('update', this._onWidgetSwipeUpdate.bind(this)); - this.widget.add_controller(swipeGesture); - const keyController = new Gtk.EventControllerKey(); keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this)); keyController.connect('key-released', this._onWidgetKeyReleased.bind(this)); this.widget.add_controller(keyController); - const scrollController = new Gtk.EventControllerScroll(); - scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); - scrollController.connect('scroll', this._onScroll.bind(this)); - this.widget.add_controller(scrollController); - - const motionController = new Gtk.EventControllerMotion(); - motionController.connect('enter', this._onWidgetEnter.bind(this)); - motionController.connect('leave', this._onWidgetLeave.bind(this)); - motionController.connect('motion', this._onWidgetMotion.bind(this)); - this.widget.add_controller(motionController); - - const dropTarget = new Gtk.DropTarget({ - actions: Gdk.DragAction.COPY, - }); - dropTarget.set_gtypes([GObject.TYPE_STRING]); - dropTarget.connect('drop', this._onDataDrop.bind(this)); - this.widget.add_controller(dropTarget); - 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)); @@ -301,100 +258,20 @@ class ClapperPlayer extends PlayerBase } } - getIsSwipeOk(velocity, otherVelocity) - { - if(!velocity) - return false; - - const absVel = Math.abs(velocity); - - if(absVel < 20 || Math.abs(otherVelocity) * 1.5 >= absVel) - return false; - - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - - return clapperWidget.fullscreenMode; - } - - _setHideCursorTimeout() - { - this._clearTimeout('hideCursor'); - this._hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { - this._hideCursorTimeout = null; - - if(this.cursorInPlayer) { - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - const blankCursor = Gdk.Cursor.new_from_name('none', null); - - this.widget.set_cursor(blankCursor); - clapperWidget.revealerTop.set_cursor(blankCursor); - } - - return GLib.SOURCE_REMOVE; - }); - } - - _setHideControlsTimeout() - { - this._clearTimeout('hideControls'); - this._hideControlsTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => { - this._hideControlsTimeout = null; - - if(this.cursorInPlayer) { - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) { - this._clearTimeout('updateTime'); - clapperWidget.revealControls(false); - } - } - - return GLib.SOURCE_REMOVE; - }); - } - - _setUpdateTimeInterval() - { - this._clearTimeout('updateTime'); - - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - const nextUpdate = clapperWidget.updateTime(); - - if(nextUpdate === null) - return; - - this._updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => { - this._updateTimeTimeout = null; - - if(clapperWidget.fullscreenMode) - this._setUpdateTimeInterval(); - - return GLib.SOURCE_REMOVE; - }); - } - - _clearTimeout(name) - { - if(!this[`_${name}Timeout`]) - return; - - GLib.source_remove(this[`_${name}Timeout`]); - this[`_${name}Timeout`] = null; - - if(name === 'updateTime') - debug('cleared update time interval'); - } - _performCloseCleanup(window) { window.disconnect(this.closeRequestSignal); this.closeRequestSignal = null; const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(!clapperWidget.fullscreenMode) { + + if(!clapperWidget.isFullscreenMode && clapperWidget.controlsRevealer.child_revealed) { const size = window.get_default_size(); - if(size[0] > 0 && size[1] > 0) - clapperWidget._saveWindowSize(size); + if(size[0] > 0 && size[1] > 0) { + settings.set_string('window-size', JSON.stringify(size)); + debug(`saved window size: ${size[0]}x${size[1]}`); + } } /* If "quitOnStop" is set here it means that we are in middle of autoclosing */ if(this.state !== GstClapper.ClapperState.STOPPED && !this.quitOnStop) { @@ -495,7 +372,7 @@ class ClapperPlayer extends PlayerBase if(settings.get_boolean('fullscreen-auto')) { const root = player.widget.get_root(); const clapperWidget = root.get_child(); - if(!clapperWidget.fullscreenMode) { + if(!clapperWidget.isFullscreenMode) { this.playOnFullscreen = true; root.fullscreen(); @@ -557,11 +434,8 @@ class ClapperPlayer extends PlayerBase bool = true; case Gdk.KEY_Left: this.adjust_position(bool); - this._clearTimeout('hideControls'); - if(this.keyPressCount > 1) { - clapperWidget.revealerBottom.set_can_focus(false); - clapperWidget.revealerBottom.revealChild(true); - } + if(this.keyPressCount > 1) + clapperWidget.revealControls(); break; default: break; @@ -581,10 +455,8 @@ class ClapperPlayer extends PlayerBase this.toggle_play(); break; case Gdk.KEY_Return: - if(clapperWidget.fullscreenMode) { + if(clapperWidget.isFullscreenMode) clapperWidget.revealControls(true); - this._setHideControlsTimeout(); - } break; case Gdk.KEY_Right: case Gdk.KEY_Left: @@ -592,7 +464,7 @@ class ClapperPlayer extends PlayerBase clapperWidget.controls.positionScale.get_value() ); this.seek_seconds(value); - this._setHideControlsTimeout(); + clapperWidget._setHideControlsTimeout(); break; case Gdk.KEY_F11: case Gdk.KEY_f: @@ -600,7 +472,7 @@ class ClapperPlayer extends PlayerBase clapperWidget.toggleFullscreen(); break; case Gdk.KEY_Escape: - if(clapperWidget.fullscreenMode) { + if(clapperWidget.isFullscreenMode) { root = this.widget.get_root(); root.unfullscreen(); } @@ -615,193 +487,6 @@ class ClapperPlayer extends PlayerBase } } - _onWidgetPressed(gesture, nPress, x, y) - { - const button = gesture.get_current_button(); - const isDouble = (nPress % 2 == 0); - this.dragAllowed = !isDouble; - - switch(button) { - case Gdk.BUTTON_PRIMARY: - if(isDouble) { - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - clapperWidget.toggleFullscreen(); - } - break; - case Gdk.BUTTON_SECONDARY: - this.toggle_play(); - break; - default: - break; - } - } - - _onWidgetEnter(controller, x, y) - { - this.cursorInPlayer = true; - this.isWidgetDragging = false; - - this._setHideCursorTimeout(); - - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) - this._setHideControlsTimeout(); - } - - _onWidgetLeave(controller) - { - this.cursorInPlayer = false; - - this._clearTimeout('hideCursor'); - this._clearTimeout('hideControls'); - } - - _onWidgetMotion(controller, posX, posY) - { - this.cursorInPlayer = true; - - /* GTK4 sometimes generates motions with same coords */ - if(this.posX === posX && this.posY === posY) - return; - - /* Do not show cursor on small movements */ - if( - Math.abs(this.posX - posX) >= 0.5 - || Math.abs(this.posY - posY) >= 0.5 - ) { - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - const defaultCursor = Gdk.Cursor.new_from_name('default', null); - - this.widget.set_cursor(defaultCursor); - clapperWidget.revealerTop.set_cursor(defaultCursor); - this._setHideCursorTimeout(); - - if(clapperWidget.floatingMode && !clapperWidget.fullscreenMode) { - clapperWidget.revealerBottom.set_can_focus(false); - clapperWidget.revealerBottom.revealChild(true); - this._setHideControlsTimeout(); - } - else if(clapperWidget.fullscreenMode) { - if(!this._updateTimeTimeout) - this._setUpdateTimeInterval(); - - if(!clapperWidget.revealerTop.get_reveal_child()) { - /* Do not grab controls key focus on mouse movement */ - clapperWidget.revealerBottom.set_can_focus(false); - clapperWidget.revealControls(true); - } - this._setHideControlsTimeout(); - } - else { - if(this._hideControlsTimeout) - this._clearTimeout('hideControls'); - if(this._updateTimeTimeout) - this._clearTimeout('updateTime'); - } - } - - this.posX = posX; - this.posY = posY; - } - - _onWidgetDragUpdate(gesture, offsetX, offsetY) - { - if(!this.dragAllowed) - return; - - const clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode) - return; - - const { gtk_double_click_distance } = this.widget.get_settings(); - - if ( - Math.abs(offsetX) > gtk_double_click_distance - || Math.abs(offsetY) > gtk_double_click_distance - ) { - const [isActive, startX, startY] = gesture.get_start_point(); - if(!isActive) return; - - const native = this.widget.get_native(); - if(!native) return; - - let [isShared, winX, winY] = this.widget.translate_coordinates( - native, startX, startY - ); - if(!isShared) return; - - const [nativeX, nativeY] = native.get_surface_transform(); - winX += nativeX; - winY += nativeY; - - this.isWidgetDragging = true; - native.get_surface().begin_move( - gesture.get_device(), - gesture.get_current_button(), - winX, - winY, - gesture.get_current_event_time() - ); - - gesture.reset(); - } - } - - _onWidgetSwipe(gesture, velocityX, velocityY) - { - if(!this.getIsSwipeOk(velocityX, velocityY)) - return; - - this._onScroll(gesture, -velocityX, 0); - } - - _onWidgetSwipeUpdate(gesture, sequence) - { - const [isCalc, velocityX, velocityY] = gesture.get_velocity(); - if(!isCalc) return; - - if(!this.getIsSwipeOk(velocityY, velocityX)) - return; - - const isIncrease = velocityY < 0; - - this.adjust_volume(isIncrease, 0.01); - } - - _onScroll(controller, dx, dy) - { - const isHorizontal = (Math.abs(dx) >= Math.abs(dy)); - const isIncrease = (isHorizontal) ? dx < 0 : dy < 0; - - if(isHorizontal) { - this.adjust_position(isIncrease); - const { controls } = this.widget.get_ancestor(Gtk.Grid); - const value = Math.round(controls.positionScale.get_value()); - this.seek_seconds(value); - } - else - this.adjust_volume(isIncrease); - - return true; - } - - _onDataDrop(dropTarget, value, x, y) - { - const playlist = value.split(/\r?\n/).filter(uri => { - return Gst.uri_is_valid(uri); - }); - - if(!playlist.length) - return false; - - this.set_playlist(playlist); - - const { application } = this.widget.get_root(); - application.activate(); - - return true; - } - _onCloseRequest(window) { this._performCloseCleanup(window); diff --git a/src/playerBase.js b/src/playerBase.js index 5939101e..6dc643ac 100644 --- a/src/playerBase.js +++ b/src/playerBase.js @@ -29,8 +29,7 @@ class ClapperPlayerBase extends GstClapper.Clapper }); this.widget = gtk4plugin.video_sink.widget; - this.widget.vexpand = true; - this.widget.hexpand = true; + this.widget.add_css_class('videowidget'); this.state = GstClapper.ClapperState.STOPPED; this.visualization_enabled = false; diff --git a/src/prefs.js b/src/prefs.js index c780dba7..a28ce983 100644 --- a/src/prefs.js +++ b/src/prefs.js @@ -73,6 +73,9 @@ class ClapperBehaviourPage extends PrefsBase.Grid this.addTitle('Resume'); this.addCheckButton('Ask to resume last unfinished video', 'resume-enabled'); + + this.addTitle('Floating Mode'); + this.addCheckButton('Show on all workspaces', 'floating-stick'); } }); diff --git a/src/revealers.js b/src/revealers.js index 44db5fd4..bbdc7860 100644 --- a/src/revealers.js +++ b/src/revealers.js @@ -1,9 +1,11 @@ const { GLib, GObject, Gtk, Pango } = imports.gi; +const { HeaderBar } = imports.src.headerbar; const Debug = imports.src.debug; - -const REVEAL_TIME = 800; +const DBus = imports.src.dbus; +const Misc = imports.src.misc; const { debug } = Debug; +const { settings } = Misc; var CustomRevealer = GObject.registerClass( class ClapperCustomRevealer extends Gtk.Revealer @@ -15,74 +17,24 @@ class ClapperCustomRevealer extends Gtk.Revealer const defaults = { visible: false, can_focus: false, + transition_duration: 800, }; Object.assign(opts, defaults); super._init(opts); this.revealerName = ''; + this.bind_property('child_revealed', this, 'visible', + GObject.BindingFlags.DEFAULT + ); } revealChild(isReveal) { - if(isReveal) { - this._clearTimeout(); - this.set_visible(isReveal); - } - else - this._setHideTimeout(); + if(isReveal) + this.visible = true; - /* Restore focusability after we are done */ - if(!isReveal) this.set_can_focus(true); - - this._timedReveal(isReveal, REVEAL_TIME); - } - - showChild(isReveal) - { - this._clearTimeout(); - 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 */ - _setHideTimeout() - { - this._clearTimeout(); - - this._revealerTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, REVEAL_TIME + 20, () => { - this._revealerTimeout = null; - this.set_visible(false); - - return GLib.SOURCE_REMOVE; - }); - } - - _clearTimeout() - { - if(!this._revealerTimeout) - return; - - GLib.source_remove(this._revealerTimeout); - this._revealerTimeout = null; + this.reveal_child = isReveal; } }); @@ -92,55 +44,108 @@ class ClapperRevealerTop extends CustomRevealer _init() { super._init({ - transition_duration: REVEAL_TIME, transition_type: Gtk.RevealerTransitionType.CROSSFADE, valign: Gtk.Align.START, }); this.revealerName = 'top'; + this._requestedTransition = this.transition_type; const initTime = GLib.DateTime.new_now_local().format('%X'); this.timeFormat = (initTime.length > 8) ? '%I:%M %p' : '%H:%M'; - this.revealerGrid = new Gtk.Grid({ - column_spacing: 8 - }); - this.revealerGrid.add_css_class('osd'); - this.revealerGrid.add_css_class('reavealertop'); - this.mediaTitle = new Gtk.Label({ ellipsize: Pango.EllipsizeMode.END, - vexpand: true, + halign: Gtk.Align.START, + valign: Gtk.Align.CENTER, + margin_start: 10, + margin_end: 10, + visible: false, + }); + this.mediaTitle.add_css_class('tvtitle'); + + this.currentTime = new Gtk.Label({ + halign: Gtk.Align.END, + valign: Gtk.Align.CENTER, + margin_start: 10, + margin_end: 10, + }); + this.currentTime.add_css_class('tvtime'); + + this.endTime = new Gtk.Label({ + halign: Gtk.Align.END, + valign: Gtk.Align.START, + margin_start: 10, + margin_end: 10, + visible: false, + }); + this.endTime.add_css_class('tvendtime'); + + const revealerBox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + }); + this.headerBar = new HeaderBar(); + revealerBox.append(this.headerBar); + + this.revealerGrid = new Gtk.Grid({ + column_spacing: 4, + margin_top: 8, + margin_start: 8, + margin_end: 8, + visible: false, + }); + this.revealerGrid.add_css_class('revealertopgrid'); + + const topLeftBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + }); + topLeftBox.add_css_class('osd'); + topLeftBox.add_css_class('roundedcorners'); + topLeftBox.append(this.mediaTitle); + + const topSpacerBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, hexpand: true, - margin_top: 14, - margin_start: 12, - xalign: 0, - yalign: 0, }); - const timeLabelOpts = { - margin_end: 10, - xalign: 1, - yalign: 0, - }; - this.currentTime = new Gtk.Label(timeLabelOpts); - this.currentTime.add_css_class('osdtime'); + const topRightBox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + halign: Gtk.Align.END, + }); + topRightBox.add_css_class('osd'); + topRightBox.add_css_class('roundedcorners'); + topRightBox.append(this.currentTime); + topRightBox.append(this.endTime); - timeLabelOpts.visible = false; - this.endTime = new Gtk.Label(timeLabelOpts); - this.endTime.add_css_class('osdendtime'); + this.revealerGrid.attach(topLeftBox, 0, 0, 1, 1); + this.revealerGrid.attach(topSpacerBox, 1, 0, 1, 1); + this.revealerGrid.attach(topRightBox, 2, 0, 1, 2); + revealerBox.append(this.revealerGrid); - 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.set_child(revealerBox); - this.set_child(this.revealerGrid); + this.connect('notify::child-revealed', this._onTopRevealed.bind(this)); } - setMediaTitle(title) + set title(text) { - this.mediaTitle.label = title; + this.mediaTitle.label = text; + } + + get title() + { + return this.mediaTitle.label; + } + + set showTitle(isShow) + { + this.mediaTitle.visible = isShow; + } + + get showTitle() + { + return this.mediaTitle.visible; } setTimes(currTime, endTime) @@ -159,6 +164,31 @@ class ClapperRevealerTop extends CustomRevealer return nextUpdate; } + + setFullscreenMode(isFullscreen, isMobileMonitor) + { + const isTvMode = (isFullscreen && !isMobileMonitor); + + this.headerBar.visible = !isTvMode; + this.revealerGrid.visible = isTvMode; + + this.headerBar.extraButtonsBox.visible = !isFullscreen; + + this._requestedTransition = (isTvMode) + ? Gtk.RevealerTransitionType.SLIDE_DOWN + : Gtk.RevealerTransitionType.CROSSFADE; + + /* Changing transition in middle can have dire consequences, + so change only when not in transition */ + if(this.reveal_child === this.child_revealed) + this.transition_type = this._requestedTransition; + } + + _onTopRevealed() + { + if(this.transition_type !== this._requestedTransition) + this.transition_type = this._requestedTransition; + } }); var RevealerBottom = GObject.registerClass( @@ -167,16 +197,25 @@ class ClapperRevealerBottom extends CustomRevealer _init() { super._init({ - transition_duration: REVEAL_TIME, transition_type: Gtk.RevealerTransitionType.SLIDE_UP, valign: Gtk.Align.END, }); this.revealerName = 'bottom'; - this.revealerBox = new Gtk.Box(); + this.revealerBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + margin_start: 8, + margin_end: 8, + margin_bottom: 8, + }); this.revealerBox.add_css_class('osd'); + this.revealerBox.add_css_class('roundedcorners'); this.set_child(this.revealerBox); + + const motionController = new Gtk.EventControllerMotion(); + motionController.connect('motion', this._onMotion.bind(this)); + this.add_controller(motionController); } append(widget) @@ -189,44 +228,79 @@ class ClapperRevealerBottom extends CustomRevealer this.revealerBox.remove(widget); } - setFloatingClass(isFloating) + _onMotion(controller, x, y) { - if(isFloating === this.revealerBox.has_css_class('floatingcontrols')) - return; + const clapperWidget = this.root.child; + clapperWidget._clearTimeout('hideControls'); + } +}); - const action = (isFloating) ? 'add' : 'remove'; - this.revealerBox[`${action}_css_class`]('floatingcontrols'); +var ControlsRevealer = GObject.registerClass( +class ClapperControlsRevealer extends Gtk.Revealer +{ + _init() + { + super._init({ + transition_duration: 600, + transition_type: Gtk.RevealerTransitionType.SLIDE_DOWN, + reveal_child: true, + }); + + this.connect('notify::child-revealed', this._onControlsRevealed.bind(this)); } - set_visible(isVisible) + toggleReveal() { - const isChange = super.set_visible(isVisible); - if(!isChange || !this.can_focus) return; + /* Prevent interrupting transition */ + if(this.reveal_child !== this.child_revealed) + return; - const parent = this.get_parent(); - const playerWidget = parent.get_first_child(); - if(!playerWidget) return; + const { widget } = this.root.child.player; - if(isVisible) { - const box = this.get_first_child(); - if(!box) return; + if(this.child_revealed) { + const [width] = this.root.get_default_size(); + const height = widget.get_height(); - const controls = box.get_first_child(); - if(!controls) return; - - const togglePlayButton = controls.get_first_child(); - if(togglePlayButton) { - togglePlayButton.grab_focus(); - debug('focus moved to toggle play button'); - } - playerWidget.set_can_focus(false); + this.add_tick_callback( + this._onUnrevealTick.bind(this, widget, width, height) + ); } - else { - playerWidget.set_can_focus(true); - playerWidget.grab_focus(); - debug('focus moved to player widget'); + else + this.visible = true; + + widget.height_request = widget.get_height(); + this.reveal_child ^= true; + + const isFloating = !this.reveal_child; + DBus.shellWindowEval('make_above', isFloating); + + const isStick = (isFloating && settings.get_boolean('floating-stick')); + DBus.shellWindowEval('stick', isStick); + } + + _onControlsRevealed() + { + if(this.child_revealed) { + const clapperWidget = this.root.child; + const [width, height] = this.root.get_default_size(); + + clapperWidget.player.widget.height_request = -1; + this.root.set_default_size(width, height); } } + + _onUnrevealTick(playerWidget, width, height) + { + const isRevealed = this.child_revealed; + + if(!isRevealed) { + playerWidget.height_request = -1; + this.visible = false; + } + this.root.set_default_size(width, height); + + return isRevealed; + } }); var ButtonsRevealer = GObject.registerClass( @@ -251,18 +325,6 @@ class ClapperButtonsRevealer extends Gtk.Revealer } } - set_reveal_child(isReveal) - { - if(this.reveal_child === isReveal) - return; - - const grandson = this.child.get_first_child(); - if(grandson && grandson.isFloating && !grandson.isFullscreen) - return; - - super.set_reveal_child(isReveal); - } - append(widget) { this.get_child().append(widget); diff --git a/src/widget.js b/src/widget.js index 99e297a7..950d9f81 100644 --- a/src/widget.js +++ b/src/widget.js @@ -1,4 +1,4 @@ -const { Gdk, GLib, GObject, GstClapper, Gtk } = imports.gi; +const { Gdk, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi; const { Controls } = imports.src.controls; const Debug = imports.src.debug; const Dialogs = imports.src.dialogs; @@ -20,15 +20,27 @@ class ClapperWidget extends Gtk.Grid * separately as a pre-made GTK widget */ Misc.loadCustomCss(); + this.posX = 0; + this.posY = 0; + this.windowSize = JSON.parse(settings.get_string('window-size')); - this.floatSize = JSON.parse(settings.get_string('float-size')); this.layoutWidth = 0; - this.fullscreenMode = false; - this.floatingMode = false; + this.isFullscreenMode = false; this.isSeekable = false; + this.isMobileMonitor = false; + + this.isDragAllowed = false; + this.isSwipePerformed = false; + + this.isCursorInPlayer = false; + this.isPopoverOpen = false; + + this._hideControlsTimeout = null; + this._updateTimeTimeout = null; this.needsTracksUpdate = true; + this.needsCursorRestore = false; this.overlay = new Gtk.Overlay(); this.revealerTop = new Revealers.RevealerTop(); @@ -41,12 +53,17 @@ class ClapperWidget extends Gtk.Grid this.controlsBox.add_css_class('controlsbox'); this.controlsBox.append(this.controls); + this.controlsRevealer = new Revealers.ControlsRevealer(); + this.controlsRevealer.set_child(this.controlsBox); + this.attach(this.overlay, 0, 0, 1, 1); - this.attach(this.controlsBox, 0, 1, 1, 1); + this.attach(this.controlsRevealer, 0, 1, 1, 1); this.mapSignal = this.connect('map', this._onMap.bind(this)); this.player = new Player(); + const playerWidget = this.player.widget; + this.controls.elapsedButton.scrolledWindow.set_child(this.player.playlistWidget); this.controls.speedAdjustment.bind_property( 'value', this.player, 'rate', GObject.BindingFlags.BIDIRECTIONAL @@ -58,39 +75,52 @@ class ClapperWidget extends Gtk.Grid /* FIXME: re-enable once ported to new GstPlayer API with messages bus */ //this.player.connect('volume-changed', this._onPlayerVolumeChanged.bind(this)); - this.overlay.set_child(this.player.widget); + this.overlay.set_child(playerWidget); this.overlay.add_overlay(this.revealerTop); this.overlay.add_overlay(this.revealerBottom); - const motionController = new Gtk.EventControllerMotion(); - motionController.connect('leave', this._onLeave.bind(this)); - this.add_controller(motionController); + const clickGesture = this._getClickGesture(); + playerWidget.add_controller(clickGesture); + const clickGestureTop = this._getClickGesture(); + this.revealerTop.add_controller(clickGestureTop); - const topClickGesture = new Gtk.GestureClick(); - topClickGesture.set_button(0); - topClickGesture.connect('pressed', this.player._onWidgetPressed.bind(this.player)); - this.revealerTop.add_controller(topClickGesture); + const dragGesture = this._getDragGesture(); + playerWidget.add_controller(dragGesture); + const dragGestureTop = this._getDragGesture(); + this.revealerTop.add_controller(dragGestureTop); - const topMotionController = new Gtk.EventControllerMotion(); - topMotionController.connect('motion', this.player._onWidgetMotion.bind(this.player)); - this.revealerTop.add_controller(topMotionController); + const swipeGesture = this._getSwipeGesture(); + playerWidget.add_controller(swipeGesture); + const swipeGestureTop = this._getSwipeGesture(); + this.revealerTop.add_controller(swipeGestureTop); - const topScrollController = new Gtk.EventControllerScroll(); - topScrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); - topScrollController.connect('scroll', this.player._onScroll.bind(this.player)); - this.revealerTop.add_controller(topScrollController); + const scrollController = this._getScrollController(); + playerWidget.add_controller(scrollController); + const scrollControllerTop = this._getScrollController(); + this.revealerTop.add_controller(scrollControllerTop); + + const motionController = this._getMotionController(); + playerWidget.add_controller(motionController); + const motionControllerTop = this._getMotionController(); + this.revealerTop.add_controller(motionControllerTop); + + const dropTarget = this._getDropTarget(); + playerWidget.add_controller(dropTarget); + const dropTargetTop = this._getDropTarget(); + this.revealerTop.add_controller(dropTargetTop); } - revealControls(isReveal) + revealControls(isAllowInput) { - for(let pos of ['Top', 'Bottom']) - this[`revealer${pos}`].revealChild(isReveal); - } + this._checkSetUpdateTimeInterval(); - showControls(isShow) - { - for(let pos of ['Top', 'Bottom']) - this[`revealer${pos}`].showChild(isShow); + this.revealerTop.revealChild(true); + this.revealerBottom.revealChild(true); + + if(isAllowInput) + this.setControlsCanFocus(true); + + this._setHideControlsTimeout(); } toggleFullscreen() @@ -98,98 +128,55 @@ class ClapperWidget extends Gtk.Grid const root = this.get_root(); if(!root) return; - const un = (this.fullscreenMode) ? 'un' : ''; + const un = (this.isFullscreenMode) ? 'un' : ''; root[`${un}fullscreen`](); } setFullscreenMode(isFullscreen) { - if(this.fullscreenMode === isFullscreen) + if(this.isFullscreenMode === isFullscreen) return; - this.fullscreenMode = isFullscreen; + debug('changing fullscreen mode'); + this.isFullscreenMode = isFullscreen; const root = this.get_root(); const action = (isFullscreen) ? 'add' : 'remove'; root[action + '_css_class']('gpufriendlyfs'); - if(!this.floatingMode) - this._changeControlsPlacement(isFullscreen); - else { - this._setWindowFloating(!isFullscreen); - this.revealerBottom.setFloatingClass(!isFullscreen); - this.controls.setFloatingMode(!isFullscreen); - this.controls.unfloatButton.set_visible(!isFullscreen); - } + if(!this.isMobileMonitor) + root[action + '_css_class']('tvmode'); + if(!isFullscreen) + this._clearTimeout('updateTime'); + + this._changeControlsPlacement(isFullscreen); this.controls.setFullscreenMode(isFullscreen); - this.showControls(isFullscreen); - this.player.widget.grab_focus(); + this.revealerTop.setFullscreenMode(isFullscreen, this.isMobileMonitor); + + if(this.revealerTop.child_revealed) + this._checkSetUpdateTimeInterval(); + + this.setControlsCanFocus(false); if(this.player.playOnFullscreen && isFullscreen) { this.player.playOnFullscreen = false; this.player.play(); } + + debug(`interface in fullscreen mode: ${isFullscreen}`); } - setFloatingMode(isFloating) + setControlsCanFocus(isControlsFocus) { - if(this.floatingMode === isFloating) - return; + this.revealerBottom.can_focus = isControlsFocus; + this.player.widget.can_focus = !isControlsFocus; - const root = this.get_root(); - const size = root.get_default_size(); + const focusWidget = (isControlsFocus) + ? this.controls.togglePlayButton + : this.player.widget; - this._saveWindowSize(size); - - if(isFloating) { - this.windowSize = size; - this.player.widget.set_size_request(192, 108); - } - else { - this.floatSize = size; - this.player.widget.set_size_request(-1, -1); - } - - this.floatingMode = isFloating; - - this.revealerBottom.setFloatingClass(isFloating); - this._changeControlsPlacement(isFloating); - this.controls.setFloatingMode(isFloating); - this.controls.unfloatButton.set_visible(isFloating); - this._setWindowFloating(isFloating); - - const resize = (isFloating) - ? this.floatSize - : this.windowSize; - - root.set_default_size(resize[0], resize[1]); - debug(`resized window: ${resize[0]}x${resize[1]}`); - - this.revealerBottom.showChild(false); - this.player.widget.grab_focus(); - } - - _setWindowFloating(isFloating) - { - const root = this.get_root(); - const cssClass = 'floatingwindow'; - - if(isFloating === root.has_css_class(cssClass)) - return; - - const action = (isFloating) ? 'add' : 'remove'; - root[action + '_css_class'](cssClass); - } - - _saveWindowSize(size) - { - const rootName = (this.floatingMode) - ? 'float' - : 'window'; - - settings.set_string(`${rootName}-size`, JSON.stringify(size)); - debug(`saved ${rootName} size: ${size[0]}x${size[1]}`); + focusWidget.grab_focus(); } _changeControlsPlacement(isOnTop) @@ -212,8 +199,8 @@ class ClapperWidget extends Gtk.Grid if(!mediaInfo) return GLib.SOURCE_REMOVE; - /* Set titlebar media title and path */ - this.updateTitles(mediaInfo); + /* Set titlebar media title */ + this.updateTitle(mediaInfo); /* Show/hide position scale on LIVE */ const isLive = mediaInfo.is_live(); @@ -322,26 +309,21 @@ class ClapperWidget extends Gtk.Grid return GLib.SOURCE_REMOVE; } - updateTitles(mediaInfo) + updateTitle(mediaInfo) { let title = mediaInfo.get_title(); - let subtitle = this.player.playlistWidget.getActiveFilename(); if(!title) { + const subtitle = this.player.playlistWidget.getActiveFilename(); + title = (subtitle.includes('.')) ? subtitle.split('.').slice(0, -1).join('.') : subtitle; - - subtitle = null; } - const root = this.get_root(); - const headerbar = root.get_titlebar(); - - if(headerbar && headerbar.updateHeaderBar) - headerbar.updateHeaderBar(title, subtitle); - - this.revealerTop.setMediaTitle(title); + this.root.title = title; + this.revealerTop.title = title; + this.revealerTop.showTitle = true; } updateTime() @@ -564,15 +546,16 @@ class ClapperWidget extends Gtk.Grid _onStateNotify(toplevel) { + const isMaximized = Boolean( + toplevel.state & Gdk.ToplevelState.MAXIMIZED + ); const isFullscreen = Boolean( toplevel.state & Gdk.ToplevelState.FULLSCREEN ); + const headerBar = this.revealerTop.headerBar; - if(this.fullscreenMode === isFullscreen) - return; - + headerBar.setMaximized(isMaximized); this.setFullscreenMode(isFullscreen); - debug(`interface in fullscreen mode: ${isFullscreen}`); } _onLayoutUpdate(surface, width, height) @@ -584,18 +567,6 @@ class ClapperWidget extends Gtk.Grid this.controls._onPlayerResize(width, height); } - _onLeave(controller) - { - if( - this.fullscreenMode - || !this.floatingMode - || this.player.isWidgetDragging - ) - return; - - this.revealerBottom.revealChild(false); - } - _onMap() { this.disconnect(this.mapSignal); @@ -616,11 +587,327 @@ class ClapperWidget extends Gtk.Grid const monitorWidth = Math.max(geometry.width, geometry.height); if(monitorWidth < 1280) { - this.controls.isMobileMonitor = true; + this.isMobileMonitor = true; debug('mobile monitor detected'); } surface.connect('notify::state', this._onStateNotify.bind(this)); surface.connect('layout', this._onLayoutUpdate.bind(this)); } + + _clearTimeout(name) + { + if(!this[`_${name}Timeout`]) + return; + + GLib.source_remove(this[`_${name}Timeout`]); + this[`_${name}Timeout`] = null; + + if(name === 'updateTime') + debug('cleared update time interval'); + } + + _setHideControlsTimeout() + { + this._clearTimeout('hideControls'); + + let time = 2500; + + if(this.isFullscreenMode && !this.isMobileMonitor) + time += 1500; + + this._hideControlsTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, () => { + this._hideControlsTimeout = null; + + if(this.isCursorInPlayer) { + const blankCursor = Gdk.Cursor.new_from_name('none', null); + + this.player.widget.set_cursor(blankCursor); + this.revealerTop.set_cursor(blankCursor); + this.needsCursorRestore = true; + } + if(!this.isPopoverOpen) { + this._clearTimeout('updateTime'); + + this.revealerTop.revealChild(false); + this.revealerBottom.revealChild(false); + } + this.setControlsCanFocus(false); + + return GLib.SOURCE_REMOVE; + }); + } + + _checkSetUpdateTimeInterval() + { + if( + this.isFullscreenMode + && !this.isMobileMonitor + && !this._updateTimeTimeout + ) { + this._setUpdateTimeInterval(); + } + } + + _setUpdateTimeInterval() + { + this._clearTimeout('updateTime'); + + const nextUpdate = this.updateTime(); + + if(nextUpdate === null) + return; + + this._updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => { + this._updateTimeTimeout = null; + + if(this.isFullscreenMode) + this._setUpdateTimeInterval(); + + return GLib.SOURCE_REMOVE; + }); + } + + _getClickGesture() + { + const clickGesture = new Gtk.GestureClick(); + clickGesture.set_button(0); + clickGesture.connect('pressed', this._onPressed.bind(this)); + clickGesture.connect('released', this._onReleased.bind(this)); + + return clickGesture; + } + + _getDragGesture() + { + const dragGesture = new Gtk.GestureDrag(); + dragGesture.connect('drag-update', this._onDragUpdate.bind(this)); + + return dragGesture; + } + + _getSwipeGesture() + { + const swipeGesture = new Gtk.GestureSwipe({ + touch_only: true, + }); + swipeGesture.connect('swipe', this._onSwipe.bind(this)); + swipeGesture.connect('update', this._onSwipeUpdate.bind(this)); + + return swipeGesture; + } + + _getScrollController() + { + const scrollController = new Gtk.EventControllerScroll(); + scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); + scrollController.connect('scroll', this._onScroll.bind(this)); + + return scrollController; + } + + _getMotionController() + { + const motionController = new Gtk.EventControllerMotion(); + motionController.connect('enter', this._onEnter.bind(this)); + motionController.connect('leave', this._onLeave.bind(this)); + motionController.connect('motion', this._onMotion.bind(this)); + + return motionController; + } + + _getDropTarget() + { + const dropTarget = new Gtk.DropTarget({ + actions: Gdk.DragAction.COPY, + }); + dropTarget.set_gtypes([GObject.TYPE_STRING]); + dropTarget.connect('drop', this._onDataDrop.bind(this)); + + return dropTarget; + } + + _getIsSwipeOk(velocity, otherVelocity) + { + if(!velocity) + return false; + + const absVel = Math.abs(velocity); + + if(absVel < 20 || Math.abs(otherVelocity) * 1.5 >= absVel) + return false; + + return this.isFullscreenMode; + } + + _onPressed(gesture, nPress, x, y) + { + const button = gesture.get_current_button(); + const isDouble = (nPress % 2 == 0); + this.isDragAllowed = !isDouble; + this.isSwipePerformed = false; + + switch(button) { + case Gdk.BUTTON_PRIMARY: + if(isDouble) + this.toggleFullscreen(); + break; + case Gdk.BUTTON_SECONDARY: + this.player.toggle_play(); + break; + default: + break; + } + } + + _onReleased(gesture, nPress, x, y) + { + /* Reveal if touch was not a swipe or was already revealed */ + if(!this.isSwipePerformed || this.revealerBottom.child_revealed) { + const { source } = gesture.get_device(); + + switch(source) { + case Gdk.InputSource.PEN: + case Gdk.InputSource.TOUCHSCREEN: + this.revealControls(); + break; + default: + break; + } + } + } + + _onDragUpdate(gesture, offsetX, offsetY) + { + if(!this.isDragAllowed || this.isFullscreenMode) + return; + + const { gtk_double_click_distance } = this.get_settings(); + + if ( + Math.abs(offsetX) > gtk_double_click_distance + || Math.abs(offsetY) > gtk_double_click_distance + ) { + const [isActive, startX, startY] = gesture.get_start_point(); + if(!isActive) return; + + const playerWidget = this.player.widget; + + const native = playerWidget.get_native(); + if(!native) return; + + let [isShared, winX, winY] = playerWidget.translate_coordinates( + native, startX, startY + ); + if(!isShared) return; + + const [nativeX, nativeY] = native.get_surface_transform(); + winX += nativeX; + winY += nativeY; + + native.get_surface().begin_move( + gesture.get_device(), + gesture.get_current_button(), + winX, + winY, + gesture.get_current_event_time() + ); + + gesture.reset(); + } + } + + _onSwipe(gesture, velocityX, velocityY) + { + if(!this._getIsSwipeOk(velocityX, velocityY)) + return; + + this._onScroll(gesture, -velocityX, 0); + this.isSwipePerformed = true; + } + + _onSwipeUpdate(gesture, sequence) + { + const [isCalc, velocityX, velocityY] = gesture.get_velocity(); + if(!isCalc) return; + + if(!this._getIsSwipeOk(velocityY, velocityX)) + return; + + const isIncrease = velocityY < 0; + + this.player.adjust_volume(isIncrease, 0.01); + this.isSwipePerformed = true; + } + + _onScroll(controller, dx, dy) + { + const isHorizontal = (Math.abs(dx) >= Math.abs(dy)); + const isIncrease = (isHorizontal) ? dx < 0 : dy < 0; + + if(isHorizontal) { + this.player.adjust_position(isIncrease); + const value = Math.round(this.controls.positionScale.get_value()); + this.player.seek_seconds(value); + } + else + this.player.adjust_volume(isIncrease); + + return true; + } + + _onEnter(controller, x, y) + { + this.isCursorInPlayer = true; + } + + _onLeave(controller) + { + if(this.isFullscreenMode) + return; + + this.isCursorInPlayer = false; + } + + _onMotion(controller, posX, posY) + { + this.isCursorInPlayer = true; + + /* GTK4 sometimes generates motions with same coords */ + if(this.posX === posX && this.posY === posY) + return; + + /* Do not show cursor on small movements */ + if( + Math.abs(this.posX - posX) >= 0.5 + || Math.abs(this.posY - posY) >= 0.5 + ) { + if(this.needsCursorRestore) { + const defaultCursor = Gdk.Cursor.new_from_name('default', null); + + this.player.widget.set_cursor(defaultCursor); + this.revealerTop.set_cursor(defaultCursor); + this.needsCursorRestore = false; + } + this.revealControls(); + } + + this.posX = posX; + this.posY = posY; + } + + _onDataDrop(dropTarget, value, x, y) + { + const playlist = value.split(/\r?\n/).filter(uri => { + return Gst.uri_is_valid(uri); + }); + + if(!playlist.length) + return false; + + this.player.set_playlist(playlist); + this.root.application.activate(); + + return true; + } });