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.
This commit is contained in:
Rafostar
2020-11-03 17:40:19 +01:00
parent ba54a36058
commit ff58713426
9 changed files with 244 additions and 53 deletions

View File

@@ -8,7 +8,7 @@
- [X] Audio visualizations (VLC) - [X] Audio visualizations (VLC)
- [X] Clock with current hour and "Ends at" time on top overlay (Kodi) - [X] Clock with current hour and "Ends at" time on top overlay (Kodi)
- [ ] Auto select subtitles matching OS language (Totem) - [ ] Auto select subtitles matching OS language (Totem)
- [ ] Picture-in-Picture mode window - [X] Picture-in-Picture mode window (floating window)
- [ ] Touch gestures/swipes support - [ ] Touch gestures/swipes support
- [ ] Media playlists (with exporting to file e.g: .vplist) - [ ] Media playlists (with exporting to file e.g: .vplist)
- [ ] Customizable seek time - [ ] Customizable seek time

View File

@@ -43,6 +43,23 @@ class ClapperApp extends Gtk.Application
); );
this.add_action(simpleAction); 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 clapperPath = Misc.getClapperPath();
let uiBuilder = Gtk.Builder.new_from_file( let uiBuilder = Gtk.Builder.new_from_file(
`${clapperPath}/ui/clapper.ui` `${clapperPath}/ui/clapper.ui`
@@ -53,23 +70,6 @@ class ClapperApp extends Gtk.Application
}; };
let headerBar = new HeaderBar(window, models); let headerBar = new HeaderBar(window, models);
window.set_titlebar(headerBar); 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() vfunc_activate()

View File

