From ff587134267e29f40cea6ff01c8bf4da652469d4 Mon Sep 17 00:00:00 2001 From: Rafostar <40623528+Rafostar@users.noreply.github.com> Date: Tue, 3 Nov 2020 17:40:19 +0100 Subject: [PATCH] Add "Floating Window Mode" A simple borderless window floating on desktop. Window can be resized and moved by dragging. It also has some minimalistic controls showing on top of the video when cursor is hovering over it.\n\n This was a feature originally requested by @zahid1905. --- TODO.md | 2 +- clapper_src/app.js | 34 +++++------ clapper_src/buttons.js | 42 +++++++++++++- clapper_src/controls.js | 18 ++++++ clapper_src/headerbar.js | 33 ++++++++++- clapper_src/player.js | 16 ++++-- clapper_src/revealers.js | 9 +++ clapper_src/widget.js | 118 ++++++++++++++++++++++++++++++--------- css/styles.css | 25 +++++++++ 9 files changed, 244 insertions(+), 53 deletions(-) diff --git a/TODO.md b/TODO.md index 8f1c7c09..c3c6f498 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,7 @@ - [X] Audio visualizations (VLC) - [X] Clock with current hour and "Ends at" time on top overlay (Kodi) - [ ] Auto select subtitles matching OS language (Totem) -- [ ] Picture-in-Picture mode window +- [X] Picture-in-Picture mode window (floating window) - [ ] Touch gestures/swipes support - [ ] Media playlists (with exporting to file e.g: .vplist) - [ ] Customizable seek time diff --git a/clapper_src/app.js b/clapper_src/app.js index 1ff32931..61bf48ee 100644 --- a/clapper_src/app.js +++ b/clapper_src/app.js @@ -43,6 +43,23 @@ class ClapperApp extends Gtk.Application ); this.add_action(simpleAction); } + + let clapperWidget = new Widget(); + window.set_child(clapperWidget); + + let size = clapperWidget.player.settings.get_string('window-size'); + try { + size = JSON.parse(size); + } + catch(err) { + debug(err); + size = null; + } + if(size) { + window.set_default_size(size[0], size[1]); + debug(`restored window dimensions: ${size[0]}x${size[1]}`); + } + let clapperPath = Misc.getClapperPath(); let uiBuilder = Gtk.Builder.new_from_file( `${clapperPath}/ui/clapper.ui` @@ -53,23 +70,6 @@ class ClapperApp extends Gtk.Application }; let headerBar = new HeaderBar(window, models); window.set_titlebar(headerBar); - - let clapperWidget = new Widget(); - let size = clapperWidget.player.settings.get_string('window-size'); - try { - size = JSON.parse(size); - } - catch(err) { - debug(err); - size = null; - } - - if(size) { - window.set_default_size(size[0], size[1]); - debug(`restored window dimensions: ${size[0]}x${size[1]}`); - } - - window.set_child(clapperWidget); } vfunc_activate() diff --git a/clapper_src/buttons.js b/clapper_src/buttons.js index 7e829ab4..e6e8ed3f 100644 --- a/clapper_src/buttons.js +++ b/clapper_src/buttons.js @@ -17,7 +17,11 @@ 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'); } @@ -30,6 +34,32 @@ 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) @@ -48,6 +78,14 @@ class ClapperIconButton extends CustomButton super._init({ icon_name: icon, }); + this.floatUnaffected = true; + } + + setFullscreenMode(isFullscreen) + { + /* Redraw icon after style class change */ + this.set_icon_name(this.icon_name); + super.setFullscreenMode(isFullscreen); } }); @@ -87,7 +125,6 @@ class ClapperLabelButton extends CustomButton label: text, single_line_mode: true, }); - this.customLabel.add_css_class('labelbutton'); this.set_child(this.customLabel); } @@ -105,6 +142,7 @@ class ClapperPopoverButton extends IconButton { super._init(icon); + this.floatUnaffected = false; this.popover = new Gtk.Popover({ position: Gtk.PositionType.TOP, }); @@ -131,7 +169,7 @@ class ClapperPopoverButton extends IconButton this.popover.set_offset(0, -this.margin_top); let cssClass = 'osd'; - if(isFullscreen == this.popover.has_css_class(cssClass)) + if(isFullscreen === this.popover.has_css_class(cssClass)) return; let action = (isFullscreen) ? 'add' : 'remove'; diff --git a/clapper_src/controls.js b/clapper_src/controls.js index 6db6e16b..e9a618c9 100644 --- a/clapper_src/controls.js +++ b/clapper_src/controls.js @@ -56,6 +56,12 @@ 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); + let keyController = new Gtk.EventControllerKey(); keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this)); keyController.connect('key-released', this._onControlsKeyReleased.bind(this)); @@ -74,6 +80,12 @@ class ClapperControls extends Gtk.Box this.set_can_focus(isFullscreen); } + setFloatingMode(isFloating) + { + for(let button of this.buttonsArr) + button.setFloatingMode(isFloating); + } + setLiveMode(isLive, isSeekable) { if(isLive) @@ -350,6 +362,12 @@ class ClapperControls extends Gtk.Box root.unfullscreen(); } + _onUnfloatClicked(button) + { + let clapperWidget = this.get_ancestor(Gtk.Grid); + clapperWidget.setFloatingMode(false); + } + _onCheckButtonToggled(checkButton) { if(!checkButton.get_active()) diff --git a/clapper_src/headerbar.js b/clapper_src/headerbar.js index 27af8a83..6a2158e2 100644 --- a/clapper_src/headerbar.js +++ b/clapper_src/headerbar.js @@ -10,6 +10,7 @@ class ClapperHeaderBar extends Gtk.HeaderBar }); this.set_title_widget(this._createWidgetForWindow(window)); + let clapperWidget = window.get_child(); let addMediaButton = new Gtk.MenuButton({ icon_name: 'list-add-symbolic', @@ -25,11 +26,27 @@ class ClapperHeaderBar extends Gtk.HeaderBar openMenuButton.set_popover(settingsPopover); this.pack_end(openMenuButton); + let buttonsBox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + }); + buttonsBox.add_css_class('linked'); + + let floatButton = new Gtk.Button({ + icon_name: 'preferences-desktop-remote-desktop-symbolic', + }); + floatButton.connect('clicked', this._onFloatButtonClicked.bind(this)); + clapperWidget.controls.unfloatButton.bind_property('visible', this, 'visible', + GObject.BindingFlags.INVERT_BOOLEAN + ); + buttonsBox.append(floatButton); + let fullscreenButton = new Gtk.Button({ icon_name: 'view-fullscreen-symbolic', }); - fullscreenButton.connect('clicked', () => this.get_parent().fullscreen()); - this.pack_end(fullscreenButton); + fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this)); + + buttonsBox.append(fullscreenButton); + this.pack_end(buttonsBox); } updateHeaderBar(title, subtitle) @@ -72,6 +89,18 @@ class ClapperHeaderBar extends Gtk.HeaderBar return box; } + + _onFloatButtonClicked() + { + let clapperWidget = this.get_prev_sibling(); + clapperWidget.setFloatingMode(true); + } + + _onFullscreenButtonClicked() + { + let window = this.get_parent(); + window.fullscreen(); + } }); var HeaderBarPopover = GObject.registerClass( diff --git a/clapper_src/player.js b/clapper_src/player.js index 5fead4c7..152971a1 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -18,6 +18,7 @@ class ClapperPlayer extends PlayerBase this.is_local_file = false; this.seek_done = true; this.dragAllowed = false; + this.isWidgetDragging = false; this.doneStartup = false; this.posX = 0; @@ -277,7 +278,7 @@ class ClapperPlayer extends PlayerBase if(this.cursorInPlayer) { let clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode) { + if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) { this._clearTimeout('updateTime'); clapperWidget.revealControls(false); } @@ -486,11 +487,12 @@ class ClapperPlayer extends PlayerBase _onWidgetEnter(controller, x, y) { this.cursorInPlayer = true; + this.isWidgetDragging = false; this._setHideCursorTimeout(); let clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode) + if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) this._setHideControlsTimeout(); } @@ -521,7 +523,12 @@ class ClapperPlayer extends PlayerBase let clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(clapperWidget.fullscreenMode) { + 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(); @@ -565,6 +572,7 @@ class ClapperPlayer extends PlayerBase let root = this.widget.get_root(); if(!root) return; + this.isWidgetDragging = true; root.get_surface().begin_move( gesture.get_device(), gesture.get_current_button(), @@ -606,7 +614,7 @@ class ClapperPlayer extends PlayerBase this.stop(); let clapperWidget = this.widget.get_ancestor(Gtk.Grid); - if(!clapperWidget.fullscreenMode) { + if(!clapperWidget.fullscreenMode && !clapperWidget.floatingMode) { let size = window.get_size(); if(size[0] > 0 && size[1] > 0) { this.settings.set_string('window-size', JSON.stringify(size)); diff --git a/clapper_src/revealers.js b/clapper_src/revealers.js index 7fe62322..915d2e21 100644 --- a/clapper_src/revealers.js +++ b/clapper_src/revealers.js @@ -190,6 +190,15 @@ class ClapperRevealerBottom extends CustomRevealer this.revealerBox.remove(widget); } + setFloatingClass(isFloating) + { + if(isFloating === this.revealerBox.has_css_class('floatingcontrols')) + return; + + let action = (isFloating) ? 'add' : 'remove'; + this.revealerBox[`${action}_css_class`]('floatingcontrols'); + } + set_visible(isVisible) { let isChange = super.set_visible(isVisible); diff --git a/clapper_src/widget.js b/clapper_src/widget.js index 834fed88..c799528f 100644 --- a/clapper_src/widget.js +++ b/clapper_src/widget.js @@ -36,6 +36,7 @@ var Widget = GObject.registerClass({ ); this.fullscreenMode = false; + this.floatingMode = false; this.isSeekable = false; this.lastRevealerEventTime = 0; @@ -63,6 +64,10 @@ var Widget = GObject.registerClass({ this.overlay.set_child(this.player.widget); this.overlay.add_overlay(this.revealerTop); this.overlay.add_overlay(this.revealerBottom); + + let motionController = new Gtk.EventControllerMotion(); + motionController.connect('leave', this._onLeave.bind(this)); + this.add_controller(motionController); } revealControls(isReveal) @@ -86,6 +91,73 @@ var Widget = GObject.registerClass({ root[`${un}fullscreen`](); } + setFullscreenMode(isFullscreen) + { + if(this.fullscreenMode === isFullscreen) + return; + + this.fullscreenMode = isFullscreen; + + if(!this.floatingMode) + this._changeControlsPlacement(isFullscreen); + else { + this._setWindowFloating(!isFullscreen); + this.revealerBottom.setFloatingClass(!isFullscreen); + this.controls.setFloatingMode(!isFullscreen); + this.controls.unfloatButton.set_visible(!isFullscreen); + } + + this.controls.setFullscreenMode(isFullscreen); + this.showControls(isFullscreen); + this.player.widget.grab_focus(); + + if(this.player.playOnFullscreen && isFullscreen) { + this.player.playOnFullscreen = false; + this.player.play(); + } + } + + setFloatingMode(isFloating) + { + if(this.floatingMode === isFloating) + return; + + this.floatingMode = isFloating; + + this.revealerBottom.setFloatingClass(isFloating); + this._changeControlsPlacement(isFloating); + this.controls.setFloatingMode(isFloating); + this.controls.unfloatButton.set_visible(isFloating); + this.revealerBottom.showChild(isFloating); + this._setWindowFloating(isFloating); + + this.player.widget.grab_focus(); + } + + _setWindowFloating(isFloating) + { + let root = this.get_root(); + + let cssClass = 'floatingwindow'; + if(isFloating === root.has_css_class(cssClass)) + return; + + let action = (isFloating) ? 'add' : 'remove'; + root[action + '_css_class'](cssClass); + } + + _changeControlsPlacement(isOnTop) + { + if(isOnTop) { + this.remove(this.controls); + this.revealerBottom.append(this.controls); + } + else { + this.revealerBottom.remove(this.controls); + this.attach(this.controls, 0, 1, 1, 1); + } + } + _onMediaInfoUpdated(player, mediaInfo) { player.disconnect(this.mediaInfoSignal); @@ -169,10 +241,9 @@ var Widget = GObject.registerClass({ this.showVisualizationsButton(isShowVis); } if(!parsedInfo[`${type}Tracks`].length) { - if(this.controls[`${type}TracksButton`].visible) { - debug(`hiding popover button without contents: ${type}`); - this.controls[`${type}TracksButton`].set_visible(false); - } + debug(`hiding popover button without contents: ${type}`); + this.controls[`${type}TracksButton`].set_visible(false); + continue; } this.controls.addCheckButtons( @@ -180,10 +251,8 @@ var Widget = GObject.registerClass({ parsedInfo[`${type}Tracks`], activeId ); - if(!this.controls[`${type}TracksButton`].visible) { - debug(`showing popover button with contents: ${type}`); - this.controls[`${type}TracksButton`].set_visible(true); - } + debug(`showing popover button with contents: ${type}`); + this.controls[`${type}TracksButton`].set_visible(true); } this.mediaInfoSignal = null; @@ -366,32 +435,27 @@ var Widget = GObject.registerClass({ let isFullscreen = Boolean( toplevel.state & Gdk.ToplevelState.FULLSCREEN ); + if(this.fullscreenMode === isFullscreen) return; - this.fullscreenMode = isFullscreen; - - if(isFullscreen) { - this.remove(this.controls); - this.revealerBottom.append(this.controls); - } - else { - this.revealerBottom.remove(this.controls); - this.attach(this.controls, 0, 1, 1, 1); - } - - this.controls.setFullscreenMode(isFullscreen); - this.showControls(isFullscreen); - this.player.widget.grab_focus(); - - if(this.player.playOnFullscreen && isFullscreen) { - this.player.playOnFullscreen = false; - this.player.play(); - } + this.setFullscreenMode(isFullscreen); this.emit('fullscreen-changed', isFullscreen); debug(`interface in fullscreen mode: ${isFullscreen}`); } + _onLeave(controller) + { + if( + this.fullscreenMode + || !this.floatingMode + || this.player.isWidgetDragging + ) + return; + + this.revealerBottom.revealChild(false); + } + _onMap() { this.disconnect(this.mapSignal); diff --git a/css/styles.css b/css/styles.css index 14f6492f..d601b308 100644 --- a/css/styles.css +++ b/css/styles.css @@ -98,6 +98,31 @@ scale marks { min-width: 6px; } +/* Floating Mode */ +.floatingwindow { + background: none; + border-radius: 12px; +} +.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 trough highlight { + border-radius: 3px; + min-height: 12px; +} +.osd.floatingcontrols .positionscale.dragging trough highlight { + border-radius: 3px; + min-height: 12px; +} + /* Preferences */ .prefsnotebook grid { margin: 10px;