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;