@@ -17,7 +17,11 @@ class ClapperCustomButton extends Gtk.Button
super._init(opts); super._init(opts);
this.floatUnaffected = false;
this.wantedVisible = true;
this.isFullscreen = false; this.isFullscreen = false;
this.isFloating = false;
this.add_css_class('flat'); this.add_css_class('flat');
} }
@@ -30,6 +34,32 @@ class ClapperCustomButton extends Gtk.Button
this.isFullscreen = isFullscreen; 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() vfunc_clicked()
{ {
if(!this.isFullscreen) if(!this.isFullscreen)
@@ -48,6 +78,14 @@ class ClapperIconButton extends CustomButton
super._init({ super._init({
icon_name: icon, 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, label: text,
single_line_mode: true, single_line_mode: true,
}); });
this.customLabel.add_css_class('labelbutton'); this.customLabel.add_css_class('labelbutton');
this.set_child(this.customLabel); this.set_child(this.customLabel);
} }
@@ -105,6 +142,7 @@ class ClapperPopoverButton extends IconButton
{ {
super._init(icon); super._init(icon);
this.floatUnaffected = false;
this.popover = new Gtk.Popover({ this.popover = new Gtk.Popover({
position: Gtk.PositionType.TOP, position: Gtk.PositionType.TOP,
}); });
@@ -131,7 +169,7 @@ class ClapperPopoverButton extends IconButton
this.popover.set_offset(0, -this.margin_top); this.popover.set_offset(0, -this.margin_top);
let cssClass = 'osd'; let cssClass = 'osd';
if(isFullscreen == this.popover.has_css_class(cssClass)) if(isFullscreen === this.popover.has_css_class(cssClass))
return; return;
let action = (isFullscreen) ? 'add' : 'remove'; let action = (isFullscreen) ? 'add' : 'remove';

View File

@@ -56,6 +56,12 @@ class ClapperControls extends Gtk.Box
this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this)); this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this));
this.unfullscreenButton.set_visible(false); 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(); let keyController = new Gtk.EventControllerKey();
keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this)); keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this));
keyController.connect('key-released', this._onControlsKeyReleased.bind(this)); keyController.connect('key-released', this._onControlsKeyReleased.bind(this));
@@ -74,6 +80,12 @@ class ClapperControls extends Gtk.Box
this.set_can_focus(isFullscreen); this.set_can_focus(isFullscreen);
} }
setFloatingMode(isFloating)
{
for(let button of this.buttonsArr)
button.setFloatingMode(isFloating);
}
setLiveMode(isLive, isSeekable) setLiveMode(isLive, isSeekable)
{ {
if(isLive) if(isLive)
@@ -350,6 +362,12 @@ class ClapperControls extends Gtk.Box
root.unfullscreen(); root.unfullscreen();
} }
_onUnfloatClicked(button)
{
let clapperWidget = this.get_ancestor(Gtk.Grid);
clapperWidget.setFloatingMode(false);
}
_onCheckButtonToggled(checkButton) _onCheckButtonToggled(checkButton)
{ {
if(!checkButton.get_active()) if(!checkButton.get_active())

View File

@@ -10,6 +10,7 @@ class ClapperHeaderBar extends Gtk.HeaderBar
}); });
this.set_title_widget(this._createWidgetForWindow(window)); this.set_title_widget(this._createWidgetForWindow(window));
let clapperWidget = window.get_child();
let addMediaButton = new Gtk.MenuButton({ let addMediaButton = new Gtk.MenuButton({
icon_name: 'list-add-symbolic', icon_name: 'list-add-symbolic',
@@ -25,11 +26,27 @@ class ClapperHeaderBar extends Gtk.HeaderBar
openMenuButton.set_popover(settingsPopover); openMenuButton.set_popover(settingsPopover);
this.pack_end(openMenuButton); 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({ let fullscreenButton = new Gtk.Button({
icon_name: 'view-fullscreen-symbolic', icon_name: 'view-fullscreen-symbolic',
}); });
fullscreenButton.connect('clicked', () => this.get_parent().fullscreen()); fullscreenButton.connect('clicked', this._onFullscreenButtonClicked.bind(this));
this.pack_end(fullscreenButton);
buttonsBox.append(fullscreenButton);
this.pack_end(buttonsBox);
} }
updateHeaderBar(title, subtitle) updateHeaderBar(title, subtitle)
@@ -72,6 +89,18 @@ class ClapperHeaderBar extends Gtk.HeaderBar
return box; return box;
} }
_onFloatButtonClicked()
{
let clapperWidget = this.get_prev_sibling();
clapperWidget.setFloatingMode(true);
}
_onFullscreenButtonClicked()
{
let window = this.get_parent();
window.fullscreen();
}
}); });
var HeaderBarPopover = GObject.registerClass( var HeaderBarPopover = GObject.registerClass(

View File

@@ -18,6 +18,7 @@ class ClapperPlayer extends PlayerBase
this.is_local_file = false; this.is_local_file = false;
this.seek_done = true; this.seek_done = true;
this.dragAllowed = false; this.dragAllowed = false;
this.isWidgetDragging = false;
this.doneStartup = false; this.doneStartup = false;
this.posX = 0; this.posX = 0;
@@ -277,7 +278,7 @@ class ClapperPlayer extends PlayerBase
if(this.cursorInPlayer) { if(this.cursorInPlayer) {
let clapperWidget = this.widget.get_ancestor(Gtk.Grid); let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
if(clapperWidget.fullscreenMode) { if(clapperWidget.fullscreenMode || clapperWidget.floatingMode) {
this._clearTimeout('updateTime'); this._clearTimeout('updateTime');
clapperWidget.revealControls(false); clapperWidget.revealControls(false);
} }
@@ -486,11 +487,12 @@ class ClapperPlayer extends PlayerBase
_onWidgetEnter(controller, x, y) _onWidgetEnter(controller, x, y)
{ {
this.cursorInPlayer = true; this.cursorInPlayer = true;
this.isWidgetDragging = false;
this._setHideCursorTimeout(); this._setHideCursorTimeout();
let clapperWidget = this.widget.get_ancestor(Gtk.Grid); let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
if(clapperWidget.fullscreenMode) if(clapperWidget.fullscreenMode || clapperWidget.floatingMode)
this._setHideControlsTimeout(); this._setHideControlsTimeout();
} }
@@ -521,7 +523,12 @@ class ClapperPlayer extends PlayerBase
let clapperWidget = this.widget.get_ancestor(Gtk.Grid); 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) if(!this._updateTimeTimeout)
this._setUpdateTimeInterval(); this._setUpdateTimeInterval();
@@ -565,6 +572,7 @@ class ClapperPlayer extends PlayerBase
let root = this.widget.get_root(); let root = this.widget.get_root();
if(!root) return; if(!root) return;
this.isWidgetDragging = true;
root.get_surface().begin_move( root.get_surface().begin_move(
gesture.get_device(), gesture.get_device(),
gesture.get_current_button(), gesture.get_current_button(),
@@ -606,7 +614,7 @@ class ClapperPlayer extends PlayerBase
this.stop(); this.stop();
let clapperWidget = this.widget.get_ancestor(Gtk.Grid); let clapperWidget = this.widget.get_ancestor(Gtk.Grid);
if(!clapperWidget.fullscreenMode) { if(!clapperWidget.fullscreenMode && !clapperWidget.floatingMode) {
let size = window.get_size(); let size = window.get_size();
if(size[0] > 0 && size[1] > 0) { if(size[0] > 0 && size[1] > 0) {
this.settings.set_string('window-size', JSON.stringify(size)); this.settings.set_string('window-size', JSON.stringify(size));

View File

@@ -190,6 +190,15 @@ class ClapperRevealerBottom extends CustomRevealer
this.revealerBox.remove(widget); 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) set_visible(isVisible)
{ {
let isChange = super.set_visible(isVisible); let isChange = super.set_visible(isVisible);

View File

@@ -36,6 +36,7 @@ var Widget = GObject.registerClass({
); );
this.fullscreenMode = false; this.fullscreenMode = false;
this.floatingMode = false;
this.isSeekable = false; this.isSeekable = false;
this.lastRevealerEventTime = 0; this.lastRevealerEventTime = 0;
@@ -63,6 +64,10 @@ var Widget = GObject.registerClass({
this.overlay.set_child(this.player.widget); this.overlay.set_child(this.player.widget);
this.overlay.add_overlay(this.revealerTop); this.overlay.add_overlay(this.revealerTop);
this.overlay.add_overlay(this.revealerBottom); this.overlay.add_overlay(this.revealerBottom);
let motionController = new Gtk.EventControllerMotion();
motionController.connect('leave', this._onLeave.bind(this));
this.add_controller(motionController);
} }
revealControls(isReveal) revealControls(isReveal)
@@ -86,6 +91,73 @@ var Widget = GObject.registerClass({
root[`${un}fullscreen`](); 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) _onMediaInfoUpdated(player, mediaInfo)
{ {
player.disconnect(this.mediaInfoSignal); player.disconnect(this.mediaInfoSignal);
@@ -169,10 +241,9 @@ var Widget = GObject.registerClass({
this.showVisualizationsButton(isShowVis); this.showVisualizationsButton(isShowVis);
} }
if(!parsedInfo[`${type}Tracks`].length) { if(!parsedInfo[`${type}Tracks`].length) {
if(this.controls[`${type}TracksButton`].visible) { debug(`hiding popover button without contents: ${type}`);
debug(`hiding popover button without contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(false);
this.controls[`${type}TracksButton`].set_visible(false);
}
continue; continue;
} }
this.controls.addCheckButtons( this.controls.addCheckButtons(
@@ -180,10 +251,8 @@ var Widget = GObject.registerClass({
parsedInfo[`${type}Tracks`], parsedInfo[`${type}Tracks`],
activeId activeId
); );
if(!this.controls[`${type}TracksButton`].visible) { debug(`showing popover button with contents: ${type}`);
debug(`showing popover button with contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(true);
this.controls[`${type}TracksButton`].set_visible(true);
}
} }
this.mediaInfoSignal = null; this.mediaInfoSignal = null;
@@ -366,32 +435,27 @@ var Widget = GObject.registerClass({
let isFullscreen = Boolean( let isFullscreen = Boolean(
toplevel.state & Gdk.ToplevelState.FULLSCREEN toplevel.state & Gdk.ToplevelState.FULLSCREEN
); );
if(this.fullscreenMode === isFullscreen) if(this.fullscreenMode === isFullscreen)
return; return;
this.fullscreenMode = isFullscreen; this.setFullscreenMode(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.emit('fullscreen-changed', isFullscreen); this.emit('fullscreen-changed', isFullscreen);
debug(`interface in fullscreen mode: ${isFullscreen}`); debug(`interface in fullscreen mode: ${isFullscreen}`);
} }
_onLeave(controller)
{
if(
this.fullscreenMode
|| !this.floatingMode
|| this.player.isWidgetDragging
)
return;
this.revealerBottom.revealChild(false);
}
_onMap() _onMap()
{ {
this.disconnect(this.mapSignal); this.disconnect(this.mapSignal);

View File

@@ -98,6 +98,31 @@ scale marks {
min-width: 6px; 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 */ /* Preferences */
.prefsnotebook grid { .prefsnotebook grid {
margin: 10px; margin: 10px;