mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-29 23:32:04 +02:00
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:
2
TODO.md
2
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
|
||||
|
@@ -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()
|
||||
|
@@ -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';
|
||||
|
18
clapper_src/controls.js
vendored
18
clapper_src/controls.js
vendored
@@ -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())
|
||||
|
@@ -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(
|
||||
|
@@ -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));
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
this.controls.addCheckButtons(
|
||||
@@ -180,11 +251,9 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